diff --git a/src/controllers/planterRegistration.controller.ts b/src/controllers/planterRegistration.controller.ts index 901366d0..c45c8f0d 100644 --- a/src/controllers/planterRegistration.controller.ts +++ b/src/controllers/planterRegistration.controller.ts @@ -1,14 +1,8 @@ -import { - Filter, - repository, -} from '@loopback/repository'; -import { - param, - get, - getFilterSchemaFor, -} from '@loopback/rest'; -import {PlanterRegistration} from '../models'; -import {PlanterRegistrationRepository} from '../repositories'; +import { Filter, repository, Where } from '@loopback/repository'; +import { param, get, getFilterSchemaFor } from '@loopback/rest'; +import { PlanterRegistration } from '../models'; +import { PlanterRegistrationRepository } from '../repositories'; +import { buildFilterQuery } from '../js/buildFilterQuery.js'; export class PlanterRegistrationController { constructor( @@ -22,7 +16,10 @@ export class PlanterRegistrationController { description: 'Array of PlanterRegistration model instances', content: { 'application/json': { - schema: {type: 'array', items: {'x-ts-type': PlanterRegistration}}, + schema: { + type: 'array', + items: { 'x-ts-type': PlanterRegistration }, + }, }, }, }, @@ -32,18 +29,38 @@ export class PlanterRegistrationController { @param.query.object('filter', getFilterSchemaFor(PlanterRegistration)) filter?: Filter, ): Promise { - return await this.planterRepository.find(filter); + const sql = `SELECT * FROM planter_registrations + LEFT JOIN ( + SELECT region.name AS country, region.geom FROM region, region_type + WHERE region_type.type='country' AND region.type_id=region_type.id + ) AS region ON ST_DWithin(region.geom, planter_registrations.geom, 0.01)`; + + const params = { + filter: filter?.where, + repo: this.planterRepository, + model: 'PlanterRegistration', + }; + + const query = buildFilterQuery(sql, params); + + return >( + await this.planterRepository.execute(query.sql, query.params) + ); } @get('/planter-registration/{id}', { responses: { '200': { description: 'PlanterRegistration model instance', - content: {'application/json': {schema: {'x-ts-type': PlanterRegistration}}}, + content: { + 'application/json': { schema: { 'x-ts-type': PlanterRegistration } }, + }, }, }, }) - async findById(@param.path.number('id') id: number): Promise { + async findById( + @param.path.number('id') id: number, + ): Promise { return await this.planterRepository.findById(id); } } diff --git a/src/controllers/trees.controller.ts b/src/controllers/trees.controller.ts index 76c8433f..a093be46 100644 --- a/src/controllers/trees.controller.ts +++ b/src/controllers/trees.controller.ts @@ -23,10 +23,11 @@ import { publishMessage } from '../messaging/RabbitMQMessaging.js'; import { config } from '../config.js'; import { v4 as uuid } from 'uuid'; import { Transaction } from 'loopback-connector'; +import { getConnector, buildFilterQuery } from '../js/buildFilterQuery.js'; // Extend the LoopBack filter types for the Trees model to include tagId // This is a workaround for the lack of proper join support in LoopBack -type TreesWhere = Where & { tagId?: string, organizationId?: number }; +type TreesWhere = Where & { tagId?: string; organizationId?: number }; type TreesFilter = Filter & { where: TreesWhere }; export class TreesController { @@ -34,8 +35,8 @@ export class TreesController { @repository(TreesRepository) public treesRepository: TreesRepository, @repository(DomainEventRepository) - public domainEventRepository: DomainEventRepository - ) { } + public domainEventRepository: DomainEventRepository, + ) {} @get('/trees/count', { responses: { @@ -48,27 +49,36 @@ export class TreesController { async count( @param.query.object('where', getWhereSchemaFor(Trees)) where?: TreesWhere, ): Promise { - // Replace organizationId with full entity tree and planter if (where && where.organizationId !== undefined) { - const clause = await this.treesRepository.getOrganizationWhereClause(where.organizationId) + const clause = await this.treesRepository.getOrganizationWhereClause( + where.organizationId, + ); where = { ...where, ...clause, - } + }; delete where.organizationId; } // In order to filter by tagId (treeTags relation), we need to bypass the LoopBack count() if (where && where.tagId !== undefined) { try { - const isTagNull = where.tagId === null - const query = this.buildFilterQuery( - `SELECT COUNT(*) FROM trees`, - `${isTagNull ? 'LEFT JOIN' : 'JOIN'} tree_tag ON trees.id=tree_tag.tree_id`, - `WHERE tree_tag.tag_id ${isTagNull ? 'IS NULL' : `=${where.tagId}`}`, - where, - ); + const isTagNull = where.tagId === null; + + const sql = `SELECT COUNT(*) FROM trees ${ + isTagNull ? 'LEFT JOIN' : 'JOIN' + } tree_tag ON trees.id=tree_tag.tree_id WHERE tree_tag.tag_id ${ + isTagNull ? 'IS NULL' : `=${where.tagId}` + }`; + + const params = { + filter: where, + repo: this.treesRepository, + model: 'Trees', + }; + + const query = buildFilterQuery(sql, params); return >( await this.treesRepository @@ -107,35 +117,42 @@ export class TreesController { // Replace plantingOrganizationId with full entity tree and planter if (filter && filter.where && filter.where.organizationId !== undefined) { const clause = await this.treesRepository.getOrganizationWhereClause( - filter.where.organizationId + filter.where.organizationId, ); filter.where = { ...filter.where, ...clause, - } + }; delete filter.where.organizationId; } // In order to filter by tagId (treeTags relation), we need to bypass the LoopBack find() if (filter && filter.where && filter.where.tagId !== undefined) { try { - const connector = this.getConnector(); + const connector = getConnector(this.treesRepository); if (connector) { // If included, replace 'id' with 'tree_id as id' to avoid ambiguity const columnNames = connector .buildColumnNames('Trees', filter) - .replace('"id"', 'trees.id as "id"') + .replace('"id"', 'trees.id as "id"'); - const isTagNull = filter.where.tagId === null - const query = this.buildFilterQuery( - `SELECT ${columnNames} from trees`, - `${isTagNull ? - 'LEFT JOIN tree_tag ON trees.id=tree_tag.tree_id ORDER BY "time_created" DESC' - : - 'JOIN tree_tag ON trees.id=tree_tag.tree_id'}`, - `WHERE tree_tag.tag_id ${isTagNull ? 'IS NULL' : `=${filter.where.tagId}`}`, - filter.where, - ); + const isTagNull = filter.where.tagId === null; + + const sql = `SELECT ${columnNames} from trees ${ + isTagNull + ? 'LEFT JOIN tree_tag ON trees.id=tree_tag.tree_id ORDER BY "time_created" DESC' + : 'JOIN tree_tag ON trees.id=tree_tag.tree_id' + } WHERE tree_tag.tag_id ${ + isTagNull ? 'IS NULL' : `=${filter.where.tagId}` + }`; + + const params = { + filter: filter?.where, + repo: this.treesRepository, + model: 'Trees', + }; + + const query = buildFilterQuery(sql, params); return >( await this.treesRepository @@ -204,8 +221,9 @@ export class TreesController { }) limit: number, ): Promise { - const query = `SELECT * FROM Trees WHERE ST_DWithin(ST_MakePoint(lat,lon), ST_MakePoint(${lat}, ${lon}), ${radius ? radius : 100 - }, false) LIMIT ${limit ? limit : 100}`; + const query = `SELECT * FROM Trees WHERE ST_DWithin(ST_MakePoint(lat,lon), ST_MakePoint(${lat}, ${lon}), ${ + radius ? radius : 100 + }, false) LIMIT ${limit ? limit : 100}`; console.log(`near query: ${query}`); return >await this.treesRepository.execute(query, []); } @@ -221,94 +239,53 @@ export class TreesController { @param.path.number('id') id: number, @requestBody() trees: Trees, ): Promise { - const tx = await this.treesRepository.dataSource.beginTransaction( - { isolationLevel: Transaction.READ_COMMITTED } - ); - try{ + const tx = await this.treesRepository.dataSource.beginTransaction({ + isolationLevel: Transaction.READ_COMMITTED, + }); + try { let verifyCaptureProcessed; let domainEvent; - if(config.enableVerificationPublishing) { + if (config.enableVerificationPublishing) { const storedTree = await this.treesRepository.findById(id); // Raise an event to indicate verification is processed // on both rejection and approval - if((!trees.approved && !trees.active && storedTree.active) || - storedTree.approved != trees.approved) { + if ( + (!trees.approved && !trees.active && storedTree.active) || + storedTree.approved != trees.approved + ) { verifyCaptureProcessed = { id: storedTree.uuid, reference_id: storedTree.id, - type: "VerifyCaptureProcessed", + type: 'VerifyCaptureProcessed', approved: trees.approved, rejection_reason: trees.rejectionReason, - created_at: new Date().toISOString() + created_at: new Date().toISOString(), }; domainEvent = { id: uuid(), payload: verifyCaptureProcessed, status: 'raised', createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + updatedAt: new Date().toISOString(), }; - await this.domainEventRepository.create( - domainEvent, { transaction: tx }); + await this.domainEventRepository.create(domainEvent, { + transaction: tx, + }); } } await this.treesRepository.updateById(id, trees, { transaction: tx }); await tx.commit(); - if(verifyCaptureProcessed) { + if (verifyCaptureProcessed) { await publishMessage(verifyCaptureProcessed, () => { - this.domainEventRepository.updateById( - domainEvent.id, { status: 'sent', updatedAt: new Date().toISOString()}); + this.domainEventRepository.updateById(domainEvent.id, { + status: 'sent', + updatedAt: new Date().toISOString(), + }); }); } - } catch(e) { + } catch (e) { await tx.rollback(); throw e; } } - - private getConnector() { - return this.treesRepository.dataSource.connector; - } - - private buildFilterQuery( - selectClause: string, - joinClause?: string, - whereClause?: string, - whereObj?: TreesWhere, - ) { - let sql = selectClause; - if (joinClause) { - sql += ` ${joinClause}`; - } - - if (whereClause) { - sql += ` ${whereClause}`; - } - - let query = new ParameterizedSQL(sql); - - if (whereObj) { - const connector = this.getConnector(); - if (connector) { - const model = connector._models.Trees.model; - - if (model) { - let safeWhere = model._sanitizeQuery(whereObj); - safeWhere = model._coerce(safeWhere); - - const whereObjClause = connector._buildWhere('Trees', safeWhere); - - if (whereObjClause && whereObjClause.sql) { - query.sql += ` ${whereClause ? 'AND' : 'WHERE'} ${whereObjClause.sql - }`; - query.params = whereObjClause.params; - } - - query = connector.parameterize(query); - } - } - } - - return query; - } } diff --git a/src/js/buildFilterQuery.js b/src/js/buildFilterQuery.js new file mode 100644 index 00000000..e547c3c3 --- /dev/null +++ b/src/js/buildFilterQuery.js @@ -0,0 +1,33 @@ +import { ParameterizedSQL } from 'loopback-connector'; + +export function getConnector(repo) { + return repo?.dataSource?.connector; +} + +export function buildFilterQuery(sql, params) { + let query = new ParameterizedSQL(sql); + + if (params.filter) { + const connector = getConnector(params.repo); + if (connector) { + const model = connector._models[params.model].model; + + if (model) { + let safeWhere = model._sanitizeQuery(params.filter); + safeWhere = model._coerce(safeWhere); + + const whereObjClause = connector._buildWhere(params.model, safeWhere); + + if (whereObjClause.sql) { + const hasWhere = /WHERE(?![^(]*\))/i.test(sql); + query.sql += ` ${hasWhere ? 'AND' : 'WHERE'} ${whereObjClause.sql}`; + query.params = whereObjClause.params; + } + + query = connector.parameterize(query); + } + } + } + + return query; +} diff --git a/src/models/planterRegistration.model.ts b/src/models/planterRegistration.model.ts index 69c6cafc..87c0d82b 100644 --- a/src/models/planterRegistration.model.ts +++ b/src/models/planterRegistration.model.ts @@ -1,12 +1,12 @@ -import {Entity, model, property} from '@loopback/repository'; +import { Entity, model, property } from '@loopback/repository'; /* eslint-disable @typescript-eslint/no-empty-interface */ @model({ settings: { idInjection: false, - postgresql: {schema: 'public', table: 'planter_registrations'}, - } + postgresql: { schema: 'public', table: 'planter_registrations' }, + }, }) export class PlanterRegistration extends Entity { @property({ @@ -42,6 +42,26 @@ export class PlanterRegistration extends Entity { }) createdAt: string; + @property({ + type: 'number', + required: false, + dataType: 'latitude', + postgresql: { + columnName: 'lat', + }, + }) + lat: number; + + @property({ + type: 'number', + required: false, + dataType: 'longitude', + postgresql: { + columnName: 'lon', + }, + }) + lon: number; + // Indexer property to allow additional data // eslint-disable-next-line @typescript-eslint/no-explicit-any [prop: string]: any; @@ -55,4 +75,5 @@ export interface PlanterRegistrationRelations { // describe navigational properties here } -export type PlanterRegistrationWithRelations = PlanterRegistration & PlanterRegistrationRelations; +export type PlanterRegistrationWithRelations = PlanterRegistration & + PlanterRegistrationRelations;