diff --git a/server/app.ts b/server/app.ts index b8ef64c..da953aa 100644 --- a/server/app.ts +++ b/server/app.ts @@ -64,24 +64,31 @@ app.use(express.json()); // parse application/json // routers app.use('/countries', countriesRouter); app.use('/v2/countries', countriesRouter); -app.use('/trees', treesRouter); + app.use('/planters', plantersRouter); +app.use('/v2/growers', growerAccountsRouter); + app.use('/organizations', organizationsRouter); app.use('/v2/organizations', organizationsRouterV2); + app.use('/species', speciesRouter); app.use('/v2/species', speciesRouterV2); + app.use('/wallets', walletsRouter); app.use('/v2/wallets', walletsRouter); -app.use('/transactions', transactionsRouter); + app.use('/tokens', tokensRouter); app.use('/v2/tokens', tokensRouter); + +app.use('/trees', treesRouter); +app.use('/v2/trees', treesRouterV2); + app.use('/v2/captures', capturesRouter); app.use('/raw-captures', rawCapturesRouter); -app.use('/v2/growers', growerAccountsRouter); -app.use('/v2/trees', treesRouterV2); app.use('/bounds', boundsRouter); app.use('/gis', gisRouter); app.use('/contract', contractsRouter); +app.use('/transactions', transactionsRouter); // Global error handler app.use(errorHandler); diff --git a/server/infra/database/CaptureRepository.ts b/server/infra/database/CaptureRepository.ts index df2cb73..d8ed638 100644 --- a/server/infra/database/CaptureRepository.ts +++ b/server/infra/database/CaptureRepository.ts @@ -1,5 +1,6 @@ import Capture from 'interfaces/Capture'; import CaptureFilter from 'interfaces/CaptureFilter'; +import CaptureLocationFilter from 'interfaces/CaptureLocationFilter'; import FilterOptions from 'interfaces/FilterOptions'; import HttpError from 'utils/HttpError'; import BaseRepository from './BaseRepository'; @@ -107,6 +108,13 @@ export default class CaptureRepository extends BaseRepository { delete filterObject.organization_id; } + // remove these filters because they'll be included manually + if (filterObject.lat && filterObject.lon && filterObject.deviation) { + delete filterObject.lat; + delete filterObject.lon; + delete filterObject.deviation; + } + result.where(filterObject); } @@ -184,6 +192,60 @@ export default class CaptureRepository extends BaseRepository { return captures; } + async getByLocation( + filterCriteria: CaptureLocationFilter, + options: FilterOptions, + ) { + const knex = this.session.getDB(); + const { sort, ...filter } = filterCriteria; + + let promise = knex.select( + knex.raw( + ` + treetracker.capture.*, + field_data.device_configuration.device_identifier, + field_data.device_configuration.manufacturer AS device_manufacturer, + treetracker.grower_account.reference_id as grower_reference_id, + treetracker.grower_account.wallet as grower_wallet, + treetracker.grower_account.location as grower_location, + field_data.session.id as session_id + FROM treetracker.capture + LEFT JOIN treetracker.grower_account + ON grower_account.id = treetracker.capture.grower_account_id + LEFT JOIN field_data.device_configuration + ON field_data.device_configuration.id = treetracker.capture.device_configuration_id + LEFT JOIN field_data.raw_capture + ON field_data.raw_capture.id = treetracker.capture.id + LEFT JOIN field_data.session + ON field_data.raw_capture.session_id = field_data.session.id + ${ + filter.lat && filter.lon && filter.deviation + ? `WHERE + ST_DWithin(treetracker.capture.estimated_geometric_location, ST_GeomFromText('POINT (${filter.lon} ${filter.lat})', 4326), ${filter.deviation}) + ` + : '' + }`, + ), + ); + + promise = promise.orderBy( + sort?.order_by || 'treetracker.capture.created_at', + sort?.order || 'desc', + ); + + const { limit, offset } = options; + if (limit) { + promise = promise.limit(limit); + } + if (offset) { + promise = promise.offset(offset); + } + + const captures = await promise; + + return captures; + } + async getCount(filterCriteria: CaptureFilter) { const knex = this.session.getDB(); const { ...filter } = filterCriteria; diff --git a/server/interfaces/CaptureLocationFilter.ts b/server/interfaces/CaptureLocationFilter.ts new file mode 100644 index 0000000..7ddfad1 --- /dev/null +++ b/server/interfaces/CaptureLocationFilter.ts @@ -0,0 +1,11 @@ +import DbModel from './DbModel'; + +interface CaptureLocationFilter extends DbModel { + session_id?: Array | undefined; + lat?: string | undefined; + lon?: string | undefined; + deviation?: string | undefined; + sort?: { order?: string; order_by?: string }; +} + +export default CaptureLocationFilter; diff --git a/server/models/Capture.ts b/server/models/Capture.ts index e67b981..ed04ac2 100644 --- a/server/models/Capture.ts +++ b/server/models/Capture.ts @@ -2,6 +2,7 @@ import CaptureRepository from 'infra/database/CaptureRepository'; import { delegateRepository } from 'infra/database/delegateRepository'; import Capture from 'interfaces/Capture'; import CaptureFilter from 'interfaces/CaptureFilter'; +import CaptureLocationFilter from 'interfaces/CaptureLocationFilter'; import FilterOptions from 'interfaces/FilterOptions'; function getByFilter( @@ -13,6 +14,21 @@ function getByFilter( }; } +function getByLocation( + captureRepository: CaptureRepository, +): ( + filter: CaptureLocationFilter, + options: FilterOptions, +) => Promise { + return async function ( + filter: CaptureLocationFilter, + options: FilterOptions, + ) { + const captures = await captureRepository.getByLocation(filter, options); + return captures; + }; +} + function getCount( captureRepository: CaptureRepository, ): (filter: CaptureFilter) => Promise { @@ -24,6 +40,7 @@ function getCount( export default { getByFilter, + getByLocation, getCount, getById: delegateRepository('getById'), }; diff --git a/server/models/RawCapture.ts b/server/models/RawCapture.ts index ee48c6b..cc58136 100644 --- a/server/models/RawCapture.ts +++ b/server/models/RawCapture.ts @@ -1,5 +1,5 @@ -import RawCaptureRepository from 'infra/database/CaptureRepository'; import { delegateRepository } from 'infra/database/delegateRepository'; +import RawCaptureRepository from 'infra/database/RawCaptureRepository'; import RawCapture from 'interfaces/Capture'; import RawCaptureFilter from 'interfaces/CaptureFilter'; import FilterOptions from 'interfaces/FilterOptions'; @@ -15,7 +15,7 @@ function getByFilter( function getCount( rawCaptureRepository: RawCaptureRepository, -): (filter: RawCaptureFilter) => Promise<{ count: number }> { +): (filter: RawCaptureFilter) => Promise { return async function (filter: RawCaptureFilter) { const count = await rawCaptureRepository.getCount(filter); return count; diff --git a/server/routers/capturesRouter.ts b/server/routers/capturesRouter.ts index e7530b8..6383030 100644 --- a/server/routers/capturesRouter.ts +++ b/server/routers/capturesRouter.ts @@ -50,6 +50,52 @@ router.get( }), ); +router.get( + '/location', + handlerWrapper(async (req, res) => { + const query = queryFormatter(req); + + // verify filter values + Joi.assert( + query, + Joi.object().keys({ + session_id: Joi.string().uuid(), + lat: Joi.number(), + lon: Joi.number(), + deviation: Joi.number(), + limit: Joi.number().integer().min(1).max(20000), + offset: Joi.number().integer().min(0), + order_by: Joi.string(), + order: Joi.string(), + whereNulls: Joi.array(), + whereNotNulls: Joi.array(), + whereIns: Joi.array(), + }), + ); + const { + deviation = '0.01', + limit = 0, + offset = 0, + order = 'desc', + order_by = 'captured_at', + ...rest + } = query; + + const repo = new CaptureRepository(new Session()); + const exe = CaptureModel.getByLocation(repo); + const sort = { order, order_by }; + const result = await exe({ ...rest, deviation, sort }, { limit, offset }); + + res.send({ + captures: result, + total: Number(result.length), + offset, + limit, + }); + res.end(); + }), +); + router.get( '/:id', handlerWrapper(async (req, res) => {