diff --git a/docs/build/miscellaneous.mdx b/docs/build/miscellaneous.mdx index b5a8f77..f68c0c8 100644 --- a/docs/build/miscellaneous.mdx +++ b/docs/build/miscellaneous.mdx @@ -1,6 +1,54 @@ # Miscellaneous +## Ajv JSON Schema Validator +[Ajv](https://ajv.js.org/), one of the best JSON schema validators, is +extremely useful in the development of backend APIs for data validation. But Ajv +is not supported by many edge environments, including Vercel, because of it's +usage of `eval`. However, the SherpaJS compiler will pre-compile standalone Ajv +schema, so schema validation can be done on the edge. + +
+ +### Example Static Schema +Start by creating a `schema.json` file containing the desired schema. +```json title="src/foo.schema.json" +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + } + }, + "required": ["foo"], + "additionalProperties": false +} +``` + +Then import your schema (in any script), and write the schema in the `AJV` +function, provided by Sherpa Core. +```typescript title="routes/index.ts" +import { Request, Response, AJV } from "sherpa-core"; +import schemaFoo from "../../src/foo.schema.json"; +const validatorFoo = AJV(schemaFoo); + +export function POST(request:Request) { + try { + if (!validatorFoo(request.body)) { + return Response.text(JSON.stringify(validatorFoo.errors)); + } + return Response.text("OK"); + } catch (e) { + console.log(e); + return Response.text(e.toString()); + } +} +``` + + +
+ + ## Environment Variables Environment variables are a key part of configuring your SherpaJS application. They allow you to set various configuration options and secrets without diff --git a/package-lock.json b/package-lock.json index aad2e4e..294af46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "license": "ISC", "dependencies": { + "@offen/esbuild-plugin-jsonschema": "^1.1.0", + "ajv": "^8.13.0", "checksum": "^1.0.0", "chokidar": "^3.6.0", "colorette": "^2.0.20", @@ -37,6 +39,22 @@ "node": ">=20" } }, + "../esbuild-plugin-jsonschema": { + "name": "@offen/esbuild-plugin-jsonschema", + "version": "1.1.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.5", + "ajv-formats": "^2.0.2" + }, + "devDependencies": { + "@studio/changes": "^2.0.1", + "esbuild": "^0.12.5", + "standard": "^16.0.3", + "tape": "^5.1.1" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1604,6 +1622,15 @@ "node": ">= 8" } }, + "node_modules/@offen/esbuild-plugin-jsonschema": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@offen/esbuild-plugin-jsonschema/-/esbuild-plugin-jsonschema-1.1.0.tgz", + "integrity": "sha512-C7uv58cVINPCcbegGJxVVPRTxt+Cg5OUZxw4tEjZFIXKqWGi1oRfesKn2/TCSnr/LI/6RLhskLutAMtETr/aaw==", + "dependencies": { + "ajv": "^8.0.5", + "ajv-formats": "^2.0.2" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2060,6 +2087,37 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3040,8 +3098,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -4184,6 +4241,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4729,7 +4791,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4796,6 +4857,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5340,7 +5409,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 9b54929..aea2b1c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ }, "license": "ISC", "dependencies": { + "@offen/esbuild-plugin-jsonschema": "^1.1.0", + "ajv": "^8.13.0", "checksum": "^1.0.0", "chokidar": "^3.6.0", "colorette": "^2.0.20", diff --git a/src/compiler/utilities/tooling/index.ts b/src/compiler/utilities/tooling/index.ts index da89525..044bbde 100644 --- a/src/compiler/utilities/tooling/index.ts +++ b/src/compiler/utilities/tooling/index.ts @@ -20,6 +20,7 @@ import { typeValidation } from "./type-validation/index.js"; import { getEnvironmentVariables } from "./dot-env/index.js"; import { ExportLoaderModule, getExportedLoader } from "./exported-loader/index.js"; import { ExportedVariable, getExportedVariables } from "./exported-variables/index.js"; +import jsonschemaPlugin from "@offen/esbuild-plugin-jsonschema"; export type { ExportLoaderModule, ExportedVariable }; @@ -65,6 +66,7 @@ export class Tooling { ...DEFAULT_ESBUILD_TARGET, format: "cjs", entryPoints: [filepath], + plugins: [jsonschemaPlugin()], write: false }); @@ -80,6 +82,7 @@ export class Tooling { ...DEFAULT_ESBUILD_TARGET, ...props.options?.developer?.bundler?.esbuild, ...props.esbuild, + plugins: [jsonschemaPlugin()], stdin: { contents: props.buffer, resolveDir: props.resolve ? path.resolve(props.resolve) : undefined, diff --git a/src/native/ajv/index.ts b/src/native/ajv/index.ts new file mode 100644 index 0000000..957770d --- /dev/null +++ b/src/native/ajv/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Sellers Industries, Inc. + * distributed under the MIT License + * + * author: Evan Sellers + * date: Wed May 22 2024 + * file: index.ts + * project: SherpaJS - Module Microservice Platform + * purpose: AJV Wrapper + * + */ + + +import { ValidateFunction, Schema, JSONSchemaType } from "ajv"; + + +export function AJV(schema:Schema|JSONSchemaType):ValidateFunction { + return schema as ValidateFunction; +} + + +// The grace of the Lord Jesus Christ be with your spirit. Amen. +// - Philippians 4:23 diff --git a/src/native/index.ts b/src/native/index.ts index 5dec1c2..b3b60d3 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -11,6 +11,7 @@ */ +import { AJV } from "./ajv/index.js"; import { Parameters } from "./parameters/index.js"; import { Headers } from "./headers/index.js"; import { Body, BodyType } from "./model.js"; @@ -20,6 +21,7 @@ import { CreateModuleInterface, Method, ModuleInterface } from "../compiler/mode export { + AJV, Headers, Parameters, Method, diff --git a/tests/endpoints/server/routes/ajv/index.ts b/tests/endpoints/server/routes/ajv/index.ts new file mode 100644 index 0000000..d0448ad --- /dev/null +++ b/tests/endpoints/server/routes/ajv/index.ts @@ -0,0 +1,17 @@ +import { Request, Response, AJV } from "../../../../../index.js"; +import schemaFoo from "../../src/foo.schema.json"; +const validatorFoo = AJV(schemaFoo); + + +export function POST(request:Request) { + try { + if (!validatorFoo(request.body)) { + return Response.text(JSON.stringify(validatorFoo.errors)); + } + return Response.text("OK"); + } catch (e) { + console.log(e); + return Response.text(e.toString()); + } +} + diff --git a/tests/endpoints/server/src/foo.schema.json b/tests/endpoints/server/src/foo.schema.json new file mode 100644 index 0000000..6aa7683 --- /dev/null +++ b/tests/endpoints/server/src/foo.schema.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + } + }, + "required": ["foo"], + "additionalProperties": false +} \ No newline at end of file