From 298307304c28fc9efbfa0970e0e90c4aa49d307b Mon Sep 17 00:00:00 2001 From: Gwynn DP Date: Sun, 19 Sep 2021 20:02:38 -0700 Subject: [PATCH 1/2] fix: planter filter to allow searching combined orgs by name and by org id --- src/controllers/planter.controller.ts | 32 +++- .../planterOrganization.controller.ts | 70 +++++--- src/controllers/trees.controller.ts | 5 +- src/js/auth.js | 8 +- src/repositories/planter.repository.ts | 157 +++++++++++++++++- src/repositories/trees.repository.ts | 9 + 6 files changed, 249 insertions(+), 32 deletions(-) diff --git a/src/controllers/planter.controller.ts b/src/controllers/planter.controller.ts index d4dd55eb..8902b90f 100644 --- a/src/controllers/planter.controller.ts +++ b/src/controllers/planter.controller.ts @@ -20,6 +20,11 @@ import { Planter, Trees } from '../models'; import { TreesFilter } from './trees.controller'; import { PlanterRepository, TreesRepository } from '../repositories'; +// Extend the LoopBack filter types for the Planter model to include organizationId +// This is a workaround for the lack of proper join support in LoopBack +type PlanterWhere = (Where & { organizationId?: number }) | undefined; +export type PlanterFilter = Filter & { where: PlanterWhere }; + export class PlanterController { constructor( @repository(PlanterRepository) @@ -38,9 +43,19 @@ export class PlanterController { }) async count( @param.query.object('where', getWhereSchemaFor(Planter)) - where?: Where, + where?: PlanterWhere, ): Promise { - return await this.planterRepository.count(where); + // Replace organizationId with full entity tree and planter + if (where) { + const { organizationId, ...whereWithoutOrganizationId } = where; + where = await this.planterRepository.applyOrganizationWhereClause( + whereWithoutOrganizationId, + organizationId, + ); + } + // console.log('get /planter/count where -->', where); + + return await this.planterRepository.countWithOrg(where); } @get('/planter', { @@ -57,9 +72,18 @@ export class PlanterController { }) async find( @param.query.object('filter', getFilterSchemaFor(Planter)) - filter?: Filter, + filter?: PlanterFilter, ): Promise { - return await this.planterRepository.find(filter); + // Replace organizationId with full entity tree and planter + if (filter?.where) { + const { organizationId, ...whereWithoutOrganizationId } = filter.where; + filter.where = await this.planterRepository.applyOrganizationWhereClause( + whereWithoutOrganizationId, + organizationId, + ); + } + + return await this.planterRepository.findWithOrg(filter); } @get('/planter/{id}', { diff --git a/src/controllers/planterOrganization.controller.ts b/src/controllers/planterOrganization.controller.ts index 21d83424..40f4eb58 100644 --- a/src/controllers/planterOrganization.controller.ts +++ b/src/controllers/planterOrganization.controller.ts @@ -27,6 +27,10 @@ import { } from '../repositories'; // import expect from "expect-runtime"; +// Extend the LoopBack filter types for the Planter model to include organizationId +// This is a workaround for the lack of proper join support in LoopBack +type PlanterWhere = (Where & { organizationId?: number }) | undefined; +export type PlanterFilter = Filter & { where: PlanterWhere }; export class PlanterOrganizationController { constructor( @repository(PlanterRepository) @@ -48,17 +52,29 @@ export class PlanterOrganizationController { async count( @param.path.number('organizationId') organizationId: Number, @param.query.object('where', getWhereSchemaFor(Planter)) - where?: Where, + where?: PlanterWhere, ): Promise { - const entityIds = await this.treesRepository.getEntityIdsByOrganizationId( - organizationId.valueOf(), - ); - where = { - ...where, - organizationId: { - inq: entityIds, - }, - }; + //override organization id if a sub-org id is in filter AND it matches one of the organization's entityIds + let orgId = organizationId.valueOf(); + + if (where) { + const { organizationId, ...whereWithoutOrganizationId } = where; + const filterOrgId = organizationId; + + if (filterOrgId && filterOrgId !== orgId) { + const entityIds = await this.planterRepository.getEntityIdsByOrganizationId( + orgId, + ); + orgId = entityIds.includes(filterOrgId) ? filterOrgId : orgId; + } + + // Replace organizationId with full entity tree and planter query + where = await this.planterRepository.applyOrganizationWhereClause( + whereWithoutOrganizationId, + orgId, + ); + } + return await this.planterRepository.count(where); } @@ -77,21 +93,29 @@ export class PlanterOrganizationController { async find( @param.path.number('organizationId') organizationId: Number, @param.query.object('filter', getFilterSchemaFor(Planter)) - filter?: Filter, + filter?: PlanterFilter, ): Promise { - const entityIds = await this.treesRepository.getEntityIdsByOrganizationId( - organizationId?.valueOf() || -1, - ); - if (filter) { - //filter should be to deal with the organization, but here is just for - //demonstration - filter.where = { - ...filter.where, - organizationId: { - inq: entityIds, - }, - }; + //override organization id if a sub-org id is in filter AND it matches one of the organization's entityIds + let orgId = organizationId.valueOf(); + + if (filter?.where) { + const { organizationId, ...whereWithoutOrganizationId } = filter.where; + const filterOrgId = organizationId; + + if (filterOrgId && filterOrgId !== orgId) { + const entityIds = await this.planterRepository.getEntityIdsByOrganizationId( + orgId, + ); + orgId = entityIds.includes(filterOrgId) ? filterOrgId : orgId; + } + + // Replace organizationId with full entity tree and planter query + filter.where = await this.planterRepository.applyOrganizationWhereClause( + whereWithoutOrganizationId, + orgId, + ); } + return await this.planterRepository.find(filter); } diff --git a/src/controllers/trees.controller.ts b/src/controllers/trees.controller.ts index 23b1f37e..f833112a 100644 --- a/src/controllers/trees.controller.ts +++ b/src/controllers/trees.controller.ts @@ -60,6 +60,8 @@ export class TreesController { ); } + console.log('get /trees where --> ', where); + return await this.treesRepository.countWithTagId( where as Where, tagId, @@ -82,8 +84,6 @@ export class TreesController { @param.query.object('filter', getFilterSchemaFor(Trees)) filter?: TreesFilter, ): Promise { - console.log('get /trees filter --> ', filter ? filter.where : null); - const tagId = filter?.where?.tagId; // Replace organizationId with full entity tree and planter @@ -95,6 +95,7 @@ export class TreesController { ); } + console.log('get /trees filter --> ', filter); // In order to filter by tagId (treeTags relation), we need to bypass the LoopBack find() return await this.treesRepository.findWithTagId(filter, tagId); } diff --git a/src/js/auth.js b/src/js/auth.js index d2390350..c0b0ce9d 100644 --- a/src/js/auth.js +++ b/src/js/auth.js @@ -357,6 +357,7 @@ router.get('/admin_users/', async (req, res) => { }); router.post('/validate/', async (req, res) => { + console.log('validate'); try { const { password } = req.body; const token = req.headers.authorization; @@ -473,6 +474,7 @@ router.post('/init', async (req, res) => { }); const isAuth = async (req, res, next) => { + console.log('isAuth...'); //white list const url = req.originalUrl; if (url.match(/\/auth\/(login|test|init|validate)/)) { @@ -484,11 +486,12 @@ const isAuth = async (req, res, next) => { const token = req.headers.authorization; const decodedToken = jwt.verify(token, jwtSecret); const userSession = decodedToken; - //inject the user extract from token to request object req.user = userSession; + // console.log('userSession', userSession); + console.log('url', url); + // VALIDATE USER DATA - // const roles = userSession.role; expect(userSession.policy).toBeInstanceOf(Object); const policies = userSession.policy.policies; expect(policies).toBeInstanceOf(Array); @@ -503,6 +506,7 @@ const isAuth = async (req, res, next) => { if (url.match(/\/auth\/check_session/)) { const user_id = req.query.id; const result = await helper.getActiveAdminUser(userSession.userName); + console.log('check_session user_id', user_id); if (result.rows.length) { const update_userSession = utils.convertCamel(result.rows[0]); diff --git a/src/repositories/planter.repository.ts b/src/repositories/planter.repository.ts index cdf9b3b1..d215ddf4 100644 --- a/src/repositories/planter.repository.ts +++ b/src/repositories/planter.repository.ts @@ -1,7 +1,14 @@ -import { DefaultCrudRepository } from '@loopback/repository'; +import { + DefaultCrudRepository, + Filter, + Where, + Count, +} from '@loopback/repository'; import { Planter, PlanterRelations } from '../models'; import { TreetrackerDataSource } from '../datasources'; import { inject } from '@loopback/core'; +import expect from 'expect-runtime'; +import { buildFilterQuery } from '../js/buildFilterQuery'; export class PlanterRepository extends DefaultCrudRepository< Planter, @@ -13,4 +20,152 @@ export class PlanterRepository extends DefaultCrudRepository< ) { super(Planter, dataSource); } + + async getEntityIdsByOrganizationId( + organizationId: number, + ): Promise> { + expect(organizationId).number(); + expect(this).property('execute').defined(); + const result = await this.execute( + `select * from getEntityRelationshipChildren(${organizationId})`, + [], + ); + return result.map((e) => e.entity_id); + } + + async getPlanterIdsByOrganizationId( + organizationId: number, + ): Promise> { + expect(organizationId).number(); + const result = await this.execute( + `select * from planter where organization_id in (select entity_id from getEntityRelationshipChildren(${organizationId}))`, + [], + ); + expect(result).match([{ id: expect.any(Number) }]); + return result.map((e) => e.id); + } + + async getNonOrganizationPlanterIds(): Promise> { + const result = await this.execute( + `select * from planter where organization_id isnull`, + [], + ); + expect(result).match([{ id: expect.any(Number) }]); + return result.map((e) => e.id); + } + + async getOrganizationWhereClause(organizationId: number): Promise { + if (organizationId === null) { + const planterIds = await this.getNonOrganizationPlanterIds(); + return { + and: [ + { plantingOrganizationId: null }, + { planterId: { inq: planterIds } }, + ], + }; + } else { + const planterIds = await this.getPlanterIdsByOrganizationId( + organizationId, + ); + const entityIds = await this.getEntityIdsByOrganizationId(organizationId); + + // console.log( + // 'getOrganizationWhereClause: planterIds, entityIds --', + // planterIds, + // entityIds, + // ); + return { + or: [ + { organizationId: { inq: entityIds } }, + { id: { inq: planterIds } }, + ], + }; + } + } + + async applyOrganizationWhereClause( + where: Object | undefined, + organizationId: number | undefined, + ): Promise { + if (!where || organizationId === undefined) { + return Promise.resolve(where); + } + const organizationWhereClause = await this.getOrganizationWhereClause( + organizationId, + ); + return { + and: [where, organizationWhereClause], + }; + } + + // loopback .find() wasn't applying the org filters + async findWithOrg( + filter?: Filter, + ): Promise<(Planter & PlanterRelations)[]> { + console.log('findWithOrg ---', filter?.where); + + if (!filter) { + return await this.find(filter); + } + + try { + if (this.dataSource.connector) { + const columnNames = this.dataSource.connector.buildColumnNames( + 'Planter', + filter, + ); + + const selectStmt = `SELECT ${columnNames} FROM planter `; + + const params = { + filter, + repo: this, + modelName: 'Planter', + }; + + const query = buildFilterQuery(selectStmt, params); + + console.log('final query', query); + + return >await this.execute(query.sql, query.params); + } else { + throw 'Connector not defined'; + } + } catch (e) { + console.log(e); + return await this.find(filter); + } + } + + async countWithOrg(where?: Where): Promise { + // console.log('countWithOrg ---', where); + + if (!where) { + return await this.count(where); + } + + try { + const selectStmt = `SELECT COUNT(*) FROM planter `; + + const params = { + filter: { where }, + repo: this, + modelName: 'Planter', + }; + + const query = buildFilterQuery(selectStmt, params); + + // console.log('final count query', query); + + return >await this.execute(query.sql, query.params).then( + (res) => { + // responds with count value as a string + return (res && res[0]) || { count: 0 }; + }, + ); + } catch (e) { + console.log(e); + return await this.count(where); + } + } } diff --git a/src/repositories/trees.repository.ts b/src/repositories/trees.repository.ts index cfad896f..aeb92887 100644 --- a/src/repositories/trees.repository.ts +++ b/src/repositories/trees.repository.ts @@ -84,6 +84,12 @@ export class TreesRepository extends DefaultCrudRepository< organizationId, ); const entityIds = await this.getEntityIdsByOrganizationId(organizationId); + + console.log( + 'getOrganizationWhereClause: planterIds, entityIds --', + planterIds, + entityIds, + ); return { or: [ { plantingOrganizationId: { inq: entityIds } }, @@ -121,6 +127,7 @@ export class TreesRepository extends DefaultCrudRepository< tagId?: string, options?: Options, ): Promise<(Trees & TreesRelations)[]> { + console.log('trees filter ********', filter); if (!filter || tagId === undefined) { return await this.find(filter, options); } @@ -144,6 +151,8 @@ export class TreesRepository extends DefaultCrudRepository< const query = buildFilterQuery(selectStmt, params); + console.log('trees build filter query ********', query); + return >await this.execute( query.sql, query.params, From 5ebf65be0e41b123ff41e9e8e4b283da00ec960e Mon Sep 17 00:00:00 2001 From: Gwynn DP Date: Wed, 22 Sep 2021 16:40:36 -0700 Subject: [PATCH 2/2] fix: org requests to allow selection of sub-orgs by name --- src/controllers/organization.controller.ts | 21 +++++++--- src/controllers/planter.controller.ts | 1 - src/controllers/trees.controller.ts | 4 +- .../treesOrganization.controller.ts | 42 +++++++++++++++---- src/repositories/planter.repository.ts | 13 ------ src/repositories/trees.repository.ts | 15 ++++--- 6 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/controllers/organization.controller.ts b/src/controllers/organization.controller.ts index c40b7ef2..bbd62fa0 100644 --- a/src/controllers/organization.controller.ts +++ b/src/controllers/organization.controller.ts @@ -14,6 +14,11 @@ import { import { Organization } from '../models'; import { OrganizationRepository } from '../repositories'; +// Extend the LoopBack filter types for the Planter model to include type +type OrganizationWhere = (Where & { type?: string }) | undefined; +export type OrganizationFilter = Filter & { + where: OrganizationWhere; +}; export class OrganizationController { constructor( @repository(OrganizationRepository) @@ -69,18 +74,24 @@ export class OrganizationController { async findByParentOrg( @param.path.number('organizationId') organizationId: Number, @param.query.object('filter', getFilterSchemaFor(Organization)) - filter?: Filter, + filter?: OrganizationFilter, ): Promise { + // if logged in as an org-account then filter for the sub-orgs + if (organizationId && filter?.where) { + filter.where = { ...filter.where, type: 'o' }; + } + + // create query to get all orgs and their planters if (filter?.where) { filter.where = await this.organizationRepository.applyOrganizationWhereClause( filter.where, organizationId.valueOf(), ); } - // console.log('/organizations ?filter --> ', organizationId, filter, filter?.where); - const result = await this.organizationRepository.find(filter); - // console.log('/organizations res --> ', result); - return result; + + const childOrgs = await this.organizationRepository.find(filter); + + return childOrgs; } @get('/organizations/{id}', { diff --git a/src/controllers/planter.controller.ts b/src/controllers/planter.controller.ts index 8902b90f..762f551d 100644 --- a/src/controllers/planter.controller.ts +++ b/src/controllers/planter.controller.ts @@ -21,7 +21,6 @@ import { TreesFilter } from './trees.controller'; import { PlanterRepository, TreesRepository } from '../repositories'; // Extend the LoopBack filter types for the Planter model to include organizationId -// This is a workaround for the lack of proper join support in LoopBack type PlanterWhere = (Where & { organizationId?: number }) | undefined; export type PlanterFilter = Filter & { where: PlanterWhere }; diff --git a/src/controllers/trees.controller.ts b/src/controllers/trees.controller.ts index f833112a..3812bf2d 100644 --- a/src/controllers/trees.controller.ts +++ b/src/controllers/trees.controller.ts @@ -60,7 +60,7 @@ export class TreesController { ); } - console.log('get /trees where --> ', where); + // console.log('get /trees/count where --> ', where); return await this.treesRepository.countWithTagId( where as Where, @@ -95,7 +95,7 @@ export class TreesController { ); } - console.log('get /trees filter --> ', filter); + // console.log('get /trees filter --> ', filter?.where); // In order to filter by tagId (treeTags relation), we need to bypass the LoopBack find() return await this.treesRepository.findWithTagId(filter, tagId); } diff --git a/src/controllers/treesOrganization.controller.ts b/src/controllers/treesOrganization.controller.ts index c2129098..5a0575fd 100644 --- a/src/controllers/treesOrganization.controller.ts +++ b/src/controllers/treesOrganization.controller.ts @@ -45,12 +45,26 @@ export class TreesOrganizationController { @param.path.number('organizationId') organizationId: number, @param.query.object('where', getWhereSchemaFor(Trees)) where?: TreesWhere, ): Promise { - const orgWhere = await this.treesRepository.applyOrganizationWhereClause( - where, - organizationId, - ); const tagId = where?.tagId; - return await this.treesRepository.countWithTagId(orgWhere, tagId); + //override organization id if a sub-org id is in filter + let orgId = organizationId.valueOf(); + + if (where) { + const { organizationId, ...whereWithoutOrganizationId } = where; + const filterOrgId = organizationId; + + if (filterOrgId && filterOrgId !== orgId) { + orgId = filterOrgId; + } + + // Replace organizationId with full entity tree and planter query + where = await this.treesRepository.applyOrganizationWhereClause( + whereWithoutOrganizationId, + orgId, + ); + } + + return await this.treesRepository.countWithTagId(where, tagId); } @get('/organization/{organizationId}/trees', { @@ -71,12 +85,26 @@ export class TreesOrganizationController { filter?: TreesFilter, ): Promise { const tagId = filter?.where?.tagId; + //override organization id if a sub-org id is in filter + let orgId = organizationId.valueOf(); + if (filter?.where) { + const { organizationId, ...whereWithoutOrganizationId } = filter.where; + const filterOrgId = organizationId; + + if (filterOrgId && filterOrgId !== orgId) { + orgId = filterOrgId; + } + + // Replace organizationId with full entity tree and planter query filter.where = await this.treesRepository.applyOrganizationWhereClause( - filter.where, - organizationId, + whereWithoutOrganizationId, + orgId, ); } + + // console.log('treesOrganization filter ---', filter?.where); + return await this.treesRepository.findWithTagId(filter, tagId); } diff --git a/src/repositories/planter.repository.ts b/src/repositories/planter.repository.ts index d215ddf4..7a05d281 100644 --- a/src/repositories/planter.repository.ts +++ b/src/repositories/planter.repository.ts @@ -69,11 +69,6 @@ export class PlanterRepository extends DefaultCrudRepository< ); const entityIds = await this.getEntityIdsByOrganizationId(organizationId); - // console.log( - // 'getOrganizationWhereClause: planterIds, entityIds --', - // planterIds, - // entityIds, - // ); return { or: [ { organizationId: { inq: entityIds } }, @@ -102,8 +97,6 @@ export class PlanterRepository extends DefaultCrudRepository< async findWithOrg( filter?: Filter, ): Promise<(Planter & PlanterRelations)[]> { - console.log('findWithOrg ---', filter?.where); - if (!filter) { return await this.find(filter); } @@ -125,8 +118,6 @@ export class PlanterRepository extends DefaultCrudRepository< const query = buildFilterQuery(selectStmt, params); - console.log('final query', query); - return >await this.execute(query.sql, query.params); } else { throw 'Connector not defined'; @@ -138,8 +129,6 @@ export class PlanterRepository extends DefaultCrudRepository< } async countWithOrg(where?: Where): Promise { - // console.log('countWithOrg ---', where); - if (!where) { return await this.count(where); } @@ -155,8 +144,6 @@ export class PlanterRepository extends DefaultCrudRepository< const query = buildFilterQuery(selectStmt, params); - // console.log('final count query', query); - return >await this.execute(query.sql, query.params).then( (res) => { // responds with count value as a string diff --git a/src/repositories/trees.repository.ts b/src/repositories/trees.repository.ts index aeb92887..2b585201 100644 --- a/src/repositories/trees.repository.ts +++ b/src/repositories/trees.repository.ts @@ -71,6 +71,7 @@ export class TreesRepository extends DefaultCrudRepository< } async getOrganizationWhereClause(organizationId: number): Promise { + // console.log('getOrganizationWhereClause orgId ---', organizationId); if (organizationId === null) { const planterIds = await this.getNonOrganizationPlanterIds(); return { @@ -85,11 +86,12 @@ export class TreesRepository extends DefaultCrudRepository< ); const entityIds = await this.getEntityIdsByOrganizationId(organizationId); - console.log( - 'getOrganizationWhereClause: planterIds, entityIds --', - planterIds, - entityIds, - ); + // console.log( + // 'getOrganizationWhereClause: planterIds, entityIds --', + // planterIds, + // entityIds, + // ); + return { or: [ { plantingOrganizationId: { inq: entityIds } }, @@ -127,7 +129,6 @@ export class TreesRepository extends DefaultCrudRepository< tagId?: string, options?: Options, ): Promise<(Trees & TreesRelations)[]> { - console.log('trees filter ********', filter); if (!filter || tagId === undefined) { return await this.find(filter, options); } @@ -151,8 +152,6 @@ export class TreesRepository extends DefaultCrudRepository< const query = buildFilterQuery(selectStmt, params); - console.log('trees build filter query ********', query); - return >await this.execute( query.sql, query.params,