diff --git a/CHANGELOG.md b/CHANGELOG.md index fe73870e..bf28cf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# [1.75.0](https://github.com/Greenstand/treetracker-query-api/compare/v1.74.1...v1.75.0) (2025-02-24) + +### Bug Fixes + +- add limit to tree query by geometry ([70926eb](https://github.com/Greenstand/treetracker-query-api/commit/70926eb7644d1e9ff5687afdbebe16031557eb55)) +- change geometry input format to geojson array ([fd50334](https://github.com/Greenstand/treetracker-query-api/commit/fd5033457a09a6ce43e3a6c5f9078352b31b29a2)) + +### Features + +- add filter by geom for trees route ([97f583e](https://github.com/Greenstand/treetracker-query-api/commit/97f583ef0143e8284f748848fecb68b56f197125)) + ## [1.74.1](https://github.com/Greenstand/treetracker-query-api/compare/v1.74.0...v1.74.1) (2025-02-03) ### Bug Fixes diff --git a/__tests__/e2e/trees.spec.ts b/__tests__/e2e/trees.spec.ts index 8662ec01..61e2a175 100644 --- a/__tests__/e2e/trees.spec.ts +++ b/__tests__/e2e/trees.spec.ts @@ -1,3 +1,4 @@ +import exp from 'constants'; import supertest from 'supertest'; import app from '../../server/app'; @@ -135,4 +136,37 @@ describe('trees', () => { }, 1000 * 30, ); + + it( + 'trees/?geoJsonStr=encodedgeoJson', + async () => { + const geoJsonArr = [ + { + type: 'Feature', + geometry: { type: 'Point', coordinates: [-122.6064, 39.0619] }, + properties: {}, + }, + { + type: 'Feature', + geometry: { type: 'Point', coordinates: [-123.3139, 38.1519] }, + properties: {}, + }, + { + type: 'Feature', + geometry: { type: 'Point', coordinates: [-121.1001, 37.8879] }, + properties: {}, + }, + ]; + + const encodedgeoJson = encodeURIComponent(JSON.stringify(geoJsonArr)); + + const response = await supertest(app).get( + `/trees?geoJsonStr=${encodedgeoJson}`, + ); + expect(response.status).toBe(200); + expect(response.body.trees.length).toBe(145); + expect(response.body.total).toBe(145); + }, + 1000 * 30, + ); }); diff --git a/deployment/base/deployment.yaml b/deployment/base/deployment.yaml index b1a565ea..61aaaf8b 100644 --- a/deployment/base/deployment.yaml +++ b/deployment/base/deployment.yaml @@ -1,4 +1,4 @@ -apiVersion: apps/v1 +apiVersion: apps/v1 kind: Deployment metadata: name: treetracker-query-api @@ -15,18 +15,10 @@ spec: labels: app: treetracker-query-api spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: doks.digitalocean.com/node-pool - operator: In - values: - - microservices-node-pool containers: - name: treetracker-query-api - image: greenstand/treetracker-query-api:TAG + image: greenstand/treetracker-query-api:v1.2.3 + imagePullPolicy: IfNotPresent ports: - containerPort: 80 env: diff --git a/deployment/base/kustomization.yaml b/deployment/base/kustomization.yaml index d566e0b5..098e0046 100644 --- a/deployment/base/kustomization.yaml +++ b/deployment/base/kustomization.yaml @@ -1,5 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization resources: + - namespace.yaml - deployment.yaml - - mapping.yaml - service.yaml - database-connection-sealed-secret.yaml + - postgrest-config.yaml + - postgrest-deployment.yaml + - postgrest-service.yaml + - postgrest-ingress.yaml + diff --git a/deployment/base/master-key.yaml b/deployment/base/master-key.yaml new file mode 100644 index 00000000..e69de29b diff --git a/deployment/base/postgrest-config.yaml b/deployment/base/postgrest-config.yaml new file mode 100644 index 00000000..9c506b6f --- /dev/null +++ b/deployment/base/postgrest-config.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgrest-config + namespace: webmap +data: + postgrest.conf: | + db-uri = "$(PGRST_DB_URI)" + db-schema = "public" + db-anon-role = "readonly_user" + server-port = 3000 diff --git a/deployment/base/postgrest-deployment.yaml b/deployment/base/postgrest-deployment.yaml new file mode 100644 index 00000000..6c727c83 --- /dev/null +++ b/deployment/base/postgrest-deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgrest + namespace: webmap +spec: + replicas: 1 + selector: + matchLabels: + app: postgrest + template: + metadata: + labels: + app: postgrest + spec: + containers: + - name: postgrest + image: postgrest/postgrest:v12.0.2 + ports: + - containerPort: 3000 + env: + - name: PGRST_DB_URI + valueFrom: + secretKeyRef: + name: query-api-database-connection + key: db + - name: PGRST_DB_SCHEMA + value: "public" + - name: PGRST_DB_ANON_ROLE + value: "readonly_user" + - name: PGRST_SERVER_PORT + value: "3000" diff --git a/deployment/base/postgrest-ingress.yaml b/deployment/base/postgrest-ingress.yaml new file mode 100644 index 00000000..19f622ed --- /dev/null +++ b/deployment/base/postgrest-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: postgrest-ingress + namespace: webmap + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - http: + paths: + - path: /query-postgrest(/|$)(.*) + pathType: Prefix + backend: + service: + name: postgrest-service + port: + number: 3000 diff --git a/deployment/base/postgrest-service.yaml b/deployment/base/postgrest-service.yaml new file mode 100644 index 00000000..8aaa19ea --- /dev/null +++ b/deployment/base/postgrest-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgrest-service + namespace: webmap +spec: + selector: + app: postgrest + ports: + - port: 3000 + targetPort: 3000 + type: ClusterIP diff --git a/deployment/base/service.yaml b/deployment/base/service.yaml index f3c0fe9e..c0c91b3f 100644 --- a/deployment/base/service.yaml +++ b/deployment/base/service.yaml @@ -2,6 +2,7 @@ apiVersion: v1 kind: Service metadata: name: treetracker-query-api + namespace: webmap spec: selector: app: treetracker-query-api @@ -9,4 +10,4 @@ spec: - name: http protocol: TCP port: 80 - targetPort: 3006 + targetPort: 3006 # change if container doesn't use 3006 diff --git a/package-lock.json b/package-lock.json index db7564d7..40a557dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "treetracker-query-api", - "version": "1.73.0", + "version": "1.75.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "treetracker-query-api", - "version": "1.73.0", + "version": "1.75.0", "license": "GPL-3.0-or-later", "dependencies": { "@sentry/node": "^5.1.0", diff --git a/package.json b/package.json index 0865feea..ba046789 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "treetracker-query-api", - "version": "1.74.1", + "version": "1.75.0", "private": false, "keywords": [ "ecology" diff --git a/server/infra/database/TreeRepository.ts b/server/infra/database/TreeRepository.ts index 4506b2ed..6a58d12d 100644 --- a/server/infra/database/TreeRepository.ts +++ b/server/infra/database/TreeRepository.ts @@ -4,6 +4,12 @@ import HttpError from 'utils/HttpError'; import BaseRepository from './BaseRepository'; import Session from './Session'; +type GeoJson = Partial<{ + geometry: { + coordinates: number[]; + }; +}>; + export default class TreeRepository extends BaseRepository { constructor(session: Session) { super('trees', session); @@ -304,4 +310,48 @@ export default class TreeRepository extends BaseRepository { const object = await this.session.getDB().raw(sql); return object.rows; } + + async getByGeometry( + geoJsonArr: GeoJson[], + options: FilterOptions, + totalCount = false, + ) { + const { limit } = options; + const pointArray = geoJsonArr.map( + (item) => + `ST_MakePoint(${item.geometry?.coordinates[0]}, ${item.geometry?.coordinates[1]})`, + ); + pointArray.push( + `ST_MakePoint(${geoJsonArr[0].geometry?.coordinates[0]}, ${geoJsonArr[0].geometry?.coordinates[1]})`, + ); + if (totalCount) { + const totalSql = ` + SELECT + COUNT(*) + FROM trees t + WHERE + ST_CONTAINS( + ST_SETSRID(ST_CONVEXHULL(ST_MAKELINE(ARRAY[${pointArray.toString()}])), 4326), + ST_SETSRID(ST_POINT(t.lon, t.lat), 4326) + ) + `; + const total = await this.session.getDB().raw(totalSql); + return parseInt(total.rows[0].count.toString()); + } + + const sql = ` + SELECT + * + FROM trees t + WHERE + ST_CONTAINS( + ST_SETSRID(ST_CONVEXHULL(ST_MAKELINE(ARRAY[${pointArray.toString()}])), 4326), + ST_SETSRID(ST_POINT(t.lon, t.lat), 4326) + ) + LIMIT ${limit} + `; + + const object = await this.session.getDB().raw(sql); + return object.rows; + } } diff --git a/server/models/Tree.ts b/server/models/Tree.ts index 369b7de0..ab20ba2e 100644 --- a/server/models/Tree.ts +++ b/server/models/Tree.ts @@ -4,11 +4,17 @@ import Tree from 'interfaces/Tree'; import { delegateRepository } from '../infra/database/delegateRepository'; import TreeRepository from '../infra/database/TreeRepository'; +type GeoJson = Partial<{ + geometry: { + coordinates: number[]; + }; +}>; type Filter = Partial<{ organization_id: number; date_range: { startDate: string; endDate: string }; tag: string; wallet_id: string; + geoJsonArr: GeoJson[]; }>; function getByFilter( @@ -41,6 +47,14 @@ function getByFilter( const trees = await treeRepository.getByWallet(filter.wallet_id, options); return trees; } + if (filter.geoJsonArr) { + log.warn('using geometry filter...'); + const trees = await treeRepository.getByGeometry(filter.geoJsonArr, { + ...options, + limit: 500, + }); + return trees; + } const trees = await treeRepository.getByFilter(filter, options); return trees; @@ -84,6 +98,15 @@ function countByFilter( return total; } + if (filter.geoJsonArr) { + log.warn('using geometry filter...'); + const total = await treeRepository.getByGeometry( + filter.geoJsonArr, + options, + true, + ); + return total; + } const total = await treeRepository.countByFilter(filter); return total; }; diff --git a/server/routers/treesRouter.ts b/server/routers/treesRouter.ts index efb09bef..4a19ddd7 100644 --- a/server/routers/treesRouter.ts +++ b/server/routers/treesRouter.ts @@ -8,6 +8,13 @@ import TreeModel from '../models/Tree'; import HttpError from '../utils/HttpError'; const router = express.Router(); + +type GeoJson = Partial<{ + geometry: { + coordinates: number[]; + }; +}>; + type Filter = Partial<{ planter_id: number; organization_id: number; @@ -15,6 +22,7 @@ type Filter = Partial<{ tag: string; wallet_id: string; active: true; + geoJsonArr: GeoJson[]; }>; router.get( @@ -70,6 +78,7 @@ router.get( offset: Joi.number().integer().min(0), startDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), endDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), + geoJsonStr: Joi.string(), }), ); const { @@ -83,6 +92,7 @@ router.get( endDate, tag, wallet_id, + geoJsonStr, } = req.query; const repo = new TreeRepository(new Session()); const filter: Filter = { active: true }; @@ -105,6 +115,8 @@ router.get( filter.tag = tag; } else if (wallet_id) { filter.wallet_id = wallet_id; + } else if (geoJsonStr) { + filter.geoJsonArr = JSON.parse(decodeURIComponent(geoJsonStr)); } const result = await TreeModel.getByFilter(repo)(filter, options);