diff --git a/.vscode/launch.json b/.vscode/launch.json index 0acc9101..73ffa482 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..426f80c8 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + } + ] +} diff --git a/docs/api/transaction/get-transactions.schema.json b/docs/api/transaction/get-transactions.schema.json index 104e8a40..be597e47 100644 --- a/docs/api/transaction/get-transactions.schema.json +++ b/docs/api/transaction/get-transactions.schema.json @@ -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": { diff --git a/docs/entities/transactions/abstract-tx-event.schema.json b/docs/entities/transactions/abstract-tx-event.schema.json index 3c7ccc2d..9e937ac7 100644 --- a/docs/entities/transactions/abstract-tx-event.schema.json +++ b/docs/entities/transactions/abstract-tx-event.schema.json @@ -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": { diff --git a/docs/entities/transactions/transaction-0-token-transfer.schema.json b/docs/entities/transactions/transaction-0-token-transfer.schema.json index 8bb4deea..de26d168 100644 --- a/docs/entities/transactions/transaction-0-token-transfer.schema.json +++ b/docs/entities/transactions/transaction-0-token-transfer.schema.json @@ -13,7 +13,6 @@ { "type": "object", "required": ["tx_type", "token_transfer"], - "additionalProperties": false, "properties": { "tx_type": { "type": "string", diff --git a/docs/entities/transactions/transaction-1-smart-contract.schema.json b/docs/entities/transactions/transaction-1-smart-contract.schema.json index 877965a6..e265ca2c 100644 --- a/docs/entities/transactions/transaction-1-smart-contract.schema.json +++ b/docs/entities/transactions/transaction-1-smart-contract.schema.json @@ -12,7 +12,6 @@ }, { "type": "object", - "additionalProperties": false, "required": ["tx_type", "smart_contract"], "properties": { "tx_type": { diff --git a/docs/entities/transactions/transaction-2-contract-call.schema.json b/docs/entities/transactions/transaction-2-contract-call.schema.json index 42309062..5d6755df 100644 --- a/docs/entities/transactions/transaction-2-contract-call.schema.json +++ b/docs/entities/transactions/transaction-2-contract-call.schema.json @@ -12,7 +12,6 @@ }, { "type": "object", - "additionalProperties": false, "required": ["tx_type", "contract_call"], "properties": { "tx_type": { diff --git a/gulpfile.js b/gulpfile.js index de9feb6e..3a71b205 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,5 +31,5 @@ function clean() { } exports.default = parallel(flattenSchemas, copyFiles); - +exports.flattenSchemas = flattenSchemas; exports.deployDocs = series(deployToGithubPages, clean); diff --git a/package-lock.json b/package-lock.json index d9ecfde9..b16393fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7b09cdc2..2adf84a7 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/api/routes/tx.ts b/src/api/routes/tx.ts index aaa12fc4..49cb7c4a 100644 --- a/src/api/routes/tx.ts +++ b/src/api/routes/tx.ts @@ -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); }); diff --git a/src/api/validate.ts b/src/api/validate.ts new file mode 100644 index 00000000..0104759d --- /dev/null +++ b/src/api/validate.ts @@ -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)}`); +} diff --git a/src/index.ts b/src/index.ts index b9823feb..a0c8647a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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()); } diff --git a/tsconfig.json b/tsconfig.json index b8755abe..d6c80e9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }