From 86a94b5523e4750fe188f8214d0050fcc4862041 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:57:44 +0200 Subject: [PATCH] feat: Add fork of json-schema-to-zod (no-changelog) (#11228) --- packages/@n8n/json-schema-to-zod/.eslintrc.js | 21 + packages/@n8n/json-schema-to-zod/.gitignore | 4 + packages/@n8n/json-schema-to-zod/.npmignore | 3 + packages/@n8n/json-schema-to-zod/LICENSE | 16 + packages/@n8n/json-schema-to-zod/README.md | 34 + .../@n8n/json-schema-to-zod/jest.config.js | 5 + packages/@n8n/json-schema-to-zod/package.json | 69 ++ packages/@n8n/json-schema-to-zod/postcjs.js | 1 + packages/@n8n/json-schema-to-zod/postesm.js | 1 + packages/@n8n/json-schema-to-zod/src/index.ts | 2 + .../src/json-schema-to-zod.ts | 15 + .../src/parsers/parse-all-of.ts | 46 + .../src/parsers/parse-any-of.ts | 19 + .../src/parsers/parse-array.ts | 34 + .../src/parsers/parse-boolean.ts | 7 + .../src/parsers/parse-const.ts | 7 + .../src/parsers/parse-default.ts | 7 + .../src/parsers/parse-enum.ts | 25 + .../src/parsers/parse-if-then-else.ts | 31 + .../src/parsers/parse-multiple-type.ts | 16 + .../src/parsers/parse-not.ts | 15 + .../src/parsers/parse-null.ts | 7 + .../src/parsers/parse-nullable.ts | 10 + .../src/parsers/parse-number.ts | 88 ++ .../src/parsers/parse-object.ts | 219 +++++ .../src/parsers/parse-one-of.ts | 41 + .../src/parsers/parse-schema.ts | 130 +++ .../src/parsers/parse-string.ts | 58 ++ packages/@n8n/json-schema-to-zod/src/types.ts | 82 ++ .../src/utils/extend-schema.ts | 23 + .../@n8n/json-schema-to-zod/src/utils/half.ts | 3 + .../@n8n/json-schema-to-zod/src/utils/its.ts | 57 ++ .../@n8n/json-schema-to-zod/src/utils/omit.ts | 8 + .../@n8n/json-schema-to-zod/test/all.json | 143 +++ .../json-schema-to-zod/test/extend-expect.ts | 16 + .../@n8n/json-schema-to-zod/test/jest.d.ts | 5 + .../test/json-schema-to-zod.test.ts | 106 ++ .../test/parsers/parse-all-of.test.ts | 48 + .../test/parsers/parse-any-of.test.ts | 31 + .../test/parsers/parse-array.test.ts | 68 ++ .../test/parsers/parse-const.test.ts | 13 + .../test/parsers/parse-enum.test.ts | 36 + .../test/parsers/parse-not.test.ts | 25 + .../test/parsers/parse-nullable.test.ts | 18 + .../test/parsers/parse-number.test.ts | 83 ++ .../test/parsers/parse-object.test.ts | 904 ++++++++++++++++++ .../test/parsers/parse-one-of.test.ts | 48 + .../test/parsers/parse-schema.test.ts | 113 +++ .../test/parsers/parse-string.test.ts | 152 +++ .../test/utils/half.test.ts | 15 + .../test/utils/omit.test.ts | 27 + .../@n8n/json-schema-to-zod/tsconfig.cjs.json | 11 + .../@n8n/json-schema-to-zod/tsconfig.esm.json | 12 + .../@n8n/json-schema-to-zod/tsconfig.json | 12 + .../json-schema-to-zod/tsconfig.types.json | 11 + pnpm-lock.yaml | 12 + 56 files changed, 3013 insertions(+) create mode 100644 packages/@n8n/json-schema-to-zod/.eslintrc.js create mode 100644 packages/@n8n/json-schema-to-zod/.gitignore create mode 100644 packages/@n8n/json-schema-to-zod/.npmignore create mode 100644 packages/@n8n/json-schema-to-zod/LICENSE create mode 100644 packages/@n8n/json-schema-to-zod/README.md create mode 100644 packages/@n8n/json-schema-to-zod/jest.config.js create mode 100644 packages/@n8n/json-schema-to-zod/package.json create mode 100644 packages/@n8n/json-schema-to-zod/postcjs.js create mode 100644 packages/@n8n/json-schema-to-zod/postesm.js create mode 100644 packages/@n8n/json-schema-to-zod/src/index.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-boolean.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-const.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-default.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-if-then-else.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-multiple-type.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-null.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-nullable.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/types.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/utils/half.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/utils/its.ts create mode 100644 packages/@n8n/json-schema-to-zod/src/utils/omit.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/all.json create mode 100644 packages/@n8n/json-schema-to-zod/test/extend-expect.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/jest.d.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-all-of.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-any-of.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-array.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-const.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-enum.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-not.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-nullable.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-number.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-object.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-one-of.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-schema.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/parsers/parse-string.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/utils/half.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts create mode 100644 packages/@n8n/json-schema-to-zod/tsconfig.cjs.json create mode 100644 packages/@n8n/json-schema-to-zod/tsconfig.esm.json create mode 100644 packages/@n8n/json-schema-to-zod/tsconfig.json create mode 100644 packages/@n8n/json-schema-to-zod/tsconfig.types.json diff --git a/packages/@n8n/json-schema-to-zod/.eslintrc.js b/packages/@n8n/json-schema-to-zod/.eslintrc.js new file mode 100644 index 0000000000..03caaf4930 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/.eslintrc.js @@ -0,0 +1,21 @@ +const sharedOptions = require('@n8n_io/eslint-config/shared'); + +/** + * @type {import('@types/eslint').ESLint.ConfigData} + */ +module.exports = { + extends: ['@n8n_io/eslint-config/node'], + + ...sharedOptions(__dirname), + + ignorePatterns: ['jest.config.js'], + + rules: { + 'unicorn/filename-case': ['error', { case: 'kebabCase' }], + '@typescript-eslint/no-duplicate-imports': 'off', + 'import/no-cycle': 'off', + 'n8n-local-rules/no-plain-errors': 'off', + + complexity: 'error', + }, +}; diff --git a/packages/@n8n/json-schema-to-zod/.gitignore b/packages/@n8n/json-schema-to-zod/.gitignore new file mode 100644 index 0000000000..d11ff827d5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +coverage +test/output diff --git a/packages/@n8n/json-schema-to-zod/.npmignore b/packages/@n8n/json-schema-to-zod/.npmignore new file mode 100644 index 0000000000..3aeebeb66b --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/.npmignore @@ -0,0 +1,3 @@ +src +tsconfig* +test diff --git a/packages/@n8n/json-schema-to-zod/LICENSE b/packages/@n8n/json-schema-to-zod/LICENSE new file mode 100644 index 0000000000..aa24f46da6 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2024, n8n +Copyright (c) 2021, Stefan Terdell + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/@n8n/json-schema-to-zod/README.md b/packages/@n8n/json-schema-to-zod/README.md new file mode 100644 index 0000000000..cb76a141b5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/README.md @@ -0,0 +1,34 @@ +# Json-Schema-to-Zod + +A package to convert JSON schema (draft 4+) objects into Zod schemas in the form of Zod objects at runtime. + +## Installation + +```sh +npm install @n8n/json-schema-to-zod +``` + +### Simple example + +```typescript +import { jsonSchemaToZod } from "json-schema-to-zod"; + +const jsonSchema = { + type: "object", + properties: { + hello: { + type: "string", + }, + }, +}; + +const zodSchema = jsonSchemaToZod(myObject); +``` + +### Overriding a parser + +You can pass a function to the `overrideParser` option, which represents a function that receives the current schema node and the reference object, and should return a zod object when it wants to replace a default output. If the default output should be used for the node just return undefined. + +## Acknowledgements + +This is a fork of [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod). diff --git a/packages/@n8n/json-schema-to-zod/jest.config.js b/packages/@n8n/json-schema-to-zod/jest.config.js new file mode 100644 index 0000000000..b8e98e8970 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('jest').Config} */ +module.exports = { + ...require('../../../jest.config'), + setupFilesAfterEnv: ['/test/extend-expect.ts'], +}; diff --git a/packages/@n8n/json-schema-to-zod/package.json b/packages/@n8n/json-schema-to-zod/package.json new file mode 100644 index 0000000000..8900213381 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/package.json @@ -0,0 +1,69 @@ +{ + "name": "@n8n/json-schema-to-zod", + "version": "1.0.0", + "description": "Converts JSON schema objects into Zod schemas", + "types": "./dist/types/index.d.ts", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "exports": { + "import": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "scripts": { + "clean": "rimraf dist .turbo", + "typecheck": "tsc --noEmit", + "dev": "tsc -w", + "format": "biome format --write src", + "format:check": "biome ci src", + "lint": "eslint . --quiet", + "lintfix": "eslint . --fix", + "build:types": "tsc -p tsconfig.types.json", + "build:cjs": "tsc -p tsconfig.cjs.json && node postcjs.js", + "build:esm": "tsc -p tsconfig.esm.json && node postesm.js", + "build": "rimraf ./dist && pnpm run build:types && pnpm run build:cjs && pnpm run build:esm", + "dry": "pnpm run build && pnpm pub --dry-run", + "test": "jest", + "test:watch": "jest --watch" + }, + "keywords": [ + "zod", + "json", + "schema", + "converter", + "cli" + ], + "author": "Stefan Terdell", + "contributors": [ + "Chen (https://github.com/werifu)", + "Nuno Carduso (https://github.com/ncardoso-barracuda)", + "Lars Strojny (https://github.com/lstrojny)", + "Navtoj Chahal (https://github.com/navtoj)", + "Ben McCann (https://github.com/benmccann)", + "Dmitry Zakharov (https://github.com/DZakh)", + "Michel Turpin (https://github.com/grimly)", + "David Barratt (https://github.com/davidbarratt)", + "pevisscher (https://github.com/pevisscher)", + "Aidin Abedi (https://github.com/aidinabedi)", + "Brett Zamir (https://github.com/brettz9)", + "n8n (https://github.com/n8n-io)" + ], + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/n8n-io/n8n" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "devDependencies": { + "@types/json-schema": "^7.0.15", + "@types/node": "^20.9.0", + "zod": "catalog:" + } +} diff --git a/packages/@n8n/json-schema-to-zod/postcjs.js b/packages/@n8n/json-schema-to-zod/postcjs.js new file mode 100644 index 0000000000..618aa03a96 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/postcjs.js @@ -0,0 +1 @@ +require('fs').writeFileSync('./dist/cjs/package.json', '{"type":"commonjs"}', 'utf-8'); diff --git a/packages/@n8n/json-schema-to-zod/postesm.js b/packages/@n8n/json-schema-to-zod/postesm.js new file mode 100644 index 0000000000..5235734d6c --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/postesm.js @@ -0,0 +1 @@ +require('fs').writeFileSync('./dist/esm/package.json', '{"type":"module"}', 'utf-8'); diff --git a/packages/@n8n/json-schema-to-zod/src/index.ts b/packages/@n8n/json-schema-to-zod/src/index.ts new file mode 100644 index 0000000000..10dae97784 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/index.ts @@ -0,0 +1,2 @@ +export type * from './types'; +export { jsonSchemaToZod } from './json-schema-to-zod.js'; diff --git a/packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts b/packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts new file mode 100644 index 0000000000..6f1c6a1315 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts @@ -0,0 +1,15 @@ +import type { z } from 'zod'; + +import { parseSchema } from './parsers/parse-schema'; +import type { JsonSchemaToZodOptions, JsonSchema } from './types'; + +export const jsonSchemaToZod = ( + schema: JsonSchema, + options: JsonSchemaToZodOptions = {}, +): T => { + return parseSchema(schema, { + path: [], + seen: new Map(), + ...options, + }) as T; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts new file mode 100644 index 0000000000..be8fd2c7e5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, JsonSchema, Refs } from '../types'; +import { half } from '../utils/half'; + +const originalIndex = Symbol('Original index'); + +const ensureOriginalIndex = (arr: JsonSchema[]) => { + const newArr = []; + + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; + if (typeof item === 'boolean') { + newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} }); + } else if (originalIndex in item) { + return arr; + } else { + newArr.push({ ...item, [originalIndex]: i }); + } + } + + return newArr; +}; + +export function parseAllOf( + jsonSchema: JsonSchemaObject & { allOf: JsonSchema[] }, + refs: Refs, +): z.ZodTypeAny { + if (jsonSchema.allOf.length === 0) { + return z.never(); + } + + if (jsonSchema.allOf.length === 1) { + const item = jsonSchema.allOf[0]; + + return parseSchema(item, { + ...refs, + path: [...refs.path, 'allOf', (item as never)[originalIndex]], + }); + } + + const [left, right] = half(ensureOriginalIndex(jsonSchema.allOf)); + + return z.intersection(parseAllOf({ allOf: left }, refs), parseAllOf({ allOf: right }, refs)); +} diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts new file mode 100644 index 0000000000..73b19b1739 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, JsonSchema, Refs } from '../types'; + +export const parseAnyOf = (jsonSchema: JsonSchemaObject & { anyOf: JsonSchema[] }, refs: Refs) => { + return jsonSchema.anyOf.length + ? jsonSchema.anyOf.length === 1 + ? parseSchema(jsonSchema.anyOf[0], { + ...refs, + path: [...refs.path, 'anyOf', 0], + }) + : z.union( + jsonSchema.anyOf.map((schema, i) => + parseSchema(schema, { ...refs, path: [...refs.path, 'anyOf', i] }), + ) as [z.ZodTypeAny, z.ZodTypeAny], + ) + : z.any(); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts new file mode 100644 index 0000000000..5e01473fd6 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, Refs } from '../types'; +import { extendSchemaWithMessage } from '../utils/extend-schema'; + +export const parseArray = (jsonSchema: JsonSchemaObject & { type: 'array' }, refs: Refs) => { + if (Array.isArray(jsonSchema.items)) { + return z.tuple( + jsonSchema.items.map((v, i) => + parseSchema(v, { ...refs, path: [...refs.path, 'items', i] }), + ) as [z.ZodTypeAny], + ); + } + + let zodSchema = !jsonSchema.items + ? z.array(z.any()) + : z.array(parseSchema(jsonSchema.items, { ...refs, path: [...refs.path, 'items'] })); + + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'minItems', + (zs, minItems, errorMessage) => zs.min(minItems, errorMessage), + ); + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'maxItems', + (zs, maxItems, errorMessage) => zs.max(maxItems, errorMessage), + ); + + return zodSchema; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-boolean.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-boolean.ts new file mode 100644 index 0000000000..be8e309e43 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-boolean.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; + +export const parseBoolean = (_jsonSchema: JsonSchemaObject & { type: 'boolean' }) => { + return z.boolean(); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-const.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-const.ts new file mode 100644 index 0000000000..445523652d --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-const.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject, Serializable } from '../types'; + +export const parseConst = (jsonSchema: JsonSchemaObject & { const: Serializable }) => { + return z.literal(jsonSchema.const as z.Primitive); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-default.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-default.ts new file mode 100644 index 0000000000..d64bcf85c8 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-default.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; + +export const parseDefault = (_jsonSchema: JsonSchemaObject) => { + return z.any(); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts new file mode 100644 index 0000000000..26385472cc --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject, Serializable } from '../types'; + +export const parseEnum = (jsonSchema: JsonSchemaObject & { enum: Serializable[] }) => { + if (jsonSchema.enum.length === 0) { + return z.never(); + } + + if (jsonSchema.enum.length === 1) { + // union does not work when there is only one element + return z.literal(jsonSchema.enum[0] as z.Primitive); + } + + if (jsonSchema.enum.every((x) => typeof x === 'string')) { + return z.enum(jsonSchema.enum as [string]); + } + + return z.union( + jsonSchema.enum.map((x) => z.literal(x as z.Primitive)) as unknown as [ + z.ZodTypeAny, + z.ZodTypeAny, + ], + ); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-if-then-else.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-if-then-else.ts new file mode 100644 index 0000000000..7cb595a615 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-if-then-else.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, JsonSchema, Refs } from '../types'; + +export const parseIfThenElse = ( + jsonSchema: JsonSchemaObject & { + if: JsonSchema; + then: JsonSchema; + else: JsonSchema; + }, + refs: Refs, +) => { + const $if = parseSchema(jsonSchema.if, { ...refs, path: [...refs.path, 'if'] }); + const $then = parseSchema(jsonSchema.then, { + ...refs, + path: [...refs.path, 'then'], + }); + const $else = parseSchema(jsonSchema.else, { + ...refs, + path: [...refs.path, 'else'], + }); + + return z.union([$then, $else]).superRefine((value, ctx) => { + const result = $if.safeParse(value).success ? $then.safeParse(value) : $else.safeParse(value); + + if (!result.success) { + result.error.errors.forEach((error) => ctx.addIssue(error)); + } + }); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-multiple-type.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-multiple-type.ts new file mode 100644 index 0000000000..65ff3c35b5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-multiple-type.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchema, JsonSchemaObject, Refs } from '../types'; + +export const parseMultipleType = ( + jsonSchema: JsonSchemaObject & { type: string[] }, + refs: Refs, +) => { + return z.union( + jsonSchema.type.map((type) => parseSchema({ ...jsonSchema, type } as JsonSchema, refs)) as [ + z.ZodTypeAny, + z.ZodTypeAny, + ], + ); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts new file mode 100644 index 0000000000..219d32c8dd --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, JsonSchema, Refs } from '../types'; + +export const parseNot = (jsonSchema: JsonSchemaObject & { not: JsonSchema }, refs: Refs) => { + return z.any().refine( + (value) => + !parseSchema(jsonSchema.not, { + ...refs, + path: [...refs.path, 'not'], + }).safeParse(value).success, + 'Invalid input: Should NOT be valid against schema', + ); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-null.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-null.ts new file mode 100644 index 0000000000..86dbfea439 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-null.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; + +export const parseNull = (_jsonSchema: JsonSchemaObject & { type: 'null' }) => { + return z.null(); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-nullable.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-nullable.ts new file mode 100644 index 0000000000..cfc575e9c7 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-nullable.ts @@ -0,0 +1,10 @@ +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, Refs } from '../types'; +import { omit } from '../utils/omit'; + +/** + * For compatibility with open api 3.0 nullable + */ +export const parseNullable = (jsonSchema: JsonSchemaObject & { nullable: true }, refs: Refs) => { + return parseSchema(omit(jsonSchema, 'nullable'), refs, true).nullable(); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts new file mode 100644 index 0000000000..504a453faf --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts @@ -0,0 +1,88 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; +import { extendSchemaWithMessage } from '../utils/extend-schema'; + +export const parseNumber = (jsonSchema: JsonSchemaObject & { type: 'number' | 'integer' }) => { + let zodSchema = z.number(); + + let isInteger = false; + if (jsonSchema.type === 'integer') { + isInteger = true; + zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'type', (zs, _, errorMsg) => + zs.int(errorMsg), + ); + } else if (jsonSchema.format === 'int64') { + isInteger = true; + zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, _, errorMsg) => + zs.int(errorMsg), + ); + } + + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'multipleOf', + (zs, multipleOf, errorMsg) => { + if (multipleOf === 1) { + if (isInteger) return zs; + + return zs.int(errorMsg); + } + + return zs.multipleOf(multipleOf, errorMsg); + }, + ); + + if (typeof jsonSchema.minimum === 'number') { + if (jsonSchema.exclusiveMinimum === true) { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'minimum', + (zs, minimum, errorMsg) => zs.gt(minimum, errorMsg), + ); + } else { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'minimum', + (zs, minimum, errorMsg) => zs.gte(minimum, errorMsg), + ); + } + } else if (typeof jsonSchema.exclusiveMinimum === 'number') { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'exclusiveMinimum', + (zs, exclusiveMinimum, errorMsg) => zs.gt(exclusiveMinimum as number, errorMsg), + ); + } + + if (typeof jsonSchema.maximum === 'number') { + if (jsonSchema.exclusiveMaximum === true) { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'maximum', + (zs, maximum, errorMsg) => zs.lt(maximum, errorMsg), + ); + } else { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'maximum', + (zs, maximum, errorMsg) => zs.lte(maximum, errorMsg), + ); + } + } else if (typeof jsonSchema.exclusiveMaximum === 'number') { + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'exclusiveMaximum', + (zs, exclusiveMaximum, errorMsg) => zs.lt(exclusiveMaximum as number, errorMsg), + ); + } + + return zodSchema; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts new file mode 100644 index 0000000000..6a87b7162b --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts @@ -0,0 +1,219 @@ +import * as z from 'zod'; + +import { parseAllOf } from './parse-all-of'; +import { parseAnyOf } from './parse-any-of'; +import { parseOneOf } from './parse-one-of'; +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, Refs } from '../types'; +import { its } from '../utils/its'; + +function parseObjectProperties(objectSchema: JsonSchemaObject & { type: 'object' }, refs: Refs) { + if (!objectSchema.properties) { + return undefined; + } + + const propertyKeys = Object.keys(objectSchema.properties); + if (propertyKeys.length === 0) { + return z.object({}); + } + + const properties: Record = {}; + + for (const key of propertyKeys) { + const propJsonSchema = objectSchema.properties[key]; + + const propZodSchema = parseSchema(propJsonSchema, { + ...refs, + path: [...refs.path, 'properties', key], + }); + + const hasDefault = typeof propJsonSchema === 'object' && propJsonSchema.default !== undefined; + + const required = Array.isArray(objectSchema.required) + ? objectSchema.required.includes(key) + : typeof propJsonSchema === 'object' && propJsonSchema.required === true; + + const isOptional = !hasDefault && !required; + + properties[key] = isOptional ? propZodSchema.optional() : propZodSchema; + } + + return z.object(properties); +} + +export function parseObject( + objectSchema: JsonSchemaObject & { type: 'object' }, + refs: Refs, +): z.ZodTypeAny { + const hasPatternProperties = Object.keys(objectSchema.patternProperties ?? {}).length > 0; + + const propertiesSchema: + | z.ZodObject, 'strip', z.ZodTypeAny> + | undefined = parseObjectProperties(objectSchema, refs); + let zodSchema: z.ZodTypeAny | undefined = propertiesSchema; + + const additionalProperties = + objectSchema.additionalProperties !== undefined + ? parseSchema(objectSchema.additionalProperties, { + ...refs, + path: [...refs.path, 'additionalProperties'], + }) + : undefined; + + if (objectSchema.patternProperties) { + const parsedPatternProperties = Object.fromEntries( + Object.entries(objectSchema.patternProperties).map(([key, value]) => { + return [ + key, + parseSchema(value, { + ...refs, + path: [...refs.path, 'patternProperties', key], + }), + ]; + }), + ); + const patternPropertyValues = Object.values(parsedPatternProperties); + + if (propertiesSchema) { + if (additionalProperties) { + zodSchema = propertiesSchema.catchall( + z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]), + ); + } else if (Object.keys(parsedPatternProperties).length > 1) { + zodSchema = propertiesSchema.catchall( + z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny]), + ); + } else { + zodSchema = propertiesSchema.catchall(patternPropertyValues[0]); + } + } else { + if (additionalProperties) { + zodSchema = z.record( + z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]), + ); + } else if (patternPropertyValues.length > 1) { + zodSchema = z.record(z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny])); + } else { + zodSchema = z.record(patternPropertyValues[0]); + } + } + + const objectPropertyKeys = new Set(Object.keys(objectSchema.properties ?? {})); + zodSchema = zodSchema.superRefine((value: Record, ctx) => { + for (const key in value) { + let wasMatched = objectPropertyKeys.has(key); + + for (const patternPropertyKey in objectSchema.patternProperties) { + const regex = new RegExp(patternPropertyKey); + if (key.match(regex)) { + wasMatched = true; + const result = parsedPatternProperties[patternPropertyKey].safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + } + + if (!wasMatched && additionalProperties) { + const result = additionalProperties.safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: 'Invalid input: must match catchall schema', + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + } + + let output: z.ZodTypeAny; + if (propertiesSchema) { + if (hasPatternProperties) { + output = zodSchema!; + } else if (additionalProperties) { + if (additionalProperties instanceof z.ZodNever) { + output = propertiesSchema.strict(); + } else { + output = propertiesSchema.catchall(additionalProperties); + } + } else { + output = zodSchema!; + } + } else { + if (hasPatternProperties) { + output = zodSchema!; + } else if (additionalProperties) { + output = z.record(additionalProperties); + } else { + output = z.record(z.any()); + } + } + + if (its.an.anyOf(objectSchema)) { + output = output.and( + parseAnyOf( + { + ...objectSchema, + anyOf: objectSchema.anyOf.map((x) => + typeof x === 'object' && + !x.type && + (x.properties ?? x.additionalProperties ?? x.patternProperties) + ? { ...x, type: 'object' } + : x, + ), + }, + refs, + ), + ); + } + + if (its.a.oneOf(objectSchema)) { + output = output.and( + parseOneOf( + { + ...objectSchema, + oneOf: objectSchema.oneOf.map((x) => + typeof x === 'object' && + !x.type && + (x.properties ?? x.additionalProperties ?? x.patternProperties) + ? { ...x, type: 'object' } + : x, + ), + }, + refs, + ), + ); + } + + if (its.an.allOf(objectSchema)) { + output = output.and( + parseAllOf( + { + ...objectSchema, + allOf: objectSchema.allOf.map((x) => + typeof x === 'object' && + !x.type && + (x.properties ?? x.additionalProperties ?? x.patternProperties) + ? { ...x, type: 'object' } + : x, + ), + }, + refs, + ), + ); + } + + return output; +} diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts new file mode 100644 index 0000000000..10931a9675 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; + +import { parseSchema } from './parse-schema'; +import type { JsonSchemaObject, JsonSchema, Refs } from '../types'; + +export const parseOneOf = (jsonSchema: JsonSchemaObject & { oneOf: JsonSchema[] }, refs: Refs) => { + if (!jsonSchema.oneOf.length) { + return z.any(); + } + + if (jsonSchema.oneOf.length === 1) { + return parseSchema(jsonSchema.oneOf[0], { + ...refs, + path: [...refs.path, 'oneOf', 0], + }); + } + + return z.any().superRefine((x, ctx) => { + const schemas = jsonSchema.oneOf.map((schema, i) => + parseSchema(schema, { + ...refs, + path: [...refs.path, 'oneOf', i], + }), + ); + + const unionErrors = schemas.reduce( + (errors, schema) => + ((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)), + [], + ); + + if (schemas.length - unionErrors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: 'invalid_union', + unionErrors, + message: 'Invalid input: Should pass single schema', + }); + } + }); +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts new file mode 100644 index 0000000000..24818bf490 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts @@ -0,0 +1,130 @@ +import * as z from 'zod'; + +import { parseAllOf } from './parse-all-of'; +import { parseAnyOf } from './parse-any-of'; +import { parseArray } from './parse-array'; +import { parseBoolean } from './parse-boolean'; +import { parseConst } from './parse-const'; +import { parseDefault } from './parse-default'; +import { parseEnum } from './parse-enum'; +import { parseIfThenElse } from './parse-if-then-else'; +import { parseMultipleType } from './parse-multiple-type'; +import { parseNot } from './parse-not'; +import { parseNull } from './parse-null'; +import { parseNullable } from './parse-nullable'; +import { parseNumber } from './parse-number'; +import { parseObject } from './parse-object'; +import { parseOneOf } from './parse-one-of'; +import { parseString } from './parse-string'; +import type { ParserSelector, Refs, JsonSchemaObject, JsonSchema } from '../types'; +import { its } from '../utils/its'; + +const addDescribes = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => { + if (jsonSchema.description) { + zodSchema = zodSchema.describe(jsonSchema.description); + } + + return zodSchema; +}; + +const addDefaults = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => { + if (jsonSchema.default !== undefined) { + zodSchema = zodSchema.default(jsonSchema.default); + } + + return zodSchema; +}; + +const addAnnotations = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => { + if (jsonSchema.readOnly) { + zodSchema = zodSchema.readonly(); + } + + return zodSchema; +}; + +const selectParser: ParserSelector = (schema, refs) => { + if (its.a.nullable(schema)) { + return parseNullable(schema, refs); + } else if (its.an.object(schema)) { + return parseObject(schema, refs); + } else if (its.an.array(schema)) { + return parseArray(schema, refs); + } else if (its.an.anyOf(schema)) { + return parseAnyOf(schema, refs); + } else if (its.an.allOf(schema)) { + return parseAllOf(schema, refs); + } else if (its.a.oneOf(schema)) { + return parseOneOf(schema, refs); + } else if (its.a.not(schema)) { + return parseNot(schema, refs); + } else if (its.an.enum(schema)) { + return parseEnum(schema); //<-- needs to come before primitives + } else if (its.a.const(schema)) { + return parseConst(schema); + } else if (its.a.multipleType(schema)) { + return parseMultipleType(schema, refs); + } else if (its.a.primitive(schema, 'string')) { + return parseString(schema); + } else if (its.a.primitive(schema, 'number') || its.a.primitive(schema, 'integer')) { + return parseNumber(schema); + } else if (its.a.primitive(schema, 'boolean')) { + return parseBoolean(schema); + } else if (its.a.primitive(schema, 'null')) { + return parseNull(schema); + } else if (its.a.conditional(schema)) { + return parseIfThenElse(schema, refs); + } else { + return parseDefault(schema); + } +}; + +export const parseSchema = ( + jsonSchema: JsonSchema, + refs: Refs = { seen: new Map(), path: [] }, + blockMeta?: boolean, +): z.ZodTypeAny => { + if (typeof jsonSchema !== 'object') return jsonSchema ? z.any() : z.never(); + + if (refs.parserOverride) { + const custom = refs.parserOverride(jsonSchema, refs); + + if (custom instanceof z.ZodType) { + return custom; + } + } + + let seen = refs.seen.get(jsonSchema); + + if (seen) { + if (seen.r !== undefined) { + return seen.r; + } + + if (refs.depth === undefined || seen.n >= refs.depth) { + return z.any(); + } + + seen.n += 1; + } else { + seen = { r: undefined, n: 0 }; + refs.seen.set(jsonSchema, seen); + } + + let parsedZodSchema = selectParser(jsonSchema, refs); + if (!blockMeta) { + if (!refs.withoutDescribes) { + parsedZodSchema = addDescribes(jsonSchema, parsedZodSchema); + } + + if (!refs.withoutDefaults) { + parsedZodSchema = addDefaults(jsonSchema, parsedZodSchema); + } + + parsedZodSchema = addAnnotations(jsonSchema, parsedZodSchema); + } + + seen.r = parsedZodSchema; + + return parsedZodSchema; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts b/packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts new file mode 100644 index 0000000000..ea2be63c30 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts @@ -0,0 +1,58 @@ +import { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; +import { extendSchemaWithMessage } from '../utils/extend-schema'; + +export const parseString = (jsonSchema: JsonSchemaObject & { type: 'string' }) => { + let zodSchema = z.string(); + + zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, format, errorMsg) => { + switch (format) { + case 'email': + return zs.email(errorMsg); + case 'ip': + return zs.ip(errorMsg); + case 'ipv4': + return zs.ip({ version: 'v4', message: errorMsg }); + case 'ipv6': + return zs.ip({ version: 'v6', message: errorMsg }); + case 'uri': + return zs.url(errorMsg); + case 'uuid': + return zs.uuid(errorMsg); + case 'date-time': + return zs.datetime({ offset: true, message: errorMsg }); + case 'time': + return zs.time(errorMsg); + case 'date': + return zs.date(errorMsg); + case 'binary': + return zs.base64(errorMsg); + case 'duration': + return zs.duration(errorMsg); + default: + return zs; + } + }); + + zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'contentEncoding', (zs, _, errorMsg) => + zs.base64(errorMsg), + ); + zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'pattern', (zs, pattern, errorMsg) => + zs.regex(new RegExp(pattern), errorMsg), + ); + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'minLength', + (zs, minLength, errorMsg) => zs.min(minLength, errorMsg), + ); + zodSchema = extendSchemaWithMessage( + zodSchema, + jsonSchema, + 'maxLength', + (zs, maxLength, errorMsg) => zs.max(maxLength, errorMsg), + ); + + return zodSchema; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/types.ts b/packages/@n8n/json-schema-to-zod/src/types.ts new file mode 100644 index 0000000000..bb342af230 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/types.ts @@ -0,0 +1,82 @@ +import type { ZodTypeAny } from 'zod'; + +export type Serializable = + | { [key: string]: Serializable } + | Serializable[] + | string + | number + | boolean + | null; + +export type JsonSchema = JsonSchemaObject | boolean; +export type JsonSchemaObject = { + // left permissive by design + type?: string | string[]; + + // object + properties?: { [key: string]: JsonSchema }; + additionalProperties?: JsonSchema; + unevaluatedProperties?: JsonSchema; + patternProperties?: { [key: string]: JsonSchema }; + minProperties?: number; + maxProperties?: number; + required?: string[] | boolean; + propertyNames?: JsonSchema; + + // array + items?: JsonSchema | JsonSchema[]; + additionalItems?: JsonSchema; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + + // string + minLength?: number; + maxLength?: number; + pattern?: string; + format?: string; + + // number + minimum?: number; + maximum?: number; + exclusiveMinimum?: number | boolean; + exclusiveMaximum?: number | boolean; + multipleOf?: number; + + // unions + anyOf?: JsonSchema[]; + allOf?: JsonSchema[]; + oneOf?: JsonSchema[]; + + if?: JsonSchema; + then?: JsonSchema; + else?: JsonSchema; + + // shared + const?: Serializable; + enum?: Serializable[]; + + errorMessage?: { [key: string]: string | undefined }; + + description?: string; + default?: Serializable; + readOnly?: boolean; + not?: JsonSchema; + contentEncoding?: string; + nullable?: boolean; +}; + +export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny; +export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny | undefined; + +export type JsonSchemaToZodOptions = { + withoutDefaults?: boolean; + withoutDescribes?: boolean; + parserOverride?: ParserOverride; + depth?: number; +}; + +export type Refs = JsonSchemaToZodOptions & { + path: Array; + seen: Map; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts b/packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts new file mode 100644 index 0000000000..1fd0ed720b --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts @@ -0,0 +1,23 @@ +import type { z } from 'zod'; + +import type { JsonSchemaObject } from '../types'; + +export function extendSchemaWithMessage< + TZod extends z.ZodTypeAny, + TJson extends JsonSchemaObject, + TKey extends keyof TJson, +>( + zodSchema: TZod, + jsonSchema: TJson, + key: TKey, + extend: (zodSchema: TZod, value: NonNullable, errorMessage?: string) => TZod, +) { + const value = jsonSchema[key]; + + if (value !== undefined) { + const errorMessage = jsonSchema.errorMessage?.[key as string]; + return extend(zodSchema, value as NonNullable, errorMessage); + } + + return zodSchema; +} diff --git a/packages/@n8n/json-schema-to-zod/src/utils/half.ts b/packages/@n8n/json-schema-to-zod/src/utils/half.ts new file mode 100644 index 0000000000..810776e6c2 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/utils/half.ts @@ -0,0 +1,3 @@ +export const half = (arr: T[]): [T[], T[]] => { + return [arr.slice(0, arr.length / 2), arr.slice(arr.length / 2)]; +}; diff --git a/packages/@n8n/json-schema-to-zod/src/utils/its.ts b/packages/@n8n/json-schema-to-zod/src/utils/its.ts new file mode 100644 index 0000000000..494c1f6372 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/utils/its.ts @@ -0,0 +1,57 @@ +import type { JsonSchema, JsonSchemaObject, Serializable } from '../types'; + +export const its = { + an: { + object: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'object' } => + x.type === 'object', + array: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'array' } => x.type === 'array', + anyOf: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + anyOf: JsonSchema[]; + } => x.anyOf !== undefined, + allOf: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + allOf: JsonSchema[]; + } => x.allOf !== undefined, + enum: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + enum: Serializable | Serializable[]; + } => x.enum !== undefined, + }, + a: { + nullable: (x: JsonSchemaObject): x is JsonSchemaObject & { nullable: true } => + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (x as any).nullable === true, + multipleType: (x: JsonSchemaObject): x is JsonSchemaObject & { type: string[] } => + Array.isArray(x.type), + not: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + not: JsonSchema; + } => x.not !== undefined, + const: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + const: Serializable; + } => x.const !== undefined, + primitive: ( + x: JsonSchemaObject, + p: T, + ): x is JsonSchemaObject & { type: T } => x.type === p, + conditional: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + if: JsonSchema; + then: JsonSchema; + else: JsonSchema; + } => Boolean('if' in x && x.if && 'then' in x && 'else' in x && x.then && x.else), + oneOf: ( + x: JsonSchemaObject, + ): x is JsonSchemaObject & { + oneOf: JsonSchema[]; + } => x.oneOf !== undefined, + }, +}; diff --git a/packages/@n8n/json-schema-to-zod/src/utils/omit.ts b/packages/@n8n/json-schema-to-zod/src/utils/omit.ts new file mode 100644 index 0000000000..af9d579fb6 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/src/utils/omit.ts @@ -0,0 +1,8 @@ +export const omit = (obj: T, ...keys: K[]): Omit => + Object.keys(obj).reduce((acc: Record, key) => { + if (!keys.includes(key as K)) { + acc[key] = obj[key as K]; + } + + return acc; + }, {}) as Omit; diff --git a/packages/@n8n/json-schema-to-zod/test/all.json b/packages/@n8n/json-schema-to-zod/test/all.json new file mode 100644 index 0000000000..f270ca3fa1 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/all.json @@ -0,0 +1,143 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "properties": { + "allOf": { + "allOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "anyOf": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "oneOf": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "array": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "maxItems": 3 + }, + "tuple": { + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 3 + }, + "const": { + "const": "xbox" + }, + "enum": { + "enum": ["ps4", "ps5"] + }, + "ifThenElse": { + "if": { + "type": "string" + }, + "then": { + "const": "x" + }, + "else": { + "enum": [1, 2, 3] + } + }, + "null": { + "type": "null" + }, + "multiple": { + "type": ["array", "boolean"] + }, + "objAdditionalTrue": { + "type": "object", + "properties": { + "x": { + "type": "string" + } + }, + "additionalProperties": true + }, + "objAdditionalFalse": { + "type": "object", + "properties": { + "x": { + "type": "string" + } + }, + "additionalProperties": false + }, + "objAdditionalNumber": { + "type": "object", + "properties": { + "x": { + "type": "string" + } + }, + "additionalProperties": { + "type": "number" + } + }, + "objAdditionalOnly": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "patternProps": { + "type": "object", + "patternProperties": { + "^x": { + "type": "string" + }, + "^y": { + "type": "number" + } + }, + "properties": { + "z": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/@n8n/json-schema-to-zod/test/extend-expect.ts b/packages/@n8n/json-schema-to-zod/test/extend-expect.ts new file mode 100644 index 0000000000..5196e43416 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/extend-expect.ts @@ -0,0 +1,16 @@ +import type { z } from 'zod'; + +expect.extend({ + toMatchZod(this: jest.MatcherContext, actual: z.ZodTypeAny, expected: z.ZodTypeAny) { + const actualSerialized = JSON.stringify(actual._def, null, 2); + const expectedSerialized = JSON.stringify(expected._def, null, 2); + const pass = this.equals(actualSerialized, expectedSerialized); + + return { + pass, + message: pass + ? () => `Expected ${actualSerialized} not to match ${expectedSerialized}` + : () => `Expected ${actualSerialized} to match ${expectedSerialized}`, + }; + }, +}); diff --git a/packages/@n8n/json-schema-to-zod/test/jest.d.ts b/packages/@n8n/json-schema-to-zod/test/jest.d.ts new file mode 100644 index 0000000000..dff5a5fa4c --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/jest.d.ts @@ -0,0 +1,5 @@ +namespace jest { + interface Matchers { + toMatchZod(expected: unknown): T; + } +} diff --git a/packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts b/packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts new file mode 100644 index 0000000000..5f383ae71b --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts @@ -0,0 +1,106 @@ +import type { JSONSchema4, JSONSchema6Definition, JSONSchema7Definition } from 'json-schema'; +import { z } from 'zod'; + +import { jsonSchemaToZod } from '../src'; + +describe('jsonSchemaToZod', () => { + test('should accept json schema 7 and 4', () => { + const schema = { type: 'string' } as unknown; + + expect(jsonSchemaToZod(schema as JSONSchema4)); + expect(jsonSchemaToZod(schema as JSONSchema6Definition)); + expect(jsonSchemaToZod(schema as JSONSchema7Definition)); + }); + + test('can exclude defaults', () => { + expect( + jsonSchemaToZod( + { + type: 'string', + default: 'foo', + }, + { withoutDefaults: true }, + ), + ).toMatchZod(z.string()); + }); + + test('should include describes', () => { + expect( + jsonSchemaToZod({ + type: 'string', + description: 'foo', + }), + ).toMatchZod(z.string().describe('foo')); + }); + + test('can exclude describes', () => { + expect( + jsonSchemaToZod( + { + type: 'string', + description: 'foo', + }, + { + withoutDescribes: true, + }, + ), + ).toMatchZod(z.string()); + }); + + test('will remove optionality if default is present', () => { + expect( + jsonSchemaToZod({ + type: 'object', + properties: { + prop: { + type: 'string', + default: 'def', + }, + }, + }), + ).toMatchZod(z.object({ prop: z.string().default('def') })); + }); + + test('will handle falsy defaults', () => { + expect( + jsonSchemaToZod({ + type: 'boolean', + default: false, + }), + ).toMatchZod(z.boolean().default(false)); + }); + + test('will ignore undefined as default', () => { + expect( + jsonSchemaToZod({ + type: 'null', + default: undefined, + }), + ).toMatchZod(z.null()); + }); + + test('should be possible to define a custom parser', () => { + expect( + jsonSchemaToZod( + { + allOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean', description: 'foo' }], + }, + { + parserOverride: (schema, refs) => { + if ( + refs.path.length === 2 && + refs.path[0] === 'allOf' && + refs.path[1] === 2 && + schema.type === 'boolean' && + schema.description === 'foo' + ) { + return z.null(); + } + + return undefined; + }, + }, + ), + ).toMatchZod(z.intersection(z.string(), z.intersection(z.number(), z.null()))); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-all-of.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-all-of.test.ts new file mode 100644 index 0000000000..546572255c --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-all-of.test.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +import { parseAllOf } from '../../src/parsers/parse-all-of'; + +describe('parseAllOf', () => { + test('should create never if empty', () => { + expect( + parseAllOf( + { + allOf: [], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.never()); + }); + + test('should handle true values', () => { + expect( + parseAllOf( + { + allOf: [{ type: 'string' }, true], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.intersection(z.string(), z.any())); + }); + + test('should handle false values', () => { + expect( + parseAllOf( + { + allOf: [{ type: 'string' }, false], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.intersection( + z.string(), + z + .any() + .refine( + (value) => !z.any().safeParse(value).success, + 'Invalid input: Should NOT be valid against schema', + ), + ), + ); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-any-of.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-any-of.test.ts new file mode 100644 index 0000000000..72abcab047 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-any-of.test.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { parseAnyOf } from '../../src/parsers/parse-any-of'; + +describe('parseAnyOf', () => { + test('should create a union from two or more schemas', () => { + expect( + parseAnyOf( + { + anyOf: [ + { + type: 'string', + }, + { type: 'number' }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.union([z.string(), z.number()])); + }); + + test('should extract a single schema', () => { + expect(parseAnyOf({ anyOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod( + z.string(), + ); + }); + + test('should return z.any() if array is empty', () => { + expect(parseAnyOf({ anyOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any()); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-array.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-array.test.ts new file mode 100644 index 0000000000..b96df3958c --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-array.test.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +import { parseArray } from '../../src/parsers/parse-array'; + +describe('parseArray', () => { + test('should create tuple with items array', () => { + expect( + parseArray( + { + type: 'array', + items: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.tuple([z.string(), z.number()])); + }); + + test('should create array with items object', () => { + expect( + parseArray( + { + type: 'array', + items: { + type: 'string', + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.array(z.string())); + }); + + test('should create min for minItems', () => { + expect( + parseArray( + { + type: 'array', + minItems: 2, + items: { + type: 'string', + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.array(z.string()).min(2)); + }); + + test('should create max for maxItems', () => { + expect( + parseArray( + { + type: 'array', + maxItems: 2, + items: { + type: 'string', + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.array(z.string()).max(2)); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-const.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-const.test.ts new file mode 100644 index 0000000000..b4f7a5afd5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-const.test.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { parseConst } from '../../src/parsers/parse-const'; + +describe('parseConst', () => { + test('should handle falsy constants', () => { + expect( + parseConst({ + const: false, + }), + ).toMatchZod(z.literal(false)); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-enum.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-enum.test.ts new file mode 100644 index 0000000000..2ed00e3def --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-enum.test.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +import { parseEnum } from '../../src/parsers/parse-enum'; + +describe('parseEnum', () => { + test('should create never with empty enum', () => { + expect( + parseEnum({ + enum: [], + }), + ).toMatchZod(z.never()); + }); + + test('should create literal with single item enum', () => { + expect( + parseEnum({ + enum: ['someValue'], + }), + ).toMatchZod(z.literal('someValue')); + }); + + test('should create enum array with string enums', () => { + expect( + parseEnum({ + enum: ['someValue', 'anotherValue'], + }), + ).toMatchZod(z.enum(['someValue', 'anotherValue'])); + }); + test('should create union with mixed enums', () => { + expect( + parseEnum({ + enum: ['someValue', 57], + }), + ).toMatchZod(z.union([z.literal('someValue'), z.literal(57)])); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-not.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-not.test.ts new file mode 100644 index 0000000000..f1bc1d7ab2 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-not.test.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { parseNot } from '../../src/parsers/parse-not'; + +describe('parseNot', () => { + test('parseNot', () => { + expect( + parseNot( + { + not: { + type: 'string', + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z + .any() + .refine( + (value) => !z.string().safeParse(value).success, + 'Invalid input: Should NOT be valid against schema', + ), + ); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-nullable.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-nullable.test.ts new file mode 100644 index 0000000000..046c3f41a1 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-nullable.test.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +import { parseSchema } from '../../src/parsers/parse-schema'; + +describe('parseNullable', () => { + test('parseSchema should not add default twice', () => { + expect( + parseSchema( + { + type: 'string', + nullable: true, + default: null, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.string().nullable().default(null)); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-number.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-number.test.ts new file mode 100644 index 0000000000..7b3cdf4ded --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-number.test.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; + +import { parseNumber } from '../../src/parsers/parse-number'; + +describe('parseNumber', () => { + test('should handle integer', () => { + expect( + parseNumber({ + type: 'integer', + }), + ).toMatchZod(z.number().int()); + + expect( + parseNumber({ + type: 'integer', + multipleOf: 1, + }), + ).toMatchZod(z.number().int()); + + expect( + parseNumber({ + type: 'number', + multipleOf: 1, + }), + ).toMatchZod(z.number().int()); + }); + + test('should handle maximum with exclusiveMinimum', () => { + expect( + parseNumber({ + type: 'number', + exclusiveMinimum: true, + minimum: 2, + }), + ).toMatchZod(z.number().gt(2)); + }); + + test('should handle maximum with exclusiveMinimum', () => { + expect( + parseNumber({ + type: 'number', + minimum: 2, + }), + ).toMatchZod(z.number().gte(2)); + }); + + test('should handle maximum with exclusiveMaximum', () => { + expect( + parseNumber({ + type: 'number', + exclusiveMaximum: true, + maximum: 2, + }), + ).toMatchZod(z.number().lt(2)); + }); + + test('should handle numeric exclusiveMaximum', () => { + expect( + parseNumber({ + type: 'number', + exclusiveMaximum: 2, + }), + ).toMatchZod(z.number().lt(2)); + }); + + test('should accept errorMessage', () => { + expect( + parseNumber({ + type: 'number', + format: 'int64', + exclusiveMinimum: 0, + maximum: 2, + multipleOf: 2, + errorMessage: { + format: 'ayy', + multipleOf: 'lmao', + exclusiveMinimum: 'deez', + maximum: 'nuts', + }, + }), + ).toMatchZod(z.number().int('ayy').multipleOf(2, 'lmao').gt(0, 'deez').lte(2, 'nuts')); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-object.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-object.test.ts new file mode 100644 index 0000000000..00a2e194cd --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-object.test.ts @@ -0,0 +1,904 @@ +/* eslint-disable n8n-local-rules/no-skipped-tests */ +import type { JSONSchema7 } from 'json-schema'; +import { z, ZodError } from 'zod'; + +import { parseObject } from '../../src/parsers/parse-object'; + +describe('parseObject', () => { + test('should handle with missing properties', () => { + expect( + parseObject( + { + type: 'object', + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.record(z.any())); + }); + + test('should handle with empty properties', () => { + expect( + parseObject( + { + type: 'object', + properties: {}, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({})); + }); + + test('With properties - should handle optional and required properties', () => { + expect( + parseObject( + { + type: 'object', + required: ['myRequiredString'], + properties: { + myOptionalString: { + type: 'string', + }, + myRequiredString: { + type: 'string', + }, + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.object({ myOptionalString: z.string().optional(), myRequiredString: z.string() }), + ); + }); + + test('With properties - should handle additionalProperties when set to false', () => { + expect( + parseObject( + { + type: 'object', + required: ['myString'], + properties: { + myString: { + type: 'string', + }, + }, + additionalProperties: false, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({ myString: z.string() }).strict()); + }); + + test('With properties - should handle additionalProperties when set to true', () => { + expect( + parseObject( + { + type: 'object', + required: ['myString'], + properties: { + myString: { + type: 'string', + }, + }, + additionalProperties: true, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({ myString: z.string() }).catchall(z.any())); + }); + + test('With properties - should handle additionalProperties when provided a schema', () => { + expect( + parseObject( + { + type: 'object', + required: ['myString'], + properties: { + myString: { + type: 'string', + }, + }, + additionalProperties: { type: 'number' }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({ myString: z.string() }).catchall(z.number())); + }); + + test('Without properties - should handle additionalProperties when set to false', () => { + expect( + parseObject( + { + type: 'object', + additionalProperties: false, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.record(z.never())); + }); + + test('Without properties - should handle additionalProperties when set to true', () => { + expect( + parseObject( + { + type: 'object', + additionalProperties: true, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.record(z.any())); + }); + + test('Without properties - should handle additionalProperties when provided a schema', () => { + expect( + parseObject( + { + type: 'object', + additionalProperties: { type: 'number' }, + }, + + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.record(z.number())); + }); + + test('Without properties - should include falsy defaults', () => { + expect( + parseObject( + { + type: 'object', + properties: { + s: { + type: 'string', + default: '', + }, + }, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({ s: z.string().default('') })); + }); + + test('eh', () => { + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + anyOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + { + required: ['c'], + properties: { + c: { + type: 'string', + }, + }, + }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z + .object({ a: z.string() }) + .and(z.union([z.object({ b: z.string() }), z.object({ c: z.string() })])), + ); + + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + anyOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + {}, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.object({ a: z.string() }).and(z.union([z.object({ b: z.string() }), z.any()]))); + + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + oneOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + { + required: ['c'], + properties: { + c: { + type: 'string', + }, + }, + }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.object({ a: z.string() }).and( + z.any().superRefine((x, ctx) => { + const schemas = [z.object({ b: z.string() }), z.object({ c: z.string() })]; + const errors = schemas.reduce( + (errors, schema) => + ((result) => (result.error ? [...errors, result.error] : errors))( + schema.safeParse(x), + ), + [], + ); + if (schemas.length - errors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: 'invalid_union', + unionErrors: errors, + message: 'Invalid input: Should pass single schema', + }); + } + }), + ), + ); + + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + oneOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + {}, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.object({ a: z.string() }).and( + z.any().superRefine((x, ctx) => { + const schemas = [z.object({ b: z.string() }), z.any()]; + const errors = schemas.reduce( + (errors, schema) => + ((result) => (result.error ? [...errors, result.error] : errors))( + schema.safeParse(x), + ), + [], + ); + if (schemas.length - errors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: 'invalid_union', + unionErrors: errors, + message: 'Invalid input: Should pass single schema', + }); + } + }), + ), + ); + + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + allOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + { + required: ['c'], + properties: { + c: { + type: 'string', + }, + }, + }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z + .object({ a: z.string() }) + .and(z.intersection(z.object({ b: z.string() }), z.object({ c: z.string() }))), + ); + + expect( + parseObject( + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + allOf: [ + { + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + {}, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.object({ a: z.string() }).and(z.intersection(z.object({ b: z.string() }), z.any())), + ); + }); + + const run = (zodSchema: z.ZodTypeAny, data: unknown) => zodSchema.safeParse(data); + + test('Functional tests - run', () => { + expect(run(z.string(), 'hello')).toEqual({ + success: true, + data: 'hello', + }); + }); + + test('Functional tests - properties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + b: { + type: 'number', + }, + }, + }; + + const expected = z.object({ a: z.string(), b: z.number().optional() }); + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + + expect(run(result, { a: 'hello' })).toEqual({ + success: true, + data: { + a: 'hello', + }, + }); + + expect(run(result, { a: 'hello', b: 123 })).toEqual({ + success: true, + data: { + a: 'hello', + b: 123, + }, + }); + + expect(run(result, { b: 'hello', x: true })).toEqual({ + success: false, + error: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['a'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['b'], + message: 'Expected number, received string', + }, + ]), + }); + }); + + test('Functional tests - properties and additionalProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + b: { + type: 'number', + }, + }, + additionalProperties: { type: 'boolean' }, + }; + + const expected = z.object({ a: z.string(), b: z.number().optional() }).catchall(z.boolean()); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + + expect(run(result, { b: 'hello', x: 'true' })).toEqual({ + success: false, + error: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['a'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['b'], + message: 'Expected number, received string', + }, + { + code: 'invalid_type', + expected: 'boolean', + received: 'string', + path: ['x'], + message: 'Expected boolean, received string', + }, + ]), + }); + }); + + test('Functional tests - properties and single-item patternProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + b: { + type: 'number', + }, + }, + patternProperties: { + '\\.': { type: 'array' }, + }, + }; + + const expected = z + .object({ a: z.string(), b: z.number().optional() }) + .catchall(z.array(z.any())) + .superRefine((value, ctx) => { + for (const key in value) { + if (key.match(new RegExp('\\\\.'))) { + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + + expect(run(result, { a: 'a', b: 2, '.': [] })).toEqual({ + success: true, + data: { a: 'a', b: 2, '.': [] }, + }); + + expect(run(result, { a: 'a', b: 2, '.': '[]' })).toEqual({ + success: false, + error: new ZodError([ + { + code: 'invalid_type', + expected: 'array', + received: 'string', + path: ['.'], + message: 'Expected array, received string', + }, + ]), + }); + }); + + test('Functional tests - properties, additionalProperties and patternProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + b: { + type: 'number', + }, + }, + additionalProperties: { type: 'boolean' }, + patternProperties: { + '\\.': { type: 'array' }, + '\\,': { type: 'array', minItems: 1 }, + }, + }; + + const expected = z + .object({ a: z.string(), b: z.number().optional() }) + .catchall(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()])) + .superRefine((value, ctx) => { + for (const key in value) { + let evaluated = ['a', 'b'].includes(key); + if (key.match(new RegExp('\\\\.'))) { + evaluated = true; + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (key.match(new RegExp('\\\\,'))) { + evaluated = true; + const result = z.array(z.any()).min(1).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (!evaluated) { + const result = z.boolean().safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: 'Invalid input: must match catchall schema', + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + }); + + test('Functional tests - additionalProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + additionalProperties: { type: 'boolean' }, + }; + + const expected = z.record(z.boolean()); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + }); + + test('Functional tests - additionalProperties and patternProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + additionalProperties: { type: 'boolean' }, + patternProperties: { + '\\.': { type: 'array' }, + '\\,': { type: 'array', minItems: 1 }, + }, + }; + + const expected = z + .record(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()])) + .superRefine((value, ctx) => { + for (const key in value) { + let evaluated = false; + if (key.match(new RegExp('\\\\.'))) { + evaluated = true; + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (key.match(new RegExp('\\\\,'))) { + evaluated = true; + const result = z.array(z.any()).min(1).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (!evaluated) { + const result = z.boolean().safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: 'Invalid input: must match catchall schema', + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + + expect(run(result, { x: true, '.': [], ',': [] })).toEqual({ + success: false, + error: new ZodError([ + { + path: [','], + code: 'custom', + message: 'Invalid input: Key matching regex /,/ must match schema', + params: { + issues: [ + { + code: 'too_small', + minimum: 1, + type: 'array', + inclusive: true, + exact: false, + message: 'Array must contain at least 1 element(s)', + path: [], + }, + ], + }, + }, + ]), + }); + }); + + test('Functional tests - single-item patternProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + patternProperties: { + '\\.': { type: 'array' }, + }, + }; + + const expected = z.record(z.array(z.any())).superRefine((value, ctx) => { + for (const key in value) { + if (key.match(new RegExp('\\\\.'))) { + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + }); + + test('Functional tests - patternProperties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + patternProperties: { + '\\.': { type: 'array' }, + '\\,': { type: 'array', minItems: 1 }, + }, + }; + + const expected = z + .record(z.union([z.array(z.any()), z.array(z.any()).min(1)])) + .superRefine((value, ctx) => { + for (const key in value) { + if (key.match(new RegExp('\\.'))) { + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (key.match(new RegExp('\\,'))) { + const result = z.array(z.any()).min(1).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(run(result, { '.': [] })).toEqual({ + success: true, + data: { '.': [] }, + }); + + expect(run(result, { ',': [] })).toEqual({ + success: false, + error: new ZodError([ + { + path: [','], + code: 'custom', + message: 'Invalid input: Key matching regex /,/ must match schema', + params: { + issues: [ + { + code: 'too_small', + minimum: 1, + type: 'array', + inclusive: true, + exact: false, + message: 'Array must contain at least 1 element(s)', + path: [], + }, + ], + }, + }, + ]), + }); + + expect(result).toMatchZod(expected); + }); + + test('Functional tests - patternProperties and properties', () => { + const schema: JSONSchema7 & { type: 'object' } = { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + b: { + type: 'number', + }, + }, + patternProperties: { + '\\.': { type: 'array' }, + '\\,': { type: 'array', minItems: 1 }, + }, + }; + + const expected = z + .object({ a: z.string(), b: z.number().optional() }) + .catchall(z.union([z.array(z.any()), z.array(z.any()).min(1)])) + .superRefine((value, ctx) => { + for (const key in value) { + if (key.match(new RegExp('\\.'))) { + const result = z.array(z.any()).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + if (key.match(new RegExp('\\,'))) { + const result = z.array(z.any()).min(1).safeParse(value[key]); + if (!result.success) { + ctx.addIssue({ + path: [...ctx.path, key], + code: 'custom', + message: `Invalid input: Key matching regex /${key}/ must match schema`, + params: { + issues: result.error.issues, + }, + }); + } + } + } + }); + + const result = parseObject(schema, { path: [], seen: new Map() }); + + expect(result).toMatchZod(expected); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-one-of.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-one-of.test.ts new file mode 100644 index 0000000000..3295200576 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-one-of.test.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +import { parseOneOf } from '../../src/parsers/parse-one-of'; + +describe('parseOneOf', () => { + test('should create a union from two or more schemas', () => { + expect( + parseOneOf( + { + oneOf: [ + { + type: 'string', + }, + { type: 'number' }, + ], + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod( + z.any().superRefine((x, ctx) => { + const schemas = [z.string(), z.number()]; + const errors = schemas.reduce( + (errors, schema) => + ((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)), + [], + ); + if (schemas.length - errors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: 'invalid_union', + unionErrors: errors, + message: 'Invalid input: Should pass single schema', + }); + } + }), + ); + }); + + test('should extract a single schema', () => { + expect(parseOneOf({ oneOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod( + z.string(), + ); + }); + + test('should return z.any() if array is empty', () => { + expect(parseOneOf({ oneOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any()); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-schema.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-schema.test.ts new file mode 100644 index 0000000000..c0f0899e28 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-schema.test.ts @@ -0,0 +1,113 @@ +import { z } from 'zod'; + +import { parseSchema } from '../../src/parsers/parse-schema'; + +describe('parseSchema', () => { + test('should be usable without providing refs', () => { + expect(parseSchema({ type: 'string' })).toMatchZod(z.string()); + }); + + test('should return a seen and processed ref', () => { + const seen = new Map(); + const schema = { + type: 'object', + properties: { + prop: { + type: 'string', + }, + }, + }; + expect(parseSchema(schema, { seen, path: [] })); + expect(parseSchema(schema, { seen, path: [] })); + }); + + test('should be possible to describe a readonly schema', () => { + expect(parseSchema({ type: 'string', readOnly: true })).toMatchZod(z.string().readonly()); + }); + + test('should handle nullable', () => { + expect( + parseSchema( + { + type: 'string', + nullable: true, + }, + { path: [], seen: new Map() }, + ), + ).toMatchZod(z.string().nullable()); + }); + + test('should handle enum', () => { + expect(parseSchema({ enum: ['someValue', 57] })).toMatchZod( + z.union([z.literal('someValue'), z.literal(57)]), + ); + }); + + test('should handle multiple type', () => { + expect(parseSchema({ type: ['string', 'number'] })).toMatchZod( + z.union([z.string(), z.number()]), + ); + }); + + test('should handle if-then-else type', () => { + expect( + parseSchema({ + if: { type: 'string' }, + then: { type: 'number' }, + else: { type: 'boolean' }, + }), + ).toMatchZod( + z.union([z.number(), z.boolean()]).superRefine((value, ctx) => { + const result = z.string().safeParse(value).success + ? z.number().safeParse(value) + : z.boolean().safeParse(value); + if (!result.success) { + result.error.errors.forEach((error) => ctx.addIssue(error)); + } + }), + ); + }); + + test('should handle anyOf', () => { + expect( + parseSchema({ + anyOf: [ + { + type: 'string', + }, + { type: 'number' }, + ], + }), + ).toMatchZod(z.union([z.string(), z.number()])); + }); + + test('should handle oneOf', () => { + expect( + parseSchema({ + oneOf: [ + { + type: 'string', + }, + { type: 'number' }, + ], + }), + ).toMatchZod( + z.any().superRefine((x, ctx) => { + const schemas = [z.string(), z.number()]; + const errors = schemas.reduce( + (errors, schema) => + ((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)), + [], + ); + if (schemas.length - errors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: 'invalid_union', + unionErrors: errors, + message: 'Invalid input: Should pass single schema', + }); + } + }), + ); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/parsers/parse-string.test.ts b/packages/@n8n/json-schema-to-zod/test/parsers/parse-string.test.ts new file mode 100644 index 0000000000..5e53135c2d --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/parsers/parse-string.test.ts @@ -0,0 +1,152 @@ +import { z } from 'zod'; + +import { parseString } from '../../src/parsers/parse-string'; + +describe('parseString', () => { + const run = (schema: z.ZodString, data: unknown) => schema.safeParse(data); + + test('DateTime format', () => { + const datetime = '2018-11-13T20:20:39Z'; + + const code = parseString({ + type: 'string', + format: 'date-time', + errorMessage: { format: 'hello' }, + }); + + expect(code).toMatchZod(z.string().datetime({ offset: true, message: 'hello' })); + + expect(run(code, datetime)).toEqual({ success: true, data: datetime }); + }); + + test('email', () => { + expect( + parseString({ + type: 'string', + format: 'email', + }), + ).toMatchZod(z.string().email()); + }); + + test('ip', () => { + expect( + parseString({ + type: 'string', + format: 'ip', + }), + ).toMatchZod(z.string().ip()); + + expect( + parseString({ + type: 'string', + format: 'ipv6', + }), + ).toMatchZod(z.string().ip({ version: 'v6' })); + }); + + test('uri', () => { + expect( + parseString({ + type: 'string', + format: 'uri', + }), + ).toMatchZod(z.string().url()); + }); + + test('uuid', () => { + expect( + parseString({ + type: 'string', + format: 'uuid', + }), + ).toMatchZod(z.string().uuid()); + }); + + test('time', () => { + expect( + parseString({ + type: 'string', + format: 'time', + }), + ).toMatchZod(z.string().time()); + }); + + test('date', () => { + expect( + parseString({ + type: 'string', + format: 'date', + }), + ).toMatchZod(z.string().date()); + }); + + test('duration', () => { + expect( + parseString({ + type: 'string', + format: 'duration', + }), + ).toMatchZod(z.string().duration()); + }); + + test('base64', () => { + expect( + parseString({ + type: 'string', + contentEncoding: 'base64', + }), + ).toMatchZod(z.string().base64()); + + expect( + parseString({ + type: 'string', + contentEncoding: 'base64', + errorMessage: { + contentEncoding: 'x', + }, + }), + ).toMatchZod(z.string().base64('x')); + + expect( + parseString({ + type: 'string', + format: 'binary', + }), + ).toMatchZod(z.string().base64()); + + expect( + parseString({ + type: 'string', + format: 'binary', + errorMessage: { + format: 'x', + }, + }), + ).toMatchZod(z.string().base64('x')); + }); + + test('should accept errorMessage', () => { + expect( + parseString({ + type: 'string', + format: 'ipv4', + pattern: 'x', + minLength: 1, + maxLength: 2, + errorMessage: { + format: 'ayy', + pattern: 'lmao', + minLength: 'deez', + maxLength: 'nuts', + }, + }), + ).toMatchZod( + z + .string() + .ip({ version: 'v4', message: 'ayy' }) + .regex(new RegExp('x'), 'lmao') + .min(1, 'deez') + .max(2, 'nuts'), + ); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/utils/half.test.ts b/packages/@n8n/json-schema-to-zod/test/utils/half.test.ts new file mode 100644 index 0000000000..afd9ee85fd --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/utils/half.test.ts @@ -0,0 +1,15 @@ +import { half } from '../../src/utils/half'; + +describe('half', () => { + test('half', () => { + const [a, b] = half(['A', 'B', 'C', 'D', 'E']); + + if (1 < 0) { + // type should be string + a[0].endsWith(''); + } + + expect(a).toEqual(['A', 'B']); + expect(b).toEqual(['C', 'D', 'E']); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts b/packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts new file mode 100644 index 0000000000..f5f51313f5 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts @@ -0,0 +1,27 @@ +import { omit } from '../../src/utils/omit'; + +describe('omit', () => { + test('omit', () => { + const input = { + a: true, + b: true, + }; + + omit( + input, + 'b', + // @ts-expect-error + 'c', + ); + + const output = omit(input, 'b'); + + // @ts-expect-error + output.b; + + expect(output.a).toBe(true); + + // @ts-expect-error + expect(output.b).toBeUndefined(); + }); +}); diff --git a/packages/@n8n/json-schema-to-zod/tsconfig.cjs.json b/packages/@n8n/json-schema-to-zod/tsconfig.cjs.json new file mode 100644 index 0000000000..2a17765d74 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "dist/cjs", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src"] +} diff --git a/packages/@n8n/json-schema-to-zod/tsconfig.esm.json b/packages/@n8n/json-schema-to-zod/tsconfig.esm.json new file mode 100644 index 0000000000..21f4508341 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "es2020", + "moduleResolution": "node", + "outDir": "dist/esm", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src"] +} diff --git a/packages/@n8n/json-schema-to-zod/tsconfig.json b/packages/@n8n/json-schema-to-zod/tsconfig.json new file mode 100644 index 0000000000..f8e6508e74 --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": ["../../../tsconfig.json"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": "src", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/@n8n/json-schema-to-zod/tsconfig.types.json b/packages/@n8n/json-schema-to-zod/tsconfig.types.json new file mode 100644 index 0000000000..63451df65a --- /dev/null +++ b/packages/@n8n/json-schema-to-zod/tsconfig.types.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10528f347b..e83c855bd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,6 +394,18 @@ importers: specifier: ^0.0.3 version: 0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4) + packages/@n8n/json-schema-to-zod: + devDependencies: + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 + '@types/node': + specifier: ^18.16.16 + version: 18.16.16 + zod: + specifier: 'catalog:' + version: 3.23.8 + packages/@n8n/nodes-langchain: dependencies: '@aws-sdk/client-sso-oidc':