feat: add schema validation to api responses

This commit is contained in:
kyranjamie
2020-04-08 14:32:34 +02:00
committed by kyranjamie
parent 06dd51fead
commit dd6be80782
14 changed files with 54 additions and 25 deletions

6
.vscode/launch.json vendored
View File

@@ -8,10 +8,11 @@
"type": "node",
"request": "launch",
"name": "Launch Program",
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
"runtimeArgs": ["-r", "ts-node/register/transpile-only", "-r", "tsconfig-paths/register"],
"args": ["${workspaceFolder}/src/index.ts"],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "generate:schemas",
"env": {
"NODE_ENV": "development",
"TS_NODE_SKIP_IGNORE": "true"
@@ -21,10 +22,11 @@
"type": "node",
"request": "launch",
"name": "Launch Program - memory-db",
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
"runtimeArgs": ["-r", "ts-node/register/transpile-only", "-r", "tsconfig-paths/register"],
"args": ["${workspaceFolder}/src/index.ts", "--compile-schemas"],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "generate:schemas",
"env": {
"STACKS_SIDECAR_DB": "memory",
"NODE_ENV": "development",

15
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "generate:schemas",
"type": "npm",
"script": "generate:schemas",
"presentation": {
"reveal": "silent"
}
}
]
}

View File

@@ -2,7 +2,6 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "GET request that returns transactions",
"type": "object",
"additionalProperties": false,
"required": ["results"],
"properties": {
"results": {

View File

@@ -2,7 +2,6 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "Abstract transaction event. Only present in `smart_contract` and `contract_call` tx types.",
"additionalProperties": false,
"required": ["events"],
"properties": {
"events": {

View File

@@ -13,7 +13,6 @@
{
"type": "object",
"required": ["tx_type", "token_transfer"],
"additionalProperties": false,
"properties": {
"tx_type": {
"type": "string",

View File

@@ -12,7 +12,6 @@
},
{
"type": "object",
"additionalProperties": false,
"required": ["tx_type", "smart_contract"],
"properties": {
"tx_type": {

View File

@@ -12,7 +12,6 @@
},
{
"type": "object",
"additionalProperties": false,
"required": ["tx_type", "contract_call"],
"properties": {
"tx_type": {

View File

@@ -31,5 +31,5 @@ function clean() {
}
exports.default = parallel(flattenSchemas, copyFiles);
exports.flattenSchemas = flattenSchemas;
exports.deployDocs = series(deployToGithubPages, clean);

18
package-lock.json generated
View File

@@ -874,6 +874,15 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/ajv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-1.0.0.tgz",
"integrity": "sha1-T7JEB0Ly9sMOf7B5e4OfxvaWaCo=",
"dev": true,
"requires": {
"ajv": "*"
}
},
"@types/babel__core": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.6.tgz",
@@ -1012,15 +1021,6 @@
"@types/range-parser": "*"
}
},
"@types/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",

View File

@@ -14,8 +14,9 @@
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx -f codeframe --fix",
"migrate": "ts-node node_modules/.bin/node-pg-migrate -j ts",
"generate:types": "ts-node ./scripts/generate-types.ts",
"generate:schemas": "gulp --silent && npm run generate:types",
"generate:docs": "redoc-cli bundle --output .tmp/index.html docs/openapi.yaml",
"validate:schemas": "gulp --silent && ./scripts/validate-schemas.ts",
"validate:schemas": "gulp flattenSchemas --silent && ./scripts/validate-schemas.ts",
"deploy:docs": "npm run generate:types && npm run generate:docs && gulp deployDocs"
},
"repository": {
@@ -55,6 +56,7 @@
"@apidevtools/json-schema-ref-parser": "^8.0.0",
"@blockstack/eslint-config": "^1.0.3",
"@blockstack/prettier-config": "0.0.6",
"@types/ajv": "^1.0.0",
"@types/bluebird": "^3.5.30",
"@types/bn.js": "^4.11.6",
"@types/compression": "^1.7.0",
@@ -88,6 +90,7 @@
"redoc-cli": "^0.9.7",
"ts-jest": "^25.2.1",
"ts-node": "^8.8.1",
"tsconfig-paths": "^3.9.0",
"yaml-lint": "^1.2.4"
}
}

View File

@@ -5,6 +5,10 @@ import * as Bluebird from 'bluebird';
import { DataStore, DbTx } from '../../datastore/common';
import { getTxFromDataStore } from '../controllers/db-controller';
import { timeout, waiter } from '../../helpers';
import { validate } from '../validate';
import * as txSchema from '@schemas/entities/transactions/transaction.schema.json';
import * as txResultsSchema from '@schemas/api/transaction/get-transactions.schema.json';
export function createTxRouter(db: DataStore): RouterWithAsync {
const router = addAsync(express.Router());
@@ -12,10 +16,11 @@ export function createTxRouter(db: DataStore): RouterWithAsync {
router.getAsync('/', async (req, res) => {
const txs = await db.getTxList();
const fullTxs = await Bluebird.mapSeries(txs.results, async tx => {
const results = await Bluebird.mapSeries(txs.results, async tx => {
return await getTxFromDataStore(tx.tx_id, db);
});
res.json({ results: fullTxs });
await validate(txResultsSchema, { results });
res.json({ results });
});
router.getAsync('/stream', async (req, res) => {
@@ -69,6 +74,7 @@ export function createTxRouter(db: DataStore): RouterWithAsync {
router.getAsync('/:tx_id', async (req, res) => {
const { tx_id } = req.params;
const txResponse = await getTxFromDataStore(tx_id, db);
await validate(txSchema, txResponse);
res.json(txResponse);
});

8
src/api/validate.ts Normal file
View File

@@ -0,0 +1,8 @@
import * as Ajv from 'ajv';
export async function validate(schema: any, data: any) {
if (process.env.NODE_ENV !== 'development') return;
const ajv = new Ajv({ schemaId: 'auto' });
const valid = await ajv.validate(schema, data);
if (!valid) throw new Error(`Schema validation:\n\n ${JSON.stringify(ajv.errors, null, 2)}`);
}

View File

@@ -26,10 +26,9 @@ import { TransactionPayloadTypeID } from './p2p/tx';
loadDotEnv();
const compileSchemas = process.argv.includes('--compile-schemas');
const generateSchemas = () => exec('npm run generate:types');
const generateSchemas = () => exec('npm run generate:schemas');
if (compileSchemas) {
generateSchemas();
watch('./docs', { recursive: true, filter: /\.schema\.json$/ }, () => generateSchemas());
}

View File

@@ -13,9 +13,10 @@
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@entities": [".tmp/index.ts"]
"@entities": [".tmp/index.ts"],
"@schemas/*": [".tmp/*"]
}
},
"include": ["src/**/*", "migrations/**/*", "tests/**/*"],
"exclude": ["lib", ".tmp/**/*"]
"include": ["src/**/*", "migrations/**/*", "tests/**/*", ".tmp/**/*.schema.json"],
"exclude": ["lib", ".tmp"]
}