@hestia-earth/data-api 0.0.2-1
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/.dockerignore +25 -0
- package/.env.test +7 -0
- package/.eslintignore +7 -0
- package/.eslintrc.js +11 -0
- package/.gitlab-ci.yml +125 -0
- package/.mocharc.js +8 -0
- package/.nvrm +1 -0
- package/.nycrc +15 -0
- package/Dockerfile +17 -0
- package/cleanup-docker.sh +4 -0
- package/commitlint.config.js +1 -0
- package/database/index.ts +76 -0
- package/database/migrations/001.do.init.sql +53 -0
- package/database/migrations/002.do.add-aggregated-sites.sql +16 -0
- package/database/migrations/003.do.add-generated-period-cols.sql +7 -0
- package/database/migrations/index.ts +36 -0
- package/database/seed/common.ts +7 -0
- package/database/seed/index.ts +55 -0
- package/database/seed/local/index.ts +28 -0
- package/database/seed/production/index.ts +3 -0
- package/database/seed/staging/index.ts +5 -0
- package/database/seed/test/index.ts +28 -0
- package/dev.ts +3 -0
- package/dist/aggregated-nodes/model/index.d.ts +25 -0
- package/dist/aggregated-nodes/model/index.js +11 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +17 -0
- package/docker-compose.yml +42 -0
- package/envs/.master.env +7 -0
- package/envs/.staging.env +7 -0
- package/index.js +3 -0
- package/package.json +105 -0
- package/run-docker.sh +14 -0
- package/run-test.sh +5 -0
- package/scripts/run-lambda.ts +10 -0
- package/scripts/run-migrations.ts +18 -0
- package/scripts/run-resetdb.ts +18 -0
- package/scripts/run-seed.ts +18 -0
- package/serverless.yml +76 -0
- package/src/aggregated-nodes/model/index.ts +37 -0
- package/src/aggregated-nodes/routes/pg-get-filters.ts +44 -0
- package/src/aggregated-nodes/routes/pg-get.ts +50 -0
- package/src/aggregated-nodes/routes.spec.ts +242 -0
- package/src/aggregated-nodes/routes.ts +56 -0
- package/src/aggregated-nodes/services/pg-get-filters.ts +52 -0
- package/src/aggregated-nodes/services/pg-get.ts +77 -0
- package/src/app.spec.ts +34 -0
- package/src/app.ts +59 -0
- package/src/config.ts +21 -0
- package/src/cors.spec.ts +32 -0
- package/src/cors.ts +7 -0
- package/src/errors.spec.ts +114 -0
- package/src/errors.ts +121 -0
- package/src/index.spec.ts +94 -0
- package/src/index.ts +14 -0
- package/src/lambdas/sentry.ts +12 -0
- package/src/lambdas/update-aggregated-nodes/handler.spec.ts +86 -0
- package/src/lambdas/update-aggregated-nodes/handler.ts +141 -0
- package/src/logger.spec.ts +20 -0
- package/src/logger.ts +45 -0
- package/src/maintenance.spec.ts +76 -0
- package/src/maintenance.ts +19 -0
- package/src/models.ts +1 -0
- package/src/routes.spec.ts +33 -0
- package/src/routes.ts +9 -0
- package/src/settings/model/index.ts +21 -0
- package/src/settings/routes/get.spec.ts +33 -0
- package/src/settings/routes/get.ts +3 -0
- package/src/settings/routes/update.spec.ts +33 -0
- package/src/settings/routes/update.ts +5 -0
- package/src/settings/routes.spec.ts +75 -0
- package/src/settings/routes.ts +21 -0
- package/src/settings/services/get.spec.ts +62 -0
- package/src/settings/services/get.ts +18 -0
- package/src/settings/services/update.spec.ts +118 -0
- package/src/settings/services/update.ts +47 -0
- package/src/slack.spec.ts +42 -0
- package/src/slack.ts +17 -0
- package/src/swagger/routes.ts +57 -0
- package/src/types/async-express-errors/index.d.ts +1 -0
- package/src/types/express/index.d.ts +10 -0
- package/src/utils/endpoint-wrapper.spec.ts +80 -0
- package/src/utils/endpoint-wrapper.ts +16 -0
- package/src/utils/middleware.spec.ts +154 -0
- package/src/utils/middleware.ts +33 -0
- package/test/Dockerfile +13 -0
- package/test/docker-compose.yml +40 -0
- package/test/fixtures/aggregated-nodes/get.ts +184 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv +5 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv.cycle.json +458 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-cycle_pivoted.csv.site.json +182 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-impactassessment_pivoted.csv +3 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleSeedWhole-impactassessment_pivoted.csv.impactAssessment.json +988 -0
- package/test/fixtures/update-aggregated-nodes/abyssinianKaleStraw-impactassessment_pivoted.csv +3 -0
- package/test/fixtures/update-aggregated-nodes/cycle-missing-impactassessment_pivoted.csv +3 -0
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv +5 -0
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv.cycle.json +584 -0
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-cycle_pivoted.csv.site.json +212 -0
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-impactassessment_pivoted.csv +3 -0
- package/test/fixtures/update-aggregated-nodes/tomatoFruit-impactassessment_pivoted.csv.impactAssessment.json +1002 -0
- package/test/prepare.ts +15 -0
- package/test/utils.ts +33 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.dist.json +14 -0
- package/tsconfig.json +37 -0
- package/tsconfig.lambdas.json +13 -0
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hestia-earth/data-api",
|
|
3
|
+
"version": "0.0.2-1",
|
|
4
|
+
"description": "Hestia Data API definitions",
|
|
5
|
+
"main": "dist/models.js",
|
|
6
|
+
"typings": "dist/models.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rm -rf build && tsc -p tsconfig.build.json",
|
|
9
|
+
"build:module": "rm -rf dist && tsc -p tsconfig.dist.json",
|
|
10
|
+
"build:lambdas": "rm -rf build-lambdas && tsc -p tsconfig.lambdas.json",
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"dev": "ts-node-dev --respawn --rs dev.ts --files --ignore-watch node_modules",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "npm run lint -- --fix",
|
|
15
|
+
"test": "nyc mocha \"./**/*.spec.ts\"",
|
|
16
|
+
"migrate": "ts-node scripts/run-migrations.ts",
|
|
17
|
+
"seed": "ts-node scripts/run-seed.ts",
|
|
18
|
+
"resetdb": "ts-node scripts/run-resetdb.ts && npm run migrate && npm run seed",
|
|
19
|
+
"pre-release": "standard-version --prerelease && git push origin develop --follow-tags",
|
|
20
|
+
"release": "standard-version",
|
|
21
|
+
"postrelease": "git push origin master --follow-tags"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+ssh://git@gitlab.com/hestia-earth/hestia-data-api.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Guillaume Royer <guillaume@hestia.earth>",
|
|
28
|
+
"license": "UNLICENSED",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"hestia",
|
|
31
|
+
"data",
|
|
32
|
+
"api"
|
|
33
|
+
],
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://gitlab.com/hestia-earth/hestia-data-api/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://gitlab.com/hestia-earth/hestia-data-api#readme",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@hestia-earth/schema": "^22.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@hestia-earth/pipeline-utils": "^0.10.4",
|
|
43
|
+
"@sentry/node": "^7.51.2",
|
|
44
|
+
"@sentry/serverless": "^7.55.2",
|
|
45
|
+
"@sentry/tracing": "^7.51.2",
|
|
46
|
+
"@slack/web-api": "^6.8.1",
|
|
47
|
+
"compression": "^1.7.4",
|
|
48
|
+
"cors": "^2.8.5",
|
|
49
|
+
"csvtojson": "^2.0.10",
|
|
50
|
+
"dotenv": "^16.0.3",
|
|
51
|
+
"exit-hook": "^3.2.0",
|
|
52
|
+
"express": "^4.17.3",
|
|
53
|
+
"express-async-errors": "^3.1.1",
|
|
54
|
+
"http2-express-bridge": "^1.0.7",
|
|
55
|
+
"lodash.chunk": "^4.2.0",
|
|
56
|
+
"lodash.uniqby": "^4.7.0",
|
|
57
|
+
"map-obj": "^5.0.2",
|
|
58
|
+
"morgan": "^1.10.0",
|
|
59
|
+
"pg": "^8.11.0",
|
|
60
|
+
"rxjs": "^7.8.1",
|
|
61
|
+
"swagger-jsdoc": "^6.2.8",
|
|
62
|
+
"swagger-ui-express": "^4.6.3",
|
|
63
|
+
"winston": "^3.8.2",
|
|
64
|
+
"@commitlint/cli": "^17.6.5",
|
|
65
|
+
"@commitlint/config-conventional": "^17.6.5",
|
|
66
|
+
"@hestia-earth/eslint-config": "^0.0.5",
|
|
67
|
+
"@types/chai": "^4.3.5",
|
|
68
|
+
"@types/chai-as-promised": "^7.1.5",
|
|
69
|
+
"@types/compression": "^1.7.2",
|
|
70
|
+
"@types/cors": "^2.8.13",
|
|
71
|
+
"@types/debug": "^4.1.8",
|
|
72
|
+
"@types/express": "^4.17.6",
|
|
73
|
+
"@types/lodash.chunk": "^4.2.7",
|
|
74
|
+
"@types/lodash.uniqby": "^4.7.7",
|
|
75
|
+
"@types/mocha-prepare": "^0.1.0",
|
|
76
|
+
"@types/morgan": "^1.9.4",
|
|
77
|
+
"@types/node-cleanup": "^2.1.2",
|
|
78
|
+
"@types/pg": "^8.10.1",
|
|
79
|
+
"@types/sinon": "^10.0.15",
|
|
80
|
+
"@types/supertest": "^2.0.12",
|
|
81
|
+
"@types/swagger-jsdoc": "^6.0.1",
|
|
82
|
+
"@types/swagger-ui-express": "^4.1.3",
|
|
83
|
+
"chai": "^4.3.7",
|
|
84
|
+
"chai-as-promised": "^7.1.1",
|
|
85
|
+
"eslint": "^7.32.0",
|
|
86
|
+
"husky": "^4.3.8",
|
|
87
|
+
"mocha-prepare": "^0.1.0",
|
|
88
|
+
"nyc": "^15.1.0",
|
|
89
|
+
"postgrator": "^7.1.1",
|
|
90
|
+
"serverless": "^3.32.2",
|
|
91
|
+
"serverless-deployment-bucket": "^1.6.0",
|
|
92
|
+
"serverless-offline": "^12.0.4",
|
|
93
|
+
"sinon": "^15.1.0",
|
|
94
|
+
"standard-version": "^9.5.0",
|
|
95
|
+
"supertest": "^6.3.3",
|
|
96
|
+
"ts-node": "^10.9.1",
|
|
97
|
+
"ts-node-dev": "^2.0.0",
|
|
98
|
+
"typescript": "^5.0.0"
|
|
99
|
+
},
|
|
100
|
+
"husky": {
|
|
101
|
+
"hooks": {
|
|
102
|
+
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/run-docker.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
});
|