From ec96c87c5994b4e8af96a61d4634a39da1786c0a Mon Sep 17 00:00:00 2001 From: Rehan van der Merwe Date: Sat, 15 Jan 2022 10:32:30 +0000 Subject: [PATCH] Updated standalone documentation and add new examples (#1866) * docs: updated standalone documentation and added new examples #1831 * remove unnecessary line breaks * remove style changes * minor corrections * minor corrections * Update docs/standalone.md * Update docs/standalone.md * Update docs/standalone.md Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- docs/standalone.md | 223 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 190 insertions(+), 33 deletions(-) diff --git a/docs/standalone.md b/docs/standalone.md index 70373fd18..5b32b73e7 100644 --- a/docs/standalone.md +++ b/docs/standalone.md @@ -2,83 +2,241 @@ [[toc]] -Ajv supports generating standalone modules with exported validation function(s), with one default export or multiple named exports, that are pre-compiled and can be used without Ajv. It is useful for several reasons: +Ajv supports generating standalone validation functions from JSON Schemas at compile/build time. These functions can then be used during runtime to do validation without initialising Ajv. It is useful for several reasons: - to reduce the browser bundle size - Ajv is not included in the bundle (although if you have a large number of schemas the bundle can become bigger - when the total size of generated validation code is bigger than Ajv code). - to reduce the start-up time - the validation and compilation of schemas will happen during build time. - to avoid dynamic code evaluation with Function constructor (used for schema compilation) - when it is prohibited by the browser page [Content Security Policy](./security.md#content-security-policy). -This functionality in Ajv v7 supersedes deprecated package ajv-pack that can be used with Ajv v6. All schemas, including those with recursive references, formats and user-defined keywords are supported, with few [limitations](#configuration-and-limitations). +This functionality in Ajv v7 supersedes the deprecated package ajv-pack that can be used with Ajv v6. All schemas, including those with recursive references, formats and user-defined keywords are supported, with few [limitations](#configuration-and-limitations). -## Usage with CLI +## Two-step process -In most cases you would use this functionality via [ajv-cli](https://github.com/ajv-validator/ajv-cli) (>= 4.0.0) to generate module that exports validation function. +The **first step** is to **generate** the standalone validation function code. This is done at compile/build time of your project and the output is a generated JS file. The **second step** is to **use** the generated JS validation function. +There are two methods to generate the code, using either the Ajv CLI or the Ajv JS library. There are also a few different options that can be passed when generating code. Below is just a highlight of a few options: + +- Set the `code.source` (JS) value to true or use the `compile` (CLI) command to generate standalone code. +- The standalone code can be generated in either ES5 or ES6, it defaults to ES6. Set the `code.es5` (JS) value to true or pass the `--code-es5` (CLI) flag to true if you want ES5 code. +- The standalone code can be generated in either CJS (module.export & require) or ESM (exports & import), it defaults to CJS. Set the `code.esm` (JS) value to true or pass the `--code-esm` (CLI) flag if you want ESM exported code. + +Note that the way the function is exported, differs if you are exporting a single or multiple schemas. See examples below. + +### Generating function(s) using CLI + +In most cases you would use this functionality via [ajv-cli](https://github.com/ajv-validator/ajv-cli) (>= 4.0.0) to generate the standalone code that exports the validation function. See [ajv-cli](https://github.com/ajv-validator/ajv-cli#compile-schemas) docs and the [cli options](https://github.com/ajv-validator/ajv-cli#ajv-options) for additional information. + +#### Using the defaults - ES6 and CJS exports ```sh npm install -g ajv-cli ajv compile -s schema.json -o validate_schema.js ``` -`validate_schema.js` will contain the module exporting validation function that can be bundled into your application. - -See [ajv-cli](https://github.com/ajv-validator/ajv-cli) docs for additional information. - -## Usage from code +### Generating using the JS library +Install the package, version >= v7.0.0: ```sh npm install ajv ``` +#### Generating functions(s) for a single schema using the JS library - ES6 and CJS exports + ```javascript -const Ajv = require("ajv") // version >= v7.0.0 -const ajv = new Ajv({code: {source: true}}) // this option is required to generate standalone code +const fs = require("fs") +const path = require("path") +const Ajv = require("ajv") const standaloneCode = require("ajv/dist/standalone").default const schema = { - $id: "https://example.com/object.json", + $id: "https://example.com/bar.json", + $schema: "http://json-schema.org/draft-07/schema#", type: "object", properties: { - foo: { - type: "string", - pattern: "^[a-z]+$", - }, + bar: {type: "string"}, }, + "required": ["bar"] } -// 1. generate module with a single default export (CommonJS and ESM compatible): +// The generated code will have a default export: +// `module.exports = ;module.exports.default = ;` +const ajv = new Ajv({code: {source: true}}) const validate = ajv.compile(schema) let moduleCode = standaloneCode(ajv, validate) -// 2. pass map of schema IDs to generate multiple exports, -// it avoids code duplication if schemas are mutually recursive or have some share elements: -let moduleCode = standaloneCode(ajv, { - validateObject: "https://example.com/object.json", -}) +// Now you can write the module code to file +fs.writeFileSync(path.join(__dirname, "../consume/validate-cjs.js"), moduleCode) +``` + +#### Generating functions(s) for multiple schemas using the JS library - ES6 and CJS exports + +```javascript +const fs = require("fs") +const path = require("path") +const Ajv = require("ajv") +const standaloneCode = require("ajv/dist/standalone").default -// 3. or generate module with all schemas added to the instance (excluding meta-schemas), -// export names would use schema IDs (or keys passed to addSchema method): +const schemaFoo = { + $id: "#/definitions/Foo", + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + foo: {"$ref": "#/definitions/Bar"} + } +} +const schemaBar = { + $id: "#/definitions/Bar", + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + bar: {type: "string"}, + }, + "required": ["bar"] +} + +// For CJS, it generates an exports array, will generate +// `exports["#/definitions/Foo"] = ...;exports["#/definitions/Bar"] = ... ;` +const ajv = new Ajv({schemas: [schemaFoo, schemaBar], code: {source: true}}) let moduleCode = standaloneCode(ajv) -// now you can -// write module code to file +// Now you can write the module code to file +fs.writeFileSync(path.join(__dirname, "../consume/validate-cjs.js"), moduleCode) +``` + +#### Generating functions(s) for multiple schemas using the JS library - ES6 and ESM exports + +```javascript const fs = require("fs") const path = require("path") -fs.writeFileSync(path.join(__dirname, "/validate.js"), moduleCode) +const Ajv = require("ajv") +const standaloneCode = require("ajv/dist/standalone").default + +const schemaFoo = { + $id: "#/definitions/Foo", + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + foo: {"$ref": "#/definitions/Bar"} + } +} +const schemaBar = { + $id: "#/definitions/Bar", + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + bar: {type: "string"}, + }, + "required": ["bar"] +} -// ... or require module from string -const requireFromString = require("require-from-string") -const standaloneValidate = requireFromString(moduleCode) // for a single default export +// For ESM, the export name needs to be a valid export name, it can not be `export const #/definitions/Foo = ...;` so we +// need to provide a mapping between a valid name and the $id field. Below will generate +// `export const Foo = ...;export const Bar = ...;` +// This mapping would not have been needed if the `$ids` was just `Bar` and `Foo` instead of `#/definitions/Foo` +// and `#/definitions/Bar` respectfully +const ajv = new Ajv({schemas: [schemaFoo, schemaBar], code: {source: true, esm: true}}) +let moduleCode = standaloneCode(ajv, { + "Foo": "#/definitions/Foo", + "Bar": "#/definitions/Bar" +}) + +// Now you can write the module code to file +fs.writeFileSync(path.join(__dirname, "../consume/validate-esm.mjs"), moduleCode) +``` + +::: warning ESM name mapping +The ESM version only requires the mapping if the ids are not valid export names. If the $ids were just the +`Foo` and `Bar` instead of `#/definitions/Foo` and `#/definitions/Bar` then the mapping would not be needed. +::: + + +## Using the validation function(s) + +### Validating a single schemas using the JS library - ES6 and CJS + +```javascript +const Bar = require('./validate-cjs') + +const barPass = { + bar: "something" +} + +const barFail = { + // bar: "something" // <= empty/omitted property that is required +} + +let validateBar = Bar +if (!validateBar(barPass)) + console.log("ERRORS 1:", validateBar.errors) //Never reaches this because valid + +if (!validateBar(barFail)) + console.log("ERRORS 2:", validateBar.errors) //Errors array gets logged +``` + +### Validating multiple schemas using the JS library - ES6 and CJS + +```javascript +const validations = require('./validate-cjs') + +const fooPass = { + foo: { + bar: "something" + } +} + +const fooFail = { + foo: { + // bar: "something" // <= empty/omitted property that is required + } +} + +let validateFoo = validations["#/definitions/Foo"]; +if (!validateFoo(fooPass)) + console.log("ERRORS 1:", validateFoo.errors); //Never reaches this because valid + +if (!validateFoo(fooFail)) + console.log("ERRORS 2:", validateFoo.errors); //Errors array gets logged + +``` + +### Validating multiple schemas using the JS library - ES6 and ESM + +```javascript +import {Foo, Bar} from './validate-multiple-esm.mjs'; + +const fooPass = { + foo: { + bar: "something" + } +} + +const fooFail = { + foo: { + // bar: "something" // bar: "something" <= empty properties + } +} + +let validateFoo = Foo; +if (!validateFoo(fooPass)) + console.log("ERRORS 1:", validateFoo.errors); //Never reaches here because valid + +if (!validateFoo(fooFail)) + console.log("ERRORS 2:", validateFoo.errors); //Errors array gets logged ``` + ### Requirement at runtime -To run the standalone generated functions, the Ajv package should still be a run-time dependency for most schemas, but generated modules can only depend on code in [runtime](https://github.com/ajv-validator/ajv/tree/master/lib/runtime) folder, so the whole Ajv will not be included in the bundle (or executed) if you require the modules with standalone validation code from your application code. +One of the main reason for using the standalone mode is to start applications faster to avoid runtime schema compilation. + +The standalone generated functions still has a dependency on the Ajv. Specifically on the code in the [runtime](https://github.com/ajv-validator/ajv/tree/master/lib/runtime) folder of the package. + +Completely isolated validation functions can be generated if desired (won't be for most use cases). Run the generated code through a bundler like ES Build to create completely isolated validation functions that can be imported/required without any dependency on Ajv. This is also not needed if your project is already using a bundler. ## Configuration and limitations To support standalone code generation: -- Ajv option `source.code` must be set to `true` +- Ajv option `code.source` must be set to `true` - only `code` and `macro` user-defined keywords are supported (see [User defined keywords](./keywords.md)). - when `code` keywords define variables in shared scope using `gen.scopeValue`, they must provide `code` property with the code snippet. See source code of pre-defined keywords for examples in [vocabularies folder](https://github.com/ajv-validator/ajv/blob/master/lib/vocabularies). - if formats are used in standalone code, ajv option `code.formats` should contain the code snippet that will evaluate to an object with all used format definitions - it can be a call to `require("...")` with the correct path (relative to the location of saved module): @@ -93,6 +251,5 @@ const ajv = new Ajv({ formats: _`require("./my-formats")`, }, }) -``` If you only use formats from [ajv-formats](https://github.com/ajv-validator/ajv-formats) this option will be set by this package automatically.