Skip to content

Commit

Permalink
client [NET-992]: pre-compiled JSON schema validator (#1642)
Browse files Browse the repository at this point in the history
## Summary

Compile JSON schema validator functions at build time instead of run
time as described here https://ajv.js.org/standalone.html to avoid
having to rely on `eval` during run time. Another added benefit is that
we don't need to include `ajv` in our webpack build, reducing the
browser bundle size slightly.

## Changes
- Mark packages `ajv` and `ajv-format` as dev dependencies.
- Add script `bin/generate-config-validator.js` that generates the
pre-built JSON schema validator function.
- Call above script in `prebuild` phase, ensuring it gets called every
time the client package is built.
- Add folder `src/generated` for generated code that gets ignored by
eslint.
  • Loading branch information
harbu authored Jul 6, 2023
1 parent 597a17c commit 7c07067
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 19 deletions.
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/client/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ test/benchmarks/**
test/memory/*
docs/**
.idea/**
src/generated/**
30 changes: 30 additions & 0 deletions packages/client/bin/generate-config-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* This script generates a client config AJV validation function to avoid
* having to compile it during run time which requires the use of `eval`.
* Use of `eval` is not allowed e.g. in Chrome plugins.
*/
/* eslint-disable @typescript-eslint/no-require-imports */

const fs = require('fs')
const path = require('path')
const Ajv = require('ajv')
const standaloneCode = require('ajv/dist/standalone').default
const { fastFormats, fullFormats } = require('ajv-formats/dist/formats')
const CONFIG_SCHEMA = require('../src/config.schema.json')

const ajv = new Ajv({
useDefaults: true,
code: {
source: true
}
})
// addFormats(ajv) does not work properly when generating stand-alone code
// (https://github.com/ajv-validator/ajv-formats/issues/68) so adding formats one-by-one
ajv.addFormat('uri', fastFormats['uri'])
ajv.addFormat('ipv4', fullFormats['ipv4'])
ajv.addFormat('ethereum-address', /^0x[a-zA-Z0-9]{40}$/)
ajv.addFormat('ethereum-private-key', /^(0x)?[a-zA-Z0-9]{64}$/)

const validate = ajv.compile(CONFIG_SCHEMA)
const moduleCode = standaloneCode(ajv, validate)
fs.writeFileSync(path.join(__dirname, "../src/generated/validateConfig.js"), moduleCode)
6 changes: 3 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
}
},
"scripts": {
"prebuild": "bash vendor-hack.sh",
"prebuild": "node bin/generate-config-validator.js && bash vendor-hack.sh",
"postbuild": "bash copy-package.sh && bash fix-esm.sh",
"build": "tsc --build ./tsconfig.node.json",
"build-production": "npm run clean; NODE_ENV=production npm run build && npm run build-browser-production",
Expand Down Expand Up @@ -53,6 +53,8 @@
"@types/secp256k1": "^4.0.3",
"@types/split2": "^4.2.0",
"@types/uuid": "^9.0.2",
"ajv": "^8.8.2",
"ajv-formats": "^2.1.1",
"babel-loader": "^9.1.2",
"babel-plugin-add-module-exports": "^1.0.4",
"babel-plugin-lodash": "^3.3.4",
Expand Down Expand Up @@ -100,8 +102,6 @@
"@streamr/network-node": "8.5.4",
"@streamr/protocol": "8.5.4",
"@streamr/utils": "8.5.4",
"ajv": "^8.8.2",
"ajv-formats": "^2.1.1",
"core-js": "^3.31.0",
"env-paths": "^2.2.1",
"eventemitter3": "^5.0.0",
Expand Down
17 changes: 4 additions & 13 deletions packages/client/src/Config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import 'reflect-metadata'
import type { Overrides } from '@ethersproject/contracts'
import cloneDeep from 'lodash/cloneDeep'
import Ajv, { ErrorObject } from 'ajv'
import addFormats from 'ajv-formats'
import type { ExternalProvider } from '@ethersproject/providers'
import { MarkOptional, DeepRequired } from 'ts-essentials'

import CONFIG_SCHEMA from './config.schema.json'
import { TrackerRegistryRecord } from '@streamr/protocol'
import { LogLevel } from '@streamr/utils'

import { IceServer, Location, WebRtcPortRange, ExternalIP } from '@streamr/network-node'
import type { ConnectionInfo } from '@ethersproject/web'
import { generateClientId } from './utils/utils'
import validate from './generated/validateConfig'

export interface ProviderAuthConfig {
ethereum: ExternalProvider
Expand Down Expand Up @@ -377,23 +375,16 @@ export const createStrictConfig = (input: StreamrClientConfig = {}): StrictStrea
}

export const validateConfig = (data: unknown): StrictStreamrClientConfig | never => {
const ajv = new Ajv({
useDefaults: true
})
addFormats(ajv)
ajv.addFormat('ethereum-address', /^0x[a-zA-Z0-9]{40}$/)
ajv.addFormat('ethereum-private-key', /^(0x)?[a-zA-Z0-9]{64}$/)
const validate = ajv.compile<StrictStreamrClientConfig>(CONFIG_SCHEMA)
if (!validate(data)) {
throw new Error(validate.errors!.map((e: ErrorObject) => {
let text = ajv.errorsText([e], { dataVar: '' }).trim()
throw new Error((validate as any).errors!.map((e: any) => {
let text = e.instancePath + " " + e.message
if (e.params.additionalProperty) {
text += `: ${e.params.additionalProperty}`
}
return text
}).join('\n'))
}
return data
return data as any
}

export const redactConfig = (config: StrictStreamrClientConfig): void => {
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/generated/validateConfig.js

Large diffs are not rendered by default.

0 comments on commit 7c07067

Please sign in to comment.