@hestia-earth/data-api 0.0.2-3 → 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 +1 -1
- package/.dockerignore +0 -25
- package/.env.test +0 -7
- package/.eslintignore +0 -7
- package/.eslintrc.js +0 -11
- package/.gitlab-ci.yml +0 -128
- package/.mocharc.js +0 -8
- package/.nvmrc +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 -60
- 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/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/package.serverless.json +0 -21
- 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 -101
- package/src/aggregated-nodes/model/index.ts +0 -37
- package/src/aggregated-nodes/routes/pg-get-filters.ts +0 -54
- package/src/aggregated-nodes/routes/pg-get.ts +0 -61
- package/src/aggregated-nodes/routes.spec.ts +0 -274
- package/src/aggregated-nodes/routes.ts +0 -56
- package/src/aggregated-nodes/services/pg-get-filters.ts +0 -62
- 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 -77
- package/src/lambdas/update-aggregated-nodes/handler.ts +0 -129
- 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 -196
- 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 -32
- package/tsconfig.build.json +0 -13
- package/tsconfig.dist.json +0 -14
- package/tsconfig.json +0 -42
- package/tsconfig.lambdas.json +0 -13
|
@@ -1,62 +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
|
-
COALESCE(ARRAY_AGG(DISTINCT products ORDER BY products) FILTER (WHERE products IS NOT NULL), '{}') as products,
|
|
13
|
-
COALESCE(
|
|
14
|
-
ARRAY_AGG(DISTINCT c.generated_period ORDER BY c.generated_period) FILTER (WHERE c.generated_period IS NOT NULL),
|
|
15
|
-
'{}'
|
|
16
|
-
) as periods,
|
|
17
|
-
COALESCE(ARRAY_AGG(DISTINCT practices ORDER BY practices) FILTER (WHERE practices IS NOT NULL), '{}') as practices,
|
|
18
|
-
COALESCE(
|
|
19
|
-
ARRAY_AGG(
|
|
20
|
-
DISTINCT (c.jsonld_pivoted->>'aggregatedQualityScore')::integer
|
|
21
|
-
ORDER BY (c.jsonld_pivoted->>'aggregatedQualityScore')::integer
|
|
22
|
-
) FILTER (WHERE c.jsonld_pivoted->>'aggregatedQualityScore' is NOT NULL),
|
|
23
|
-
'{}'
|
|
24
|
-
) AS "minAggregatedQualityScore",
|
|
25
|
-
COALESCE(ARRAY_AGG(
|
|
26
|
-
DISTINCT s.jsonld_pivoted['country']->>'@id'
|
|
27
|
-
ORDER BY s.jsonld_pivoted['country']->>'@id'
|
|
28
|
-
) FILTER (WHERE s.jsonld_pivoted['country']->>'@id' IS NOT NULL), '{}') AS regions,
|
|
29
|
-
COALESCE(
|
|
30
|
-
ARRAY_AGG(
|
|
31
|
-
DISTINCT c.jsonld_pivoted->>'defaultMethodClassification'
|
|
32
|
-
ORDER BY c.jsonld_pivoted->>'defaultMethodClassification'
|
|
33
|
-
) FILTER (WHERE c.jsonld_pivoted->>'defaultMethodClassification' IS NOT NULL),
|
|
34
|
-
'{}'
|
|
35
|
-
) AS "defaultMethodClassifications"
|
|
36
|
-
FROM aggregated_cycles AS c
|
|
37
|
-
LEFT JOIN
|
|
38
|
-
jsonb_object_keys(c.jsonld_pivoted['products']) as products on TRUE
|
|
39
|
-
LEFT JOIN
|
|
40
|
-
jsonb_object_keys(c.jsonld_pivoted['practices']) as practices on TRUE
|
|
41
|
-
LEFT JOIN aggregated_impact_assessments AS ia
|
|
42
|
-
ON c.hestia_id = ia.cycle_hestia_id
|
|
43
|
-
AND ${filterClauses.products.iaFilter}
|
|
44
|
-
JOIN aggregated_sites AS s
|
|
45
|
-
ON c.site_hestia_id = s.hestia_id
|
|
46
|
-
AND ${filterClauses.regions}
|
|
47
|
-
WHERE ${filterClauses.practices}
|
|
48
|
-
AND ${filterClauses.defaultMethodClassifications}
|
|
49
|
-
AND ${filterClauses.products.cycleFilter}
|
|
50
|
-
AND ${filterClauses.minAggregatedQualityScore}
|
|
51
|
-
AND ${filterClauses.periods}
|
|
52
|
-
`,
|
|
53
|
-
params
|
|
54
|
-
];
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const applyFilters = async ({ filter }: { filter: IFilter; query?: string }) => {
|
|
58
|
-
// TODO: handle query later
|
|
59
|
-
const [pgQuery, params] = getFiltersQuery(filter);
|
|
60
|
-
const res = await doQuery<IFilters>(pgQuery, params);
|
|
61
|
-
return res?.rows[0];
|
|
62
|
-
};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { partition as _partition } from 'lodash';
|
|
2
|
-
import { query as doQuery } from '../../../database';
|
|
3
|
-
import { AggregatedNodeRow, IFilter } from '../model';
|
|
4
|
-
|
|
5
|
-
type FilterClauseFunc = (args: { idx: number; exclude: boolean }) => string | Record<string, string>;
|
|
6
|
-
|
|
7
|
-
const filterClauseMapping: Record<keyof IFilter, FilterClauseFunc> = {
|
|
8
|
-
periods: ({ idx, exclude = false }) => (exclude ? 'TRUE' : `c.generated_period = ANY($${idx}::text[])`),
|
|
9
|
-
// IA aggregatedQualityScore is always equal to that of cycle
|
|
10
|
-
minAggregatedQualityScore: ({ idx, exclude = false }) =>
|
|
11
|
-
exclude ? 'TRUE' : `(c.jsonld_pivoted->>'aggregatedQualityScore')::integer >= $${idx}`,
|
|
12
|
-
practices: ({ idx, exclude = false }) => (exclude ? 'TRUE' : `c.jsonld_pivoted['practices'] ?| $${idx}::text[]`),
|
|
13
|
-
products: ({ idx, exclude = false }) => ({
|
|
14
|
-
iaFilter: exclude ? 'TRUE' : `ia.jsonld_pivoted['product']['term']->>'@id' = ANY($${idx}::text[])`,
|
|
15
|
-
cycleFilter: exclude ? 'TRUE' : `c.jsonld_pivoted['products'] ?| $${idx}::text[]`
|
|
16
|
-
}),
|
|
17
|
-
// country field also holds values like 'Northern Europe'
|
|
18
|
-
regions: ({ idx, exclude = false }) =>
|
|
19
|
-
exclude ? 'TRUE' : `s.jsonld_pivoted['country']->>'@id' = ANY($${idx}::text[])`,
|
|
20
|
-
defaultMethodClassifications: ({ idx, exclude = false }) =>
|
|
21
|
-
exclude ? 'TRUE' : `c.jsonld_pivoted->>'defaultMethodClassification' = ANY($${idx}::text[])`
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const getFilterClauses = (filter: IFilter) => {
|
|
25
|
-
const filterClauses: Partial<Record<keyof IFilter, Record<string, string>>> = {};
|
|
26
|
-
const params = [];
|
|
27
|
-
|
|
28
|
-
const [included, excluded] = _partition(Object.keys(filterClauseMapping), (key) => filter[key]);
|
|
29
|
-
|
|
30
|
-
included.forEach((key, idx) => {
|
|
31
|
-
filterClauses[key] = filterClauseMapping[key]({ idx: idx + 1 });
|
|
32
|
-
params.push(filter[key]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
excluded.forEach((key) => {
|
|
36
|
-
filterClauses[key] = filterClauseMapping[key]({ exclude: true });
|
|
37
|
-
});
|
|
38
|
-
return { filterClauses, params };
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const getNodesQuery = (filter: IFilter): [string, any[]] => {
|
|
42
|
-
const { filterClauses, params } = getFilterClauses(filter);
|
|
43
|
-
return [
|
|
44
|
-
`
|
|
45
|
-
SELECT
|
|
46
|
-
c.jsonld_pivoted AS cycle,
|
|
47
|
-
s.jsonld_pivoted['country'] AS region,
|
|
48
|
-
COALESCE(
|
|
49
|
-
jsonb_agg(ia.jsonld_pivoted ORDER BY ia.jsonld_pivoted->>'@id') FILTER (WHERE ia.jsonld_pivoted IS NOT NULL),
|
|
50
|
-
'[]'
|
|
51
|
-
) AS "impactAssessments"
|
|
52
|
-
FROM aggregated_cycles AS c
|
|
53
|
-
LEFT JOIN aggregated_impact_assessments AS ia
|
|
54
|
-
ON c.hestia_id = ia.cycle_hestia_id
|
|
55
|
-
AND ${filterClauses.products.iaFilter}
|
|
56
|
-
JOIN aggregated_sites AS s
|
|
57
|
-
ON c.site_hestia_id = s.hestia_id
|
|
58
|
-
AND ${filterClauses.regions}
|
|
59
|
-
WHERE ${filterClauses.practices}
|
|
60
|
-
AND ${filterClauses.defaultMethodClassifications}
|
|
61
|
-
AND ${filterClauses.products.cycleFilter}
|
|
62
|
-
AND ${filterClauses.minAggregatedQualityScore}
|
|
63
|
-
AND ${filterClauses.periods}
|
|
64
|
-
GROUP BY cycle, region
|
|
65
|
-
ORDER BY c.jsonld_pivoted->>'@id'
|
|
66
|
-
LIMIT 500
|
|
67
|
-
`,
|
|
68
|
-
params
|
|
69
|
-
];
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const applyFilter = async ({ filter }: { page?: number; filter: IFilter; query?: string }) => {
|
|
73
|
-
// TODO: handle page and query later
|
|
74
|
-
const [pgQuery, params] = getNodesQuery(filter);
|
|
75
|
-
const result = await doQuery<AggregatedNodeRow>(pgQuery, params);
|
|
76
|
-
return { count: result.rowCount, results: result.rows };
|
|
77
|
-
};
|
package/src/app.spec.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import * as sinon from 'sinon';
|
|
3
|
-
import 'mocha';
|
|
4
|
-
|
|
5
|
-
import { logger } from './logger';
|
|
6
|
-
import { app, healthcheck } from './app';
|
|
7
|
-
|
|
8
|
-
let stubs: sinon.SinonStub[] = [];
|
|
9
|
-
|
|
10
|
-
describe('app', () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
stubs = [];
|
|
13
|
-
stubs.push(sinon.stub(logger, 'error'));
|
|
14
|
-
stubs.push(sinon.stub(logger, 'debug'));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
stubs.forEach((stub) => stub.restore());
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should be defined', () => {
|
|
22
|
-
expect(app !== undefined).to.equal(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('healthcheck', () => {
|
|
26
|
-
it('should return status 200', () => {
|
|
27
|
-
const res = {
|
|
28
|
-
status: sinon.stub().returns({ send() {} })
|
|
29
|
-
};
|
|
30
|
-
healthcheck(null, res);
|
|
31
|
-
expect(res.status.calledWith(200)).to.equal(true);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
package/src/app.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import * as express from 'express';
|
|
2
|
-
import 'express-async-errors';
|
|
3
|
-
const http2Express = require('http2-express-bridge');
|
|
4
|
-
import { RequestHandler } from 'express';
|
|
5
|
-
|
|
6
|
-
const app: express.Express = http2Express(express);
|
|
7
|
-
|
|
8
|
-
import * as Sentry from '@sentry/node';
|
|
9
|
-
import * as Tracing from '@sentry/tracing';
|
|
10
|
-
import { sentryConfig } from './config';
|
|
11
|
-
|
|
12
|
-
Sentry.init({
|
|
13
|
-
...sentryConfig,
|
|
14
|
-
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })]
|
|
15
|
-
});
|
|
16
|
-
app.use(Sentry.Handlers.requestHandler());
|
|
17
|
-
app.use(Sentry.Handlers.tracingHandler());
|
|
18
|
-
|
|
19
|
-
import cors from 'cors';
|
|
20
|
-
import { disableCors } from './config';
|
|
21
|
-
import { whitelistOrigin } from './cors';
|
|
22
|
-
app.use(cors(disableCors ? undefined : { origin: whitelistOrigin }));
|
|
23
|
-
|
|
24
|
-
import compression from 'compression';
|
|
25
|
-
app.use(compression());
|
|
26
|
-
|
|
27
|
-
import * as bodyParser from 'body-parser';
|
|
28
|
-
// based on AWS Lambda body size limit
|
|
29
|
-
const limit = '6mb';
|
|
30
|
-
app.use(bodyParser.json({ limit }));
|
|
31
|
-
app.use(bodyParser.urlencoded({ limit, extended: false }));
|
|
32
|
-
|
|
33
|
-
export const healthcheck = (_req, res) => res.status(200).send();
|
|
34
|
-
app.get('/health', healthcheck);
|
|
35
|
-
|
|
36
|
-
import swagger from './swagger/routes';
|
|
37
|
-
app.use('/swagger', swagger());
|
|
38
|
-
|
|
39
|
-
// make sure settings is not blocked by maintenance mode
|
|
40
|
-
import settings from './settings/routes';
|
|
41
|
-
app.use('/settings', settings());
|
|
42
|
-
|
|
43
|
-
import { maintenance } from './maintenance';
|
|
44
|
-
// Until express v5 promises must be coerced to void
|
|
45
|
-
// See https://github.com/davidbanham/express-async-errors/issues/36#issuecomment-944954003
|
|
46
|
-
app.use(maintenance as RequestHandler);
|
|
47
|
-
|
|
48
|
-
// log routes after healthcheck and maintenance
|
|
49
|
-
import morgan from 'morgan';
|
|
50
|
-
import { stream } from './logger';
|
|
51
|
-
app.use(morgan('combined', { stream }));
|
|
52
|
-
|
|
53
|
-
import routes from './routes';
|
|
54
|
-
routes(app);
|
|
55
|
-
|
|
56
|
-
import errorHandler from './errors';
|
|
57
|
-
errorHandler(app);
|
|
58
|
-
|
|
59
|
-
export { app };
|
package/src/config.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const { version } = require('../package.json');
|
|
2
|
-
|
|
3
|
-
export const PORT = process.env.PORT || '3000';
|
|
4
|
-
export const apiUrl = (process.env.API_URL || 'http://localhost:{PORT}').replace('{PORT}', PORT);
|
|
5
|
-
export const webappUrl = process.env.WEBAPP_URL || 'http://localhost';
|
|
6
|
-
|
|
7
|
-
export const disableCors = (process.env.CORS_DISABLED || 'false') === 'true';
|
|
8
|
-
export const whitelist = (process.env.CORS_WHITELIST || '').split(',');
|
|
9
|
-
whitelist.push(apiUrl);
|
|
10
|
-
whitelist.push(webappUrl);
|
|
11
|
-
|
|
12
|
-
export const sentryConfig = {
|
|
13
|
-
dsn: process.env.SENTRY_DSN,
|
|
14
|
-
environment: process.env.SENTRY_ENV,
|
|
15
|
-
release: version
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const slackConfig = {
|
|
19
|
-
token: process.env.SLACK_TOKEN,
|
|
20
|
-
channel: process.env.SLACK_CHANNEL
|
|
21
|
-
};
|
package/src/cors.spec.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import * as sinon from 'sinon';
|
|
3
|
-
import 'mocha';
|
|
4
|
-
|
|
5
|
-
import { whitelistOrigin } from './cors';
|
|
6
|
-
|
|
7
|
-
describe('cors', () => {
|
|
8
|
-
describe('whitelistOrigin', () => {
|
|
9
|
-
const origin = 'http://localhost';
|
|
10
|
-
let callback: sinon.SinonStub;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
callback = sinon.stub();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should allow localhost', () => {
|
|
17
|
-
whitelistOrigin(origin, callback);
|
|
18
|
-
expect(callback.calledWith(null, true)).to.equal(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should allow no origin', () => {
|
|
22
|
-
whitelistOrigin(undefined, callback);
|
|
23
|
-
expect(callback.calledWith(null, true)).to.equal(true);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should not allow other origin', () => {
|
|
27
|
-
const other = 'other origin';
|
|
28
|
-
whitelistOrigin(other, callback);
|
|
29
|
-
expect(callback.calledWith(null, true)).to.equal(false);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
});
|
package/src/cors.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { whitelist } from './config';
|
|
2
|
-
import { HestiaError } from './errors';
|
|
3
|
-
|
|
4
|
-
export const whitelistOrigin = (origin: string, callback: Function) =>
|
|
5
|
-
!origin || whitelist.includes(origin)
|
|
6
|
-
? callback(null, true)
|
|
7
|
-
: callback(new HestiaError(`${origin} is not allowed by CORS`));
|
package/src/errors.spec.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import * as sinon from 'sinon';
|
|
3
|
-
import 'mocha';
|
|
4
|
-
|
|
5
|
-
import { logger } from './logger';
|
|
6
|
-
import { Errors, errorHandler, HestiaError } from './errors';
|
|
7
|
-
|
|
8
|
-
class Response {
|
|
9
|
-
statusCode = 200;
|
|
10
|
-
json() {}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let stubs: sinon.SinonStub[] = [];
|
|
14
|
-
|
|
15
|
-
describe('errors', () => {
|
|
16
|
-
let errorStub: sinon.SinonStub;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
stubs = [];
|
|
20
|
-
stubs.push((errorStub = sinon.stub(logger, 'error')));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
stubs.forEach((stub) => stub.restore());
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('errorHandler', () => {
|
|
28
|
-
let error: any = {
|
|
29
|
-
message: 'error'
|
|
30
|
-
};
|
|
31
|
-
const response = new Response();
|
|
32
|
-
|
|
33
|
-
it('should set status code to 500', () => {
|
|
34
|
-
errorHandler(error, null, response as any);
|
|
35
|
-
expect(response.statusCode).to.equal(500);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should render error as json', () => {
|
|
39
|
-
let stub: sinon.SinonStub;
|
|
40
|
-
stubs.push((stub = sinon.stub(response, 'json')));
|
|
41
|
-
errorHandler(error, null, response as any);
|
|
42
|
-
|
|
43
|
-
expect(
|
|
44
|
-
stub.calledWith({
|
|
45
|
-
message: error.message
|
|
46
|
-
})
|
|
47
|
-
).to.equal(true);
|
|
48
|
-
stub.restore();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should log Error stack', () => {
|
|
52
|
-
const err = new Error();
|
|
53
|
-
errorHandler(err, null, response as any);
|
|
54
|
-
expect(errorStub.calledWith(err.stack)).to.equal(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('Hestia error', () => {
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
error = new HestiaError('error');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should not log the error', () => {
|
|
63
|
-
errorHandler(error, null, response as any);
|
|
64
|
-
expect(errorStub.called).to.equal(false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should set status code to 400', () => {
|
|
68
|
-
errorHandler(error, null, response as any);
|
|
69
|
-
expect(response.statusCode).to.equal(400);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe(Errors.Unauthorized, () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
error = { message: Errors.Unauthorized };
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should set status code to 401', () => {
|
|
79
|
-
errorHandler(error, null, response as any);
|
|
80
|
-
expect(response.statusCode).to.equal(401);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe(Errors.NotFound, () => {
|
|
85
|
-
beforeEach(() => {
|
|
86
|
-
error = { message: Errors.NotFound };
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should set status code to 404', () => {
|
|
90
|
-
errorHandler(error, null, response as any);
|
|
91
|
-
expect(response.statusCode).to.equal(404);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('no message', () => {
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
error = {};
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should render default error', () => {
|
|
101
|
-
let stub: sinon.SinonStub;
|
|
102
|
-
stubs.push((stub = sinon.stub(response, 'json')));
|
|
103
|
-
errorHandler(error, null, response as any);
|
|
104
|
-
|
|
105
|
-
expect(
|
|
106
|
-
stub.calledWith({
|
|
107
|
-
message: 'There was an error processing your request.'
|
|
108
|
-
})
|
|
109
|
-
).to.equal(true);
|
|
110
|
-
stub.restore();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
package/src/errors.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { Express, Request, Response, NextFunction } from 'express';
|
|
2
|
-
import * as Sentry from '@sentry/node';
|
|
3
|
-
|
|
4
|
-
import { logger } from './logger';
|
|
5
|
-
|
|
6
|
-
export enum Errors {
|
|
7
|
-
UnderMaintenance = 'under-maintenance',
|
|
8
|
-
ReadOnly = 'read-only',
|
|
9
|
-
InvalidQueryParam = 'invalid-param-query',
|
|
10
|
-
MissingQueryParam = 'missing-param-query',
|
|
11
|
-
InvalidBodyParam = 'invalid-param-body',
|
|
12
|
-
MissingBodyParam = 'missing-param-body',
|
|
13
|
-
SchemaInvalid = 'invalid-schema',
|
|
14
|
-
NotFound = 'not-found',
|
|
15
|
-
Unauthorized = 'Unauthorized'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export enum StatusCodes {
|
|
19
|
-
MethodNotAllowed = 405,
|
|
20
|
-
RateLimit = 429,
|
|
21
|
-
ServiceUnavailable = 503,
|
|
22
|
-
InternalServerError = 500
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class HestiaError extends Error {
|
|
26
|
-
constructor(message: string, public details = {}, public statusCode?: StatusCodes) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = 'HestiaError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const throwError = (message: string, details?: any, statusCode?: StatusCodes) => {
|
|
33
|
-
throw new HestiaError(message, details, statusCode);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const isHestiaError = (error: Error) => error.name === 'HestiaError';
|
|
37
|
-
|
|
38
|
-
export const shouldHandleError = (error: Error) => !isHestiaError(error);
|
|
39
|
-
|
|
40
|
-
const statusCode = (error: Error) =>
|
|
41
|
-
(error as any).statusCode ||
|
|
42
|
-
(errorMessage(error).includes(Errors.NotFound)
|
|
43
|
-
? 404
|
|
44
|
-
: error.message === Errors.Unauthorized
|
|
45
|
-
? 401
|
|
46
|
-
: isHestiaError(error)
|
|
47
|
-
? 400
|
|
48
|
-
: 500);
|
|
49
|
-
|
|
50
|
-
export const errorMessage = (error?: Error) => error?.message || 'There was an error processing your request.';
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @swagger
|
|
54
|
-
*
|
|
55
|
-
* components:
|
|
56
|
-
* responses:
|
|
57
|
-
* DefaultObject:
|
|
58
|
-
* content:
|
|
59
|
-
* application/json:
|
|
60
|
-
* schema:
|
|
61
|
-
* type: object
|
|
62
|
-
* BadRequestError:
|
|
63
|
-
* description: Bad Request
|
|
64
|
-
* content:
|
|
65
|
-
* application/json:
|
|
66
|
-
* schema:
|
|
67
|
-
* $ref: '#/components/schemas/Error'
|
|
68
|
-
* UnauthorizedError:
|
|
69
|
-
* description: Unauthorized
|
|
70
|
-
* content:
|
|
71
|
-
* application/json:
|
|
72
|
-
* schema:
|
|
73
|
-
* $ref: '#/components/schemas/Error'
|
|
74
|
-
* example:
|
|
75
|
-
* - message: Unauthorized
|
|
76
|
-
* NotFoundError:
|
|
77
|
-
* description: The requested ressource was not found
|
|
78
|
-
* content:
|
|
79
|
-
* application/json:
|
|
80
|
-
* schema:
|
|
81
|
-
* $ref: '#/components/schemas/Error'
|
|
82
|
-
* UnexpectedError:
|
|
83
|
-
* description: Unexpected error
|
|
84
|
-
* content:
|
|
85
|
-
* application/json:
|
|
86
|
-
* schema:
|
|
87
|
-
* $ref: '#/components/schemas/Error'
|
|
88
|
-
* ServiceUnavailable:
|
|
89
|
-
* description: Service Unavailable due to Maintenance
|
|
90
|
-
* content:
|
|
91
|
-
* application/json:
|
|
92
|
-
* schema:
|
|
93
|
-
* $ref: '#/components/schemas/Error'
|
|
94
|
-
* NoContent:
|
|
95
|
-
* description: Return nothing
|
|
96
|
-
*
|
|
97
|
-
* schemas:
|
|
98
|
-
* Error:
|
|
99
|
-
* type: object
|
|
100
|
-
* required:
|
|
101
|
-
* - message
|
|
102
|
-
* properties:
|
|
103
|
-
* message:
|
|
104
|
-
* type: string
|
|
105
|
-
*/
|
|
106
|
-
export const errorHandler = (err: any, _r: Request, res: Response, _n?: NextFunction) => {
|
|
107
|
-
// error was not thrown intentionally
|
|
108
|
-
const unexpectedError = shouldHandleError(err);
|
|
109
|
-
unexpectedError && logger.error(err.stack ? err.stack : err);
|
|
110
|
-
const message = errorMessage(err);
|
|
111
|
-
res.statusCode = statusCode(err);
|
|
112
|
-
return res.json({
|
|
113
|
-
message,
|
|
114
|
-
...('details' in err ? { details: err.details } : {})
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export default (app: Express) => {
|
|
119
|
-
app.use(Sentry.Handlers.errorHandler({ shouldHandleError }));
|
|
120
|
-
app.use(errorHandler);
|
|
121
|
-
};
|
package/src/index.spec.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import * as sinon from 'sinon';
|
|
3
|
-
import 'mocha';
|
|
4
|
-
|
|
5
|
-
import { run } from './index';
|
|
6
|
-
import { app } from './app';
|
|
7
|
-
import { logger } from './logger';
|
|
8
|
-
import { PORT } from './config';
|
|
9
|
-
|
|
10
|
-
let stubs: sinon.SinonStub[] = [];
|
|
11
|
-
|
|
12
|
-
describe('run', () => {
|
|
13
|
-
const server: any = {};
|
|
14
|
-
let loggerStub: sinon.SinonStub;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
stubs = [];
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
stubs.forEach((stub) => stub.restore());
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('start error', () => {
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
loggerStub = sinon.stub(logger, 'error');
|
|
27
|
-
stubs.push(loggerStub);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('error as string', () => {
|
|
31
|
-
const error = 'error';
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
stubs.push(
|
|
35
|
-
sinon.stub(app, 'listen').returns({
|
|
36
|
-
on: (_p, callback: any) => {
|
|
37
|
-
callback(error);
|
|
38
|
-
return server;
|
|
39
|
-
}
|
|
40
|
-
} as any)
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should log the error', () => {
|
|
45
|
-
run();
|
|
46
|
-
expect(loggerStub.calledWith(error)).to.equal(true);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('error as Error', () => {
|
|
51
|
-
let error: Error;
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
error = new Error('error');
|
|
55
|
-
|
|
56
|
-
stubs.push(
|
|
57
|
-
sinon.stub(app, 'listen').returns({
|
|
58
|
-
on: (_p, callback: any) => {
|
|
59
|
-
callback(error);
|
|
60
|
-
return server;
|
|
61
|
-
}
|
|
62
|
-
} as any)
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should log the error', () => {
|
|
67
|
-
run();
|
|
68
|
-
expect(loggerStub.calledWith(error.stack)).to.equal(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('start success', () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
stubs.push(
|
|
76
|
-
sinon.stub(app, 'listen').callsFake((_p, callback) => {
|
|
77
|
-
callback();
|
|
78
|
-
return {
|
|
79
|
-
on: (_p1, _cb: any) => {
|
|
80
|
-
return server;
|
|
81
|
-
}
|
|
82
|
-
} as any;
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
loggerStub = sinon.stub(logger, 'info');
|
|
86
|
-
stubs.push(loggerStub);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should log the port', () => {
|
|
90
|
-
run();
|
|
91
|
-
expect(loggerStub.calledWith(`App running on port ${PORT}`)).to.equal(true);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import '../database';
|
|
2
|
-
import { logger } from './logger';
|
|
3
|
-
import { PORT } from './config';
|
|
4
|
-
import { app } from './app';
|
|
5
|
-
|
|
6
|
-
export const run = () => {
|
|
7
|
-
const server = app
|
|
8
|
-
.listen(PORT, () => logger.info(`App running on port ${PORT}`))
|
|
9
|
-
.on('error', (err) => logger.error(err.stack || err));
|
|
10
|
-
|
|
11
|
-
// ALB idle timeout set to 300 seconds
|
|
12
|
-
server.keepAliveTimeout = 301 * 1000;
|
|
13
|
-
server.headersTimeout = 305 * 1000;
|
|
14
|
-
};
|
package/src/lambdas/sentry.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as Sentry from '@sentry/serverless';
|
|
2
|
-
|
|
3
|
-
import pkg from '../../package.json';
|
|
4
|
-
|
|
5
|
-
Sentry.AWSLambda.init({
|
|
6
|
-
dsn: process.env.SENTRY_DSN,
|
|
7
|
-
environment: process.env.STAGE,
|
|
8
|
-
release: pkg.version,
|
|
9
|
-
enabled: process.env.IS_OFFLINE !== 'true'
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const wrapHandler = handler => Sentry.AWSLambda.wrapHandler(handler);
|