@hestia-earth/data-api 0.0.2-2 → 0.0.2-4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/.dockerignore +0 -25
- package/.env.test +0 -7
- package/.eslintignore +0 -7
- package/.eslintrc.js +0 -11
- package/.gitlab-ci.yml +0 -125
- package/.mocharc.js +0 -8
- package/.nvrm +0 -1
- package/.nycrc +0 -15
- package/Dockerfile +0 -17
- package/cleanup-docker.sh +0 -4
- package/commitlint.config.js +0 -1
- package/database/index.ts +0 -76
- package/database/migrations/001.do.init.sql +0 -53
- package/database/migrations/002.do.add-aggregated-sites.sql +0 -16
- package/database/migrations/003.do.add-generated-period-cols.sql +0 -7
- package/database/migrations/index.ts +0 -36
- package/database/seed/common.ts +0 -7
- package/database/seed/index.ts +0 -55
- package/database/seed/local/index.ts +0 -28
- package/database/seed/production/index.ts +0 -3
- package/database/seed/staging/index.ts +0 -5
- package/database/seed/test/index.ts +0 -28
- package/dev.ts +0 -3
- package/dist/aggregated-nodes/model/index.js +0 -11
- package/docker-compose.yml +0 -42
- package/envs/.master.env +0 -7
- package/envs/.staging.env +0 -7
- package/index.js +0 -3
- package/run-docker.sh +0 -14
- package/run-test.sh +0 -5
- package/scripts/run-lambda.ts +0 -10
- package/scripts/run-migrations.ts +0 -18
- package/scripts/run-resetdb.ts +0 -18
- package/scripts/run-seed.ts +0 -18
- package/serverless.yml +0 -76
- package/src/aggregated-nodes/model/index.ts +0 -37
- package/src/aggregated-nodes/routes/pg-get-filters.ts +0 -44
- package/src/aggregated-nodes/routes/pg-get.ts +0 -50
- package/src/aggregated-nodes/routes.spec.ts +0 -242
- package/src/aggregated-nodes/routes.ts +0 -56
- package/src/aggregated-nodes/services/pg-get-filters.ts +0 -52
- package/src/aggregated-nodes/services/pg-get.ts +0 -77
- package/src/app.spec.ts +0 -34
- package/src/app.ts +0 -59
- package/src/config.ts +0 -21
- package/src/cors.spec.ts +0 -32
- package/src/cors.ts +0 -7
- package/src/errors.spec.ts +0 -114
- package/src/errors.ts +0 -121
- package/src/index.spec.ts +0 -94
- package/src/index.ts +0 -14
- package/src/lambdas/sentry.ts +0 -12
- package/src/lambdas/update-aggregated-nodes/handler.spec.ts +0 -86
- package/src/lambdas/update-aggregated-nodes/handler.ts +0 -141
- package/src/logger.spec.ts +0 -20
- package/src/logger.ts +0 -45
- package/src/maintenance.spec.ts +0 -76
- package/src/maintenance.ts +0 -19
- package/src/models.ts +0 -1
- package/src/routes.ts +0 -8
- package/src/settings/model/index.ts +0 -21
- package/src/settings/routes/get.spec.ts +0 -33
- package/src/settings/routes/get.ts +0 -3
- package/src/settings/routes/update.spec.ts +0 -33
- package/src/settings/routes/update.ts +0 -5
- package/src/settings/routes.spec.ts +0 -75
- package/src/settings/routes.ts +0 -21
- package/src/settings/services/get.spec.ts +0 -62
- package/src/settings/services/get.ts +0 -18
- package/src/settings/services/update.spec.ts +0 -118
- package/src/settings/services/update.ts +0 -47
- package/src/slack.spec.ts +0 -42
- package/src/slack.ts +0 -17
- package/src/swagger/routes.ts +0 -57
- package/src/types/async-express-errors/index.d.ts +0 -1
- package/src/types/express/index.d.ts +0 -10
- package/src/utils/endpoint-wrapper.spec.ts +0 -80
- package/src/utils/endpoint-wrapper.ts +0 -16
- package/src/utils/middleware.spec.ts +0 -154
- package/src/utils/middleware.ts +0 -33
- package/test/Dockerfile +0 -13
- package/test/docker-compose.yml +0 -40
- package/test/fixtures/aggregated-nodes/get.ts +0 -184
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv +0 -5
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv.cycle.json +0 -458
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv.site.json +0 -182
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-impactassessment_pivoted.csv +0 -3
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-impactassessment_pivoted.csv.impactAssessment.json +0 -988
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleStraw-impactassessment_pivoted.csv +0 -3
- package/test/fixtures/update-aggregated-nodes/cycle-missing-impactassessment_pivoted.csv +0 -3
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv +0 -5
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv.cycle.json +0 -584
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv.site.json +0 -212
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-impactassessment_pivoted.csv +0 -3
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-impactassessment_pivoted.csv.impactAssessment.json +0 -1002
- package/test/prepare.ts +0 -13
- package/test/utils.ts +0 -33
- package/tsconfig.build.json +0 -13
- package/tsconfig.dist.json +0 -14
- package/tsconfig.json +0 -42
- package/tsconfig.lambdas.json +0 -13
package/run-docker.sh
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
docker build --progress=plain \
|
|
4
|
-
-t hestia-data-api:latest \
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
docker run --rm \
|
|
8
|
-
--name hestia-data-api \
|
|
9
|
-
--env-file .env \
|
|
10
|
-
--env PORT=80 \
|
|
11
|
-
-v ${PWD}/scripts:/app/scripts \
|
|
12
|
-
-v ${PWD}/src:/app/src \
|
|
13
|
-
-p 3001:80 \
|
|
14
|
-
hestia-data-api:latest npm run dev
|
package/run-test.sh
DELETED
package/scripts/run-lambda.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
|
-
dotenv.config();
|
|
4
|
-
|
|
5
|
-
import { updateAggregatedNodes } from '../src/lambdas/update-aggregated-nodes/handler';
|
|
6
|
-
|
|
7
|
-
updateAggregatedNodes().then(() => process.exit(0)).catch(err => {
|
|
8
|
-
console.error(err);
|
|
9
|
-
process.exit(1);
|
|
10
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
|
-
dotenv.config();
|
|
4
|
-
|
|
5
|
-
import { runMigrations } from '../database/migrations';
|
|
6
|
-
|
|
7
|
-
void (async function () {
|
|
8
|
-
const { gracefulExit } = await import('exit-hook');
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
await runMigrations();
|
|
12
|
-
gracefulExit(0);
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
gracefulExit(1);
|
|
17
|
-
}
|
|
18
|
-
})();
|
package/scripts/run-resetdb.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
|
-
dotenv.config();
|
|
4
|
-
|
|
5
|
-
import { resetDb } from '../test/utils';
|
|
6
|
-
|
|
7
|
-
void (async function () {
|
|
8
|
-
const { gracefulExit } = await import('exit-hook');
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
await resetDb();
|
|
12
|
-
gracefulExit(0);
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
gracefulExit(1);
|
|
17
|
-
}
|
|
18
|
-
})();
|
package/scripts/run-seed.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
|
-
dotenv.config();
|
|
4
|
-
|
|
5
|
-
import { runSeed } from '../database/seed';
|
|
6
|
-
|
|
7
|
-
void (async function () {
|
|
8
|
-
const { gracefulExit } = await import('exit-hook');
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
await runSeed();
|
|
12
|
-
gracefulExit(0);
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
console.error(error);
|
|
16
|
-
gracefulExit(1);
|
|
17
|
-
}
|
|
18
|
-
})();
|
package/serverless.yml
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
frameworkVersion: '3'
|
|
2
|
-
service: hestia-data-api
|
|
3
|
-
|
|
4
|
-
plugins:
|
|
5
|
-
- serverless-deployment-bucket
|
|
6
|
-
- serverless-offline
|
|
7
|
-
|
|
8
|
-
custom:
|
|
9
|
-
serverless-offline:
|
|
10
|
-
httpPort: 3002
|
|
11
|
-
stage: ${env:STAGE, self:provider.stage}
|
|
12
|
-
securityGroup:
|
|
13
|
-
staging: sg-0800627978a40527b
|
|
14
|
-
prod: sg-049204c3b58a8bd08
|
|
15
|
-
subnetId1:
|
|
16
|
-
# private 1a
|
|
17
|
-
staging: subnet-04ad6e355249c757d
|
|
18
|
-
prod: subnet-00c4a891bce6f48e0
|
|
19
|
-
subnetId2:
|
|
20
|
-
# private 1b
|
|
21
|
-
staging: subnet-01260544aaa3c6810
|
|
22
|
-
prod: subnet-02365f46c569f5e1a
|
|
23
|
-
debugging:
|
|
24
|
-
staging: true
|
|
25
|
-
prod: false
|
|
26
|
-
logLevel:
|
|
27
|
-
staging: DEBUG
|
|
28
|
-
prod: INFO
|
|
29
|
-
bucketData: hestia-data-${self:custom.stage}
|
|
30
|
-
domain:
|
|
31
|
-
staging: https://www-staging.hestia.earth
|
|
32
|
-
prod: https://www.hestia.earth
|
|
33
|
-
|
|
34
|
-
provider:
|
|
35
|
-
name: aws
|
|
36
|
-
runtime: nodejs18.x
|
|
37
|
-
region: us-east-1
|
|
38
|
-
stage: staging
|
|
39
|
-
deploymentBucket:
|
|
40
|
-
name: hestia-serverless-${self:custom.stage}
|
|
41
|
-
memorySize: 512
|
|
42
|
-
timeout: 600
|
|
43
|
-
environment:
|
|
44
|
-
SERVICE_NAME: ${self:service}
|
|
45
|
-
STAGE: ${self:custom.stage}
|
|
46
|
-
DEBUG: ${self:custom.debugging.${self:custom.stage}}
|
|
47
|
-
LOG_LEVEL: ${self:custom.logLevel.${self:custom.stage}}
|
|
48
|
-
SENTRY_DSN: https://00ed710e2b664894a3ff3124bfff24cf@o441427.ingest.0.1.0.io/4505186792767488
|
|
49
|
-
BUCKET_DATA: ${self:custom.bucketData}
|
|
50
|
-
PGHOST: ${env:PGHOST}
|
|
51
|
-
PGDATABASE: ${env:PGDATABASE}
|
|
52
|
-
PGPORT: ${env:PGPORT}
|
|
53
|
-
PGUSER: ${env:PGUSER}
|
|
54
|
-
PGPASSWORD: ${env:PGPASSWORD}
|
|
55
|
-
iamRoleStatements:
|
|
56
|
-
- Effect: Allow
|
|
57
|
-
Action:
|
|
58
|
-
- s3:GetObject
|
|
59
|
-
- s3:GetObjectAcl
|
|
60
|
-
- s3:ListObjects
|
|
61
|
-
- s3:ListBucket
|
|
62
|
-
Resource:
|
|
63
|
-
- "arn:aws:s3:::${self:custom.bucketData}"
|
|
64
|
-
- "arn:aws:s3:::${self:custom.bucketData}/*"
|
|
65
|
-
vpc:
|
|
66
|
-
securityGroupIds:
|
|
67
|
-
- ${self:custom.securityGroup.${self:custom.stage}}
|
|
68
|
-
subnetIds:
|
|
69
|
-
- ${self:custom.subnetId1.${self:custom.stage}}
|
|
70
|
-
- ${self:custom.subnetId2.${self:custom.stage}}
|
|
71
|
-
|
|
72
|
-
functions:
|
|
73
|
-
updateAggregatedNodes:
|
|
74
|
-
handler: build/src/lambdas/update-aggregated-nodes/handler.updateAggregatedNodes
|
|
75
|
-
events:
|
|
76
|
-
- schedule: cron(0 2 * * ? *) # 2:00am every day
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { ISiteJSONLD } from '@hestia-earth/schema';
|
|
2
|
-
|
|
3
|
-
export interface IFilter {
|
|
4
|
-
regions?: string[];
|
|
5
|
-
periods?: string[];
|
|
6
|
-
products?: string[];
|
|
7
|
-
defaultMethodClassifications?: string[];
|
|
8
|
-
practices?: string[];
|
|
9
|
-
minAggregatedQualityScore?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const validFilterFields = [
|
|
13
|
-
'regions',
|
|
14
|
-
'periods',
|
|
15
|
-
'products',
|
|
16
|
-
'defaultMethodClassifications',
|
|
17
|
-
'practices',
|
|
18
|
-
'minAggregatedQualityScore'
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
export interface IFilters {
|
|
22
|
-
cycleCount: number;
|
|
23
|
-
impactAssessmentCount: number;
|
|
24
|
-
products: string[];
|
|
25
|
-
periods: string[];
|
|
26
|
-
practices: string[];
|
|
27
|
-
minAggregatedQualityScore: number[];
|
|
28
|
-
regions: string[];
|
|
29
|
-
defaultMethodClassifications: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// TODO: update with pivoted types when available
|
|
33
|
-
export interface AggregatedNodeRow {
|
|
34
|
-
cycle: any; // pivoted cycle
|
|
35
|
-
region: ISiteJSONLD['country'];
|
|
36
|
-
impactAssessments: [any]; // pivoted impact assessments
|
|
37
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Request } from 'express';
|
|
2
|
-
|
|
3
|
-
import { applyFilters } from '../services/pg-get-filters';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @swagger
|
|
7
|
-
*
|
|
8
|
-
* paths:
|
|
9
|
-
* /aggregated-nodes/pg/filters:
|
|
10
|
-
* get:
|
|
11
|
-
* summary: "Get the additional filters available for a filter selection and node counts (uses PG only)"
|
|
12
|
-
* parameters:
|
|
13
|
-
* - in: query
|
|
14
|
-
* name: filter
|
|
15
|
-
* schema:
|
|
16
|
-
* type: string
|
|
17
|
-
* example: "regions=Africa|Albania;products=abyssinianKaleStraw|abyssinianKaleSeedWhole;periods=1970-1973"
|
|
18
|
-
* description: Set of filters
|
|
19
|
-
* - in: query
|
|
20
|
-
* name: query
|
|
21
|
-
* schema:
|
|
22
|
-
* type: string
|
|
23
|
-
* example: Wheat
|
|
24
|
-
* description: Free search text
|
|
25
|
-
* responses:
|
|
26
|
-
* 200:
|
|
27
|
-
* content:
|
|
28
|
-
* application/json:
|
|
29
|
-
* schema:
|
|
30
|
-
* type: object
|
|
31
|
-
* 400:
|
|
32
|
-
* $ref: '#/components/responses/BadRequestError'
|
|
33
|
-
* 404:
|
|
34
|
-
* $ref: '#/components/responses/NotFoundError'
|
|
35
|
-
* 500:
|
|
36
|
-
* $ref: '#/components/responses/UnexpectedError'
|
|
37
|
-
* 503:
|
|
38
|
-
* $ref: '#/components/responses/ServiceUnavailable'
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
export const getFilters = ({ query: qs, filter }: Request) => {
|
|
42
|
-
const query = qs.query as string;
|
|
43
|
-
return applyFilters({ filter, query });
|
|
44
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Request } from 'express';
|
|
2
|
-
|
|
3
|
-
import { applyFilter } from '../services/pg-get';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @swagger
|
|
7
|
-
*
|
|
8
|
-
* paths:
|
|
9
|
-
* /aggregated-nodes/pg:
|
|
10
|
-
* get:
|
|
11
|
-
* summary: "Get the aggregated nodes corresponding to a set of filters and search query (uses PG only)"
|
|
12
|
-
* parameters:
|
|
13
|
-
* - in: query
|
|
14
|
-
* name: filter
|
|
15
|
-
* schema:
|
|
16
|
-
* type: string
|
|
17
|
-
* example: "regions=Africa|Albania;products=abyssinianKaleStraw|abyssinianKaleSeedWhole;periods=1970-1973"
|
|
18
|
-
* description: Set of filters
|
|
19
|
-
* - in: query
|
|
20
|
-
* name: page
|
|
21
|
-
* schema:
|
|
22
|
-
* type: integer
|
|
23
|
-
* example: 1
|
|
24
|
-
* - in: query
|
|
25
|
-
* name: query
|
|
26
|
-
* schema:
|
|
27
|
-
* type: string
|
|
28
|
-
* example: Wheat
|
|
29
|
-
* description: Free search text
|
|
30
|
-
* responses:
|
|
31
|
-
* 200:
|
|
32
|
-
* content:
|
|
33
|
-
* application/json:
|
|
34
|
-
* schema:
|
|
35
|
-
* type: object
|
|
36
|
-
* 400:
|
|
37
|
-
* $ref: '#/components/responses/BadRequestError'
|
|
38
|
-
* 404:
|
|
39
|
-
* $ref: '#/components/responses/NotFoundError'
|
|
40
|
-
* 500:
|
|
41
|
-
* $ref: '#/components/responses/UnexpectedError'
|
|
42
|
-
* 503:
|
|
43
|
-
* $ref: '#/components/responses/ServiceUnavailable'
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
export const get = ({ query: qs, filter }: Request) => {
|
|
47
|
-
const page = Number(qs.page) || 1;
|
|
48
|
-
const query = qs.query as string;
|
|
49
|
-
return applyFilter({ page, filter, query });
|
|
50
|
-
};
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import * as sinon from 'sinon';
|
|
3
|
-
import 'mocha';
|
|
4
|
-
import express from 'express';
|
|
5
|
-
import supertest from 'supertest';
|
|
6
|
-
import bodyParser from 'body-parser';
|
|
7
|
-
|
|
8
|
-
import router from './routes';
|
|
9
|
-
import { Errors, errorHandler } from '../errors';
|
|
10
|
-
import { initDb } from '../../test/utils';
|
|
11
|
-
import { seed } from '../../database/seed';
|
|
12
|
-
|
|
13
|
-
import * as fixtures from '../../test/fixtures/aggregated-nodes/get';
|
|
14
|
-
|
|
15
|
-
let stubs: sinon.SinonStub[] = [];
|
|
16
|
-
|
|
17
|
-
const app = express();
|
|
18
|
-
|
|
19
|
-
const getFixtureNode = (type: 'site' | 'cycle' | 'impactAssessment', id: string) =>
|
|
20
|
-
fixtures[
|
|
21
|
-
{ cycle: 'aggregatedCycles', site: 'aggregatedSites', impactAssessment: 'aggregatedImpactAssessments' }[type]
|
|
22
|
-
].find((node) => node.jsonld_pivoted['@id'] === id).jsonld_pivoted;
|
|
23
|
-
|
|
24
|
-
app.use(bodyParser.json());
|
|
25
|
-
app.use(router());
|
|
26
|
-
app.use(errorHandler);
|
|
27
|
-
|
|
28
|
-
describe('routes', () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
stubs = [];
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
stubs.forEach((stub) => stub.restore());
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('aggregated-nodes', () => {
|
|
38
|
-
beforeEach(async () => {
|
|
39
|
-
await initDb();
|
|
40
|
-
await seed(fixtures);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('GET /pg', () => {
|
|
44
|
-
it('when invalid filter passed returns invalid param error', async () => {
|
|
45
|
-
const result = await supertest(app)
|
|
46
|
-
.get('/pg')
|
|
47
|
-
.query({ filter: 'invalidfilter' })
|
|
48
|
-
.set('Accept', 'application/json');
|
|
49
|
-
|
|
50
|
-
expect(result.body.message).to.equal(Errors.InvalidQueryParam);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('when neither filter nor query passed returns missing param error', async () => {
|
|
54
|
-
const result = await supertest(app).get('/pg').query({}).set('Accept', 'application/json');
|
|
55
|
-
|
|
56
|
-
expect(result.body.message).to.equal(Errors.MissingQueryParam);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('can filter by regions', async () => {
|
|
60
|
-
const expected = [
|
|
61
|
-
{
|
|
62
|
-
cycle: getFixtureNode('cycle', '1'),
|
|
63
|
-
region: getFixtureNode('site', '1').country,
|
|
64
|
-
impactAssessments: [getFixtureNode('impactAssessment', '1'), getFixtureNode('impactAssessment', '2')]
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
cycle: getFixtureNode('cycle', '2'),
|
|
68
|
-
region: getFixtureNode('site', '1').country,
|
|
69
|
-
impactAssessments: [getFixtureNode('impactAssessment', '3')]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
cycle: getFixtureNode('cycle', '3'),
|
|
73
|
-
region: getFixtureNode('site', '2').country,
|
|
74
|
-
impactAssessments: [getFixtureNode('impactAssessment', '4')]
|
|
75
|
-
}
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
const filter = 'regions=GADM-POL|GADM-DEU';
|
|
79
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
80
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('can filter by practices', async () => {
|
|
84
|
-
const expected = [
|
|
85
|
-
{
|
|
86
|
-
cycle: getFixtureNode('cycle', '1'),
|
|
87
|
-
region: getFixtureNode('site', '1').country,
|
|
88
|
-
impactAssessments: [getFixtureNode('impactAssessment', '1'), getFixtureNode('impactAssessment', '2')]
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
cycle: getFixtureNode('cycle', '2'),
|
|
92
|
-
region: getFixtureNode('site', '1').country,
|
|
93
|
-
impactAssessments: [getFixtureNode('impactAssessment', '3')]
|
|
94
|
-
}
|
|
95
|
-
];
|
|
96
|
-
const filter = 'practices=earthingUpByHand|deepRipping';
|
|
97
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
98
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('can filter by minimum aggregatedQualityScore', async () => {
|
|
102
|
-
const expected = [
|
|
103
|
-
{
|
|
104
|
-
cycle: getFixtureNode('cycle', '1'),
|
|
105
|
-
region: getFixtureNode('site', '1').country,
|
|
106
|
-
impactAssessments: [getFixtureNode('impactAssessment', '1'), getFixtureNode('impactAssessment', '2')]
|
|
107
|
-
}
|
|
108
|
-
];
|
|
109
|
-
const filter = 'minAggregatedQualityScore=3';
|
|
110
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
111
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('can filter by defaultMethodClassifications', async () => {
|
|
115
|
-
const expected = [
|
|
116
|
-
{
|
|
117
|
-
cycle: getFixtureNode('cycle', '2'),
|
|
118
|
-
region: getFixtureNode('site', '1').country,
|
|
119
|
-
impactAssessments: [getFixtureNode('impactAssessment', '3')]
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
cycle: getFixtureNode('cycle', '3'),
|
|
123
|
-
region: getFixtureNode('site', '2').country,
|
|
124
|
-
impactAssessments: [getFixtureNode('impactAssessment', '4')]
|
|
125
|
-
}
|
|
126
|
-
];
|
|
127
|
-
const filter = 'defaultMethodClassifications=physical measurement|expert opinion';
|
|
128
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
129
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('can filter by periods', async () => {
|
|
133
|
-
const expected = [
|
|
134
|
-
{
|
|
135
|
-
cycle: getFixtureNode('cycle', '3'),
|
|
136
|
-
region: getFixtureNode('site', '2').country,
|
|
137
|
-
impactAssessments: [getFixtureNode('impactAssessment', '4')]
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
cycle: getFixtureNode('cycle', '4'),
|
|
141
|
-
region: getFixtureNode('site', '3').country,
|
|
142
|
-
impactAssessments: []
|
|
143
|
-
}
|
|
144
|
-
];
|
|
145
|
-
const filter = 'periods=2010-2019|2000-2009';
|
|
146
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
147
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('can filter by products', async () => {
|
|
151
|
-
const expected = [
|
|
152
|
-
{
|
|
153
|
-
cycle: getFixtureNode('cycle', '1'),
|
|
154
|
-
region: getFixtureNode('site', '1').country,
|
|
155
|
-
// impactAssessments have also been filtered
|
|
156
|
-
impactAssessments: [getFixtureNode('impactAssessment', '1')]
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
cycle: getFixtureNode('cycle', '2'),
|
|
160
|
-
region: getFixtureNode('site', '1').country,
|
|
161
|
-
impactAssessments: [getFixtureNode('impactAssessment', '3')]
|
|
162
|
-
}
|
|
163
|
-
];
|
|
164
|
-
const filter = 'products=wheatGrain|fishFingerlings';
|
|
165
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
166
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('can combine multiple filters', async () => {
|
|
170
|
-
const expected = [
|
|
171
|
-
{
|
|
172
|
-
cycle: getFixtureNode('cycle', '5'),
|
|
173
|
-
region: getFixtureNode('site', '3').country,
|
|
174
|
-
// impactAssessments have also been filtered
|
|
175
|
-
impactAssessments: []
|
|
176
|
-
}
|
|
177
|
-
];
|
|
178
|
-
const filter = [
|
|
179
|
-
'regions=GADM-POL|GADM-DEU|GADM-BRA',
|
|
180
|
-
'products=wheatGrain|fishFingerlings|cheese',
|
|
181
|
-
'periods=2010-2019|2020-2029',
|
|
182
|
-
'defaultMethodClassifications=modelled|expert opinion',
|
|
183
|
-
'minAggregatedQualityScore=2',
|
|
184
|
-
'practices=earthingUpByHand|deepRipping|baggingFruit'
|
|
185
|
-
].join(';');
|
|
186
|
-
const result = await supertest(app).get('/pg').query({ filter }).set('Accept', 'application/json');
|
|
187
|
-
expect(result.body.rows).to.deep.equal(expected);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe('GET /pg/filters', () => {
|
|
192
|
-
it('when invalid filter passed returns invalid param error', async () => {
|
|
193
|
-
const result = await supertest(app)
|
|
194
|
-
.get('/pg/filters')
|
|
195
|
-
.query({ filter: 'invalidfilter' })
|
|
196
|
-
.set('Accept', 'application/json');
|
|
197
|
-
|
|
198
|
-
expect(result.body.message).to.equal(Errors.InvalidQueryParam);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('returns the filters and node counts available for the current selection', () => {
|
|
202
|
-
it('returns subset of filters corresponding to selected nodes', async () => {
|
|
203
|
-
const expected = {
|
|
204
|
-
cycleCount: 1,
|
|
205
|
-
impactAssessmentCount: 0,
|
|
206
|
-
products: ['apples', 'cheese'],
|
|
207
|
-
periods: ['2020-2029'],
|
|
208
|
-
practices: ['baggingFruit'],
|
|
209
|
-
minAggregatedQualityScore: [2],
|
|
210
|
-
regions: ['GADM-BRA'],
|
|
211
|
-
defaultMethodClassifications: ['modelled']
|
|
212
|
-
};
|
|
213
|
-
const filter = [
|
|
214
|
-
'regions=GADM-POL|GADM-DEU|GADM-BRA',
|
|
215
|
-
'products=wheatGrain|fishFingerlings|cheese',
|
|
216
|
-
'periods=2010-2019|2020-2029',
|
|
217
|
-
'defaultMethodClassifications=modelled|expert opinion',
|
|
218
|
-
'minAggregatedQualityScore=2',
|
|
219
|
-
'practices=earthingUpByHand|deepRipping|baggingFruit'
|
|
220
|
-
].join(';');
|
|
221
|
-
const result = await supertest(app).get('/pg/filters').query({ filter }).set('Accept', 'application/json');
|
|
222
|
-
expect(result.body).to.deep.equal(expected);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('returns all filters when there is nothing currently selected', async () => {
|
|
226
|
-
const expected = {
|
|
227
|
-
cycleCount: 8,
|
|
228
|
-
impactAssessmentCount: 4,
|
|
229
|
-
products: ['apples', 'barley', 'cheese', 'fishFingerlings', 'wheatGrain'],
|
|
230
|
-
periods: ['1990-1999', '2000-2009', '2010-2019', '2020-2029'],
|
|
231
|
-
practices: ['baggingFruit', 'deepRipping', 'earthingUpByHand'],
|
|
232
|
-
minAggregatedQualityScore: [1, 2, 3],
|
|
233
|
-
regions: ['GADM-BRA', 'GADM-DEU', 'GADM-POL'],
|
|
234
|
-
defaultMethodClassifications: ['expert opinion', 'modelled', 'physical measurement', 'unsourced assumption']
|
|
235
|
-
};
|
|
236
|
-
const result = await supertest(app).get('/pg/filters').query({}).set('Accept', 'application/json');
|
|
237
|
-
expect(result.body).to.deep.equal(expected);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Request, Response, NextFunction, Router } from 'express';
|
|
2
|
-
import { endpointWrapper } from '../utils/endpoint-wrapper';
|
|
3
|
-
import { get as pgGet } from './routes/pg-get';
|
|
4
|
-
import { HestiaError, Errors } from '../errors';
|
|
5
|
-
import { validFilterFields } from './model';
|
|
6
|
-
import { getFilters as pgGetFilters } from './routes/pg-get-filters';
|
|
7
|
-
import { IFilter } from './model';
|
|
8
|
-
|
|
9
|
-
const isValidFilterString = (filter: string) =>
|
|
10
|
-
filter.split(';').every((field) => {
|
|
11
|
-
const [key] = field.split('=');
|
|
12
|
-
return validFilterFields.includes(key);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const validateGet = (req: Request, _res: Response, next: NextFunction) => {
|
|
16
|
-
const missingParam = !req.query.filter && !req.query.query;
|
|
17
|
-
const invalidParam = req.query.filter && !isValidFilterString(req.query.filter as string);
|
|
18
|
-
return missingParam
|
|
19
|
-
? next(new HestiaError(Errors.MissingQueryParam))
|
|
20
|
-
: invalidParam
|
|
21
|
-
? next(new HestiaError(Errors.InvalidQueryParam))
|
|
22
|
-
: next();
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const validateGetFilters = (req: Request, _res: Response, next: NextFunction) => {
|
|
26
|
-
const invalidParam = req.query.filter && !isValidFilterString(req.query.filter as string);
|
|
27
|
-
return invalidParam ? next(new HestiaError(Errors.InvalidQueryParam)) : next();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const parseArrayField = (val: string) => val.split('|');
|
|
31
|
-
|
|
32
|
-
const filterFieldParsers: Record<keyof IFilter, (val: string) => any> = {
|
|
33
|
-
regions: parseArrayField,
|
|
34
|
-
periods: parseArrayField,
|
|
35
|
-
products: parseArrayField,
|
|
36
|
-
defaultMethodClassifications: parseArrayField,
|
|
37
|
-
minAggregatedQualityScore: (val: string) => Number(val),
|
|
38
|
-
practices: parseArrayField
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const parseFilterString = (req: Request, _res: Response, next: NextFunction) => {
|
|
42
|
-
req.filter = ((req.query.filter || '') as string).split(';').reduce((acc, entry) => {
|
|
43
|
-
const [key, val] = entry.split('=');
|
|
44
|
-
return filterFieldParsers[key] ? { ...acc, [key]: filterFieldParsers[key](val) } : acc;
|
|
45
|
-
}, {});
|
|
46
|
-
next();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export default () => {
|
|
50
|
-
const router = Router();
|
|
51
|
-
|
|
52
|
-
router.get('/pg/filters', validateGetFilters, parseFilterString, endpointWrapper(pgGetFilters));
|
|
53
|
-
router.get('/pg', validateGet, parseFilterString, endpointWrapper(pgGet));
|
|
54
|
-
|
|
55
|
-
return router;
|
|
56
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { getFilterClauses } from './pg-get';
|
|
2
|
-
import { query as doQuery } from '../../../database';
|
|
3
|
-
import { IFilter, IFilters } from '../model';
|
|
4
|
-
|
|
5
|
-
const getFiltersQuery = (filter: IFilter): [string, any[]] => {
|
|
6
|
-
const { filterClauses, params } = getFilterClauses(filter);
|
|
7
|
-
return [
|
|
8
|
-
`
|
|
9
|
-
SELECT
|
|
10
|
-
COUNT(DISTINCT c) as "cycleCount",
|
|
11
|
-
COUNT(DISTINCT ia) AS "impactAssessmentCount",
|
|
12
|
-
ARRAY_AGG(DISTINCT product ORDER BY product) as products,
|
|
13
|
-
ARRAY_AGG(DISTINCT c.generated_period ORDER BY c.generated_period) as periods,
|
|
14
|
-
ARRAY_AGG(DISTINCT practice ORDER BY practice) as practices,
|
|
15
|
-
ARRAY_AGG(
|
|
16
|
-
DISTINCT (c.jsonld_pivoted->>'aggregatedQualityScore')::integer
|
|
17
|
-
ORDER BY (c.jsonld_pivoted->>'aggregatedQualityScore')::integer
|
|
18
|
-
) AS "minAggregatedQualityScore",
|
|
19
|
-
ARRAY_AGG(
|
|
20
|
-
DISTINCT s.jsonld_pivoted['country']->>'@id'
|
|
21
|
-
ORDER BY s.jsonld_pivoted['country']->>'@id'
|
|
22
|
-
) AS regions,
|
|
23
|
-
ARRAY_AGG(
|
|
24
|
-
DISTINCT c.jsonld_pivoted->>'defaultMethodClassification'
|
|
25
|
-
ORDER BY c.jsonld_pivoted->>'defaultMethodClassification'
|
|
26
|
-
) AS "defaultMethodClassifications"
|
|
27
|
-
FROM aggregated_cycles AS c
|
|
28
|
-
LEFT JOIN aggregated_impact_assessments AS ia
|
|
29
|
-
ON c.hestia_id = ia.cycle_hestia_id
|
|
30
|
-
AND ${filterClauses.products.iaFilter}
|
|
31
|
-
JOIN aggregated_sites AS s
|
|
32
|
-
ON c.site_hestia_id = s.hestia_id
|
|
33
|
-
AND ${filterClauses.regions}
|
|
34
|
-
CROSS JOIN LATERAL
|
|
35
|
-
jsonb_object_keys(c.jsonld_pivoted['products']) as products(product),
|
|
36
|
-
jsonb_object_keys(c.jsonld_pivoted['practices']) as practices(practice)
|
|
37
|
-
WHERE ${filterClauses.practices}
|
|
38
|
-
AND ${filterClauses.defaultMethodClassifications}
|
|
39
|
-
AND ${filterClauses.products.cycleFilter}
|
|
40
|
-
AND ${filterClauses.minAggregatedQualityScore}
|
|
41
|
-
AND ${filterClauses.periods}
|
|
42
|
-
`,
|
|
43
|
-
params
|
|
44
|
-
];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const applyFilters = async ({ filter }: { filter: IFilter; query?: string }) => {
|
|
48
|
-
// TODO: handle query later
|
|
49
|
-
const [pgQuery, params] = getFiltersQuery(filter);
|
|
50
|
-
const res = await doQuery<IFilters>(pgQuery, params);
|
|
51
|
-
return res?.rows[0];
|
|
52
|
-
};
|