From 56bcac376668471b3cb2a54ccd4e0fd86b4d87a3 Mon Sep 17 00:00:00 2001 From: DubeSebastien90 <100394271+DubeSebastien90@users.noreply.github.com> Date: Fri, 29 May 2026 14:02:03 -0400 Subject: [PATCH 1/4] feat(zui): Path propagation for from json schema and from object (#15226) --- packages/zui/package.json | 2 +- packages/zui/src/transforms/common/errors.ts | 12 +- .../zui-from-json-schema/index.test.ts | 146 ++++++++++++++++++ .../transforms/zui-from-json-schema/index.ts | 84 +++++----- .../zui-from-json-schema/iterables/array.ts | 29 ++-- .../zui-from-json-schema/primitives/index.ts | 9 +- .../src/transforms/zui-from-object/index.ts | 14 +- .../zui-from-object/object-to-zui.test.ts | 25 +++ 8 files changed, 261 insertions(+), 60 deletions(-) diff --git a/packages/zui/package.json b/packages/zui/package.json index 3bdd2ab47ee..f4dd5e82160 100644 --- a/packages/zui/package.json +++ b/packages/zui/package.json @@ -1,6 +1,6 @@ { "name": "@bpinternal/zui", - "version": "2.2.1", + "version": "2.3.0", "description": "A fork of Zod with additional features", "type": "module", "source": "./src/index.ts", diff --git a/packages/zui/src/transforms/common/errors.ts b/packages/zui/src/transforms/common/errors.ts index 3551001e3db..e93578ef588 100644 --- a/packages/zui/src/transforms/common/errors.ts +++ b/packages/zui/src/transforms/common/errors.ts @@ -21,20 +21,20 @@ export abstract class ZuiTransformError extends Error { // json-schema-to-zui-error export class JSONSchemaToZuiError extends ZuiTransformError { - public constructor(message?: string) { - super('json-schema-to-zui', message) + public constructor(message?: string, path?: string) { + super('json-schema-to-zui', message, path) } } export class UnsupportedJSONSchemaToZuiError extends JSONSchemaToZuiError { - public constructor(schema: JSONSchema7) { - super(`JSON Schema ${JSON.stringify(schema)} cannot be transformed to ZUI type.`) + public constructor(schema: JSONSchema7, path: string) { + super(`JSON Schema ${JSON.stringify(schema)} cannot be transformed to ZUI type.`, path) } } // object-to-zui-error export class ObjectToZuiError extends ZuiTransformError { - public constructor(message?: string) { - super('object-to-zui', message) + public constructor(message: string, path: string) { + super('object-to-zui', message, path) } } diff --git a/packages/zui/src/transforms/zui-from-json-schema/index.test.ts b/packages/zui/src/transforms/zui-from-json-schema/index.test.ts index 1dc57a53635..fdf1fa53bbb 100644 --- a/packages/zui/src/transforms/zui-from-json-schema/index.test.ts +++ b/packages/zui/src/transforms/zui-from-json-schema/index.test.ts @@ -1,6 +1,7 @@ import * as z from '../../z' import { describe, test, expect } from 'vitest' import { fromJSONSchema } from './index' +import * as errs from '../common/errors' import { JSONSchema7 } from 'json-schema' import { Schema as ZuiJSONSchema } from '../common/json-schema' import { toJSONSchema } from '../zui-to-json-schema' @@ -1134,4 +1135,149 @@ describe.concurrent('zuifromJSONSchemaNext', () => { expect(bool._def.description).toBe('A boolean parameter') expect(nul._def.description).toBe('A null parameter') }) + + describe.concurrent('error path propagation', () => { + test('should add object keys to path', () => { + try { + fromJSONSchema({ + type: 'object', + properties: { + foo: { + type: 'object', + properties: { + bar: { + type: 'object', + patternProperties: { '^S_': { type: 'string' } }, + }, + }, + }, + }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo.bar') + } + }) + + test('should add [number] to path for array item error', () => { + try { + fromJSONSchema({ + type: 'array', + items: { if: { properties: { active: { type: 'boolean' } } } }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#[number]') + } + }) + + test('should add [index] to path for tuple item error', () => { + try { + fromJSONSchema({ + type: 'array', + items: [{ type: 'string' }, { type: 'number' }, { patternProperties: { '^x_': { type: 'string' } } }], + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#[2]') + } + }) + + test('should add [number] to path for set item error', () => { + try { + fromJSONSchema({ + type: 'array', + uniqueItems: true, + items: { if: { properties: { active: { type: 'boolean' } } } }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#[number]') + } + }) + + test('should add [index] to path for anyOf (union) branch error', () => { + try { + fromJSONSchema({ + type: 'object', + properties: { + foo: { anyOf: [{ type: 'string' }, { if: { properties: { active: { type: 'boolean' } } } }] }, + }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo[1]') + } + }) + + test('should add [index] to path for oneOf (undiscriminated union) branch error', () => { + try { + fromJSONSchema({ + type: 'object', + properties: { + foo: { oneOf: [{ type: 'string' }, { patternProperties: { '^x_': { type: 'string' } } }] }, + }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo[1]') + } + }) + + test('should add [index] to path for allOf (intersection) branch error', () => { + try { + fromJSONSchema({ + type: 'object', + properties: { + foo: { + allOf: [ + { type: 'object', properties: { bar: { type: 'string' } } }, + { propertyNames: { pattern: '^[A-Z]+$' } }, + ], + }, + }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo[1]') + } + }) + + test('should add [string] to path for object additionalProperties (catchall) error', () => { + try { + fromJSONSchema({ + type: 'object', + properties: { + foo: { type: 'string' }, + }, + additionalProperties: { if: { properties: { active: { type: 'boolean' } } } }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#[string]') + } + }) + + test('should add [number] to path for tuple rest (additionalItems) error', () => { + try { + fromJSONSchema({ + type: 'array', + items: [{ type: 'number' }, { type: 'number' }], + additionalItems: { patternProperties: { '^x_': { type: 'string' } } }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#[number]') + } + }) + }) // error path propagation }) diff --git a/packages/zui/src/transforms/zui-from-json-schema/index.ts b/packages/zui/src/transforms/zui-from-json-schema/index.ts index c2de6ff8fbe..bad32b4f3f3 100644 --- a/packages/zui/src/transforms/zui-from-json-schema/index.ts +++ b/packages/zui/src/transforms/zui-from-json-schema/index.ts @@ -1,4 +1,5 @@ import { JSONSchema7, JSONSchema7Definition } from 'json-schema' +import { PropertyPath } from '../../utils/property-path-utils' import * as z from '../../z' import * as errors from '../common/errors' import { ArraySchema, SetSchema, TupleSchema } from '../common/json-schema' @@ -14,10 +15,10 @@ const DEFAULT_TYPE = z.any() * @returns ZUI Schema */ export function fromJSONSchema(schema: JSONSchema7): z.ZodType { - return _fromJSONSchema(schema) + return _fromJSONSchema(schema, new PropertyPath()) } -function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { +function _fromJSONSchema(schema: JSONSchema7Definition | undefined, path: PropertyPath): z.ZodType { if (schema === undefined) { return DEFAULT_TYPE } @@ -31,36 +32,36 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } if (schema.default !== undefined) { - const inner = _fromJSONSchema({ ...schema, default: undefined }) + const inner = _fromJSONSchema({ ...schema, default: undefined }, path) return inner.default(schema.default) } if (schema.readOnly) { - const inner = _fromJSONSchema({ ...schema, readOnly: undefined }) + const inner = _fromJSONSchema({ ...schema, readOnly: undefined }, path) return inner.readonly() } if (schema.description !== undefined) { - const inner = _fromJSONSchema({ ...schema, description: undefined }) + const inner = _fromJSONSchema({ ...schema, description: undefined }, path) return inner.describe(schema.description) } if (schema.patternProperties !== undefined) { - throw new errors.UnsupportedJSONSchemaToZuiError({ patternProperties: schema.patternProperties }) + throw new errors.UnsupportedJSONSchemaToZuiError({ patternProperties: schema.patternProperties }, path.toString()) } if (schema.propertyNames !== undefined) { - throw new errors.UnsupportedJSONSchemaToZuiError({ propertyNames: schema.propertyNames }) + throw new errors.UnsupportedJSONSchemaToZuiError({ propertyNames: schema.propertyNames }, path.toString()) } if (schema.if !== undefined) { - throw new errors.UnsupportedJSONSchemaToZuiError({ if: schema.if }) + throw new errors.UnsupportedJSONSchemaToZuiError({ if: schema.if }, path.toString()) } if (schema.then !== undefined) { - throw new errors.UnsupportedJSONSchemaToZuiError({ then: schema.then }) + throw new errors.UnsupportedJSONSchemaToZuiError({ then: schema.then }, path.toString()) } if (schema.else !== undefined) { - throw new errors.UnsupportedJSONSchemaToZuiError({ else: schema.else }) + throw new errors.UnsupportedJSONSchemaToZuiError({ else: schema.else }, path.toString()) } if (schema.$ref !== undefined) { @@ -74,7 +75,7 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { if (schema.not === true) { return z.never() } - throw new errors.UnsupportedJSONSchemaToZuiError({ not: schema.not }) + throw new errors.UnsupportedJSONSchemaToZuiError({ not: schema.not }, path.toString()) } if (Array.isArray(schema.type)) { @@ -82,10 +83,12 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { return DEFAULT_TYPE } if (schema.type.length === 1) { - return _fromJSONSchema({ ...schema, type: schema.type[0] }) + return _fromJSONSchema({ ...schema, type: schema.type[0] }, path) } const { type: _, ...tmp } = schema - const types = schema.type.map((t) => _fromJSONSchema({ ...tmp, type: t })) as [z.ZodType, z.ZodType, ...z.ZodType[]] + const types = schema.type.map((t, index) => + _fromJSONSchema({ ...tmp, type: t }, path.withIndexType('number', index)) + ) as [z.ZodType, z.ZodType, ...z.ZodType[]] return z.union(types) } @@ -93,11 +96,11 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { if (schema.enum && schema.enum.length > 0) { return z.enum(schema.enum as [string, ...string[]]) } - return toZuiPrimitive('string', schema) + return toZuiPrimitive('string', schema, path) } if (schema.type === 'integer') { - const zSchema = toZuiPrimitive('number', schema) + const zSchema = toZuiPrimitive('number', schema, path) if (zSchema.typeName === 'ZodNumber') { return zSchema.int() } @@ -106,32 +109,32 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } if (schema.type === 'number') { - return toZuiPrimitive('number', schema) + return toZuiPrimitive('number', schema, path) } if (schema.type === 'boolean') { - return toZuiPrimitive('boolean', schema) + return toZuiPrimitive('boolean', schema, path) } if (schema.type === 'null') { - return toZuiPrimitive('null', schema) + return toZuiPrimitive('null', schema, path) } if (schema.type === 'array') { - return arrayJSONSchemaToZuiArray(schema as ArraySchema | TupleSchema | SetSchema, _fromJSONSchema) + return arrayJSONSchemaToZuiArray(schema as ArraySchema | TupleSchema | SetSchema, _fromJSONSchema, path) } if (schema.type === 'object') { if (schema.additionalProperties !== undefined && schema.properties !== undefined) { - const catchAll = _fromJSONSchema(schema.additionalProperties) - const inner = _fromJSONSchema({ ...schema, additionalProperties: undefined }) as z.ZodObject + const catchAll = _fromJSONSchema(schema.additionalProperties, path.withIndexType('string')) + const inner = _fromJSONSchema({ ...schema, additionalProperties: undefined }, path) as z.ZodObject return inner.catchall(catchAll) } if (schema.properties !== undefined) { const properties: Record = {} for (const [key, value] of Object.entries(schema.properties)) { - const mapped: z.ZodType = _fromJSONSchema(value) + const mapped: z.ZodType = _fromJSONSchema(value, path.withIndexType('key', key)) const required: string[] = schema.required ?? [] // If the property is already optional (e.g., has a default value), don't wrap it again properties[key] = required.includes(key) ? mapped : mapped.isOptional() ? mapped : mapped.optional() @@ -140,7 +143,7 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } if (schema.additionalProperties !== undefined) { - const inner = _fromJSONSchema(schema.additionalProperties) + const inner = _fromJSONSchema(schema.additionalProperties, path.withIndexType('string')) return z.record(inner) } @@ -153,22 +156,22 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } if (schema.anyOf.length === 1) { - return _fromJSONSchema(schema.anyOf[0]) + return _fromJSONSchema(schema.anyOf[0], path) } if (guards.isOptionalSchema(schema)) { - const inner = _fromJSONSchema(schema.anyOf[0]) + const inner = _fromJSONSchema(schema.anyOf[0], path) return inner.optional() } if (guards.isNullableSchema(schema)) { - const inner = _fromJSONSchema(schema.anyOf[0]) + const inner = _fromJSONSchema(schema.anyOf[0], path) return inner.nullable() } if (guards.isDiscriminatedUnionSchema(schema) && schema['x-zui']?.def?.discriminator) { const { discriminator } = schema['x-zui'].def - const options = schema.anyOf.map(_fromJSONSchema) as [ + const options = schema.anyOf.map((s, index) => _fromJSONSchema(s, path.withIndexType('number', index))) as [ z.ZodDiscriminatedUnionOption, z.ZodDiscriminatedUnionOption, ...z.ZodDiscriminatedUnionOption[], @@ -176,7 +179,11 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { return z.discriminatedUnion(discriminator, options) } - const options = schema.anyOf.map(_fromJSONSchema) as [z.ZodType, z.ZodType, ...z.ZodType[]] + const options = schema.anyOf.map((s, index) => _fromJSONSchema(s, path.withIndexType('number', index))) as [ + z.ZodType, + z.ZodType, + ...z.ZodType[], + ] return z.union(options) } @@ -186,13 +193,13 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } if (schema.oneOf.length === 1) { - return _fromJSONSchema(schema.oneOf[0]) + return _fromJSONSchema(schema.oneOf[0], path) } if (guards.isExclusiveDiscriminatedUnionSchema(schema)) { const discriminator = schema.discriminator?.propertyName || schema['x-zui']?.def?.discriminator if (discriminator) { - const options = schema.oneOf.map(_fromJSONSchema) as [ + const options = schema.oneOf.map((s, index) => _fromJSONSchema(s, path.withIndexType('number', index))) as [ z.ZodDiscriminatedUnionOption, z.ZodDiscriminatedUnionOption, ...z.ZodDiscriminatedUnionOption[], @@ -201,7 +208,11 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { } } - const options = schema.oneOf.map(_fromJSONSchema) as [z.ZodType, z.ZodType, ...z.ZodType[]] + const options = schema.oneOf.map((s, index) => _fromJSONSchema(s, path.withIndexType('number', index))) as [ + z.ZodType, + z.ZodType, + ...z.ZodType[], + ] return z.union(options) } @@ -210,12 +221,13 @@ function _fromJSONSchema(schema: JSONSchema7Definition | undefined): z.ZodType { return DEFAULT_TYPE } if (schema.allOf.length === 1) { - return _fromJSONSchema(schema.allOf[0]) + return _fromJSONSchema(schema.allOf[0], path) } - const [left, ...right] = schema.allOf as [JSONSchema7, ...JSONSchema7[]] - const zLeft = _fromJSONSchema(left) - const zRight = _fromJSONSchema({ allOf: right }) - return z.intersection(zLeft, zRight) + const schemas = schema.allOf as [JSONSchema7, JSONSchema7, ...JSONSchema7[]] + const zSchemas = schemas.map((s, index) => _fromJSONSchema(s, path.withIndexType('number', index))) + return zSchemas + .slice(2) + .reduce((acc, s) => z.intersection(acc, s), z.intersection(zSchemas[0]!, zSchemas[1]!)) } schema.type satisfies undefined diff --git a/packages/zui/src/transforms/zui-from-json-schema/iterables/array.ts b/packages/zui/src/transforms/zui-from-json-schema/iterables/array.ts index 03a7e606e41..85aba34dbf3 100644 --- a/packages/zui/src/transforms/zui-from-json-schema/iterables/array.ts +++ b/packages/zui/src/transforms/zui-from-json-schema/iterables/array.ts @@ -1,16 +1,18 @@ import { JSONSchema7Definition } from 'json-schema' +import { PropertyPath } from '../../../utils/property-path-utils' import * as z from '../../../z' import { ArraySchema, SetSchema, TupleSchema } from '../../common/json-schema' export const arrayJSONSchemaToZuiArray = ( schema: ArraySchema | SetSchema | TupleSchema, - toZui: (x: JSONSchema7Definition) => z.ZodType + toZui: (x: JSONSchema7Definition, path: PropertyPath) => z.ZodType, + path: PropertyPath ): z.ZodArray | z.ZodSet | z.ZodTuple => _isTuple(schema) - ? _handleTuple(schema, toZui) + ? _handleTuple(schema, toZui, path) : _isSet(schema) - ? _handleSet(schema, toZui) - : _handleArray(schema, toZui) + ? _handleSet(schema, toZui, path) + : _handleArray(schema, toZui, path) const _isTuple = (schema: ArraySchema | SetSchema | TupleSchema): schema is TupleSchema => Array.isArray(schema.items) @@ -19,13 +21,16 @@ const _isSet = (schema: ArraySchema | SetSchema | TupleSchema): schema is SetSch const _handleTuple = ( { items, additionalItems }: TupleSchema, - toZui: (x: JSONSchema7Definition) => z.ZodType + toZui: (x: JSONSchema7Definition, path: PropertyPath) => z.ZodType, + path: PropertyPath ): z.ZodTuple => { - const itemSchemas = items.map(toZui) as [] | [z.ZodType, ...z.ZodType[]] + const itemSchemas = items.map((item, index) => toZui(item, path.withIndexType('number', index))) as + | [] + | [z.ZodType, ...z.ZodType[]] let zodTuple: z.ZodTuple = z.tuple(itemSchemas) if (additionalItems !== undefined) { - zodTuple = zodTuple.rest(toZui(additionalItems)) + zodTuple = zodTuple.rest(toZui(additionalItems, path.withIndexType('number'))) } return zodTuple @@ -33,9 +38,10 @@ const _handleTuple = ( const _handleSet = ( { items, minItems, maxItems }: SetSchema, - toZui: (x: JSONSchema7Definition) => z.ZodType + toZui: (x: JSONSchema7Definition, path: PropertyPath) => z.ZodType, + path: PropertyPath ): z.ZodSet => { - let zodSet = z.set(toZui(items)) + let zodSet = z.set(toZui(items, path.withIndexType('number'))) if (minItems) { zodSet = zodSet.min(minItems) @@ -50,9 +56,10 @@ const _handleSet = ( const _handleArray = ( { minItems, maxItems, items }: ArraySchema, - toZui: (x: JSONSchema7Definition) => z.ZodType + toZui: (x: JSONSchema7Definition, path: PropertyPath) => z.ZodType, + path: PropertyPath ): z.ZodArray | z.ZodSet | z.ZodTuple => { - let zodArray = z.array(toZui(items)) + let zodArray = z.array(toZui(items, path.withIndexType('number'))) if (minItems && minItems === maxItems) { return zodArray.length(minItems) diff --git a/packages/zui/src/transforms/zui-from-json-schema/primitives/index.ts b/packages/zui/src/transforms/zui-from-json-schema/primitives/index.ts index 0702f0b32f9..60cc4ec695f 100644 --- a/packages/zui/src/transforms/zui-from-json-schema/primitives/index.ts +++ b/packages/zui/src/transforms/zui-from-json-schema/primitives/index.ts @@ -1,4 +1,5 @@ import { JSONSchema7, JSONSchema7Type } from 'json-schema' +import { PropertyPath } from '../../../utils/property-path-utils' import * as z from '../../../z' import * as errs from '../../common/errors' import { numberJSONSchemaToZuiNumber } from './number' @@ -23,7 +24,11 @@ type ReturnType = | z.ZodLiteral | z.ZodUnion<[z.ZodLiteral, ...z.ZodLiteral[]]> -export const toZuiPrimitive = (type: T, schema: JSONSchema7): ReturnType => { +export const toZuiPrimitive = ( + type: T, + schema: JSONSchema7, + path?: PropertyPath +): ReturnType => { const values: JSONSchema7Type[] = [] if (schema.enum !== undefined) { values.push(...schema.enum) @@ -53,7 +58,7 @@ export const toZuiPrimitive = (type: T, schema: JSONSche } if (!zuiPrimitive) { - throw new errs.JSONSchemaToZuiError(`Unknown primitive type: "${type}"`) + throw new errs.JSONSchemaToZuiError(`Unknown primitive type: "${type}"`, path?.toString()) } } else { if (primitiveValues.length === 1) { diff --git a/packages/zui/src/transforms/zui-from-object/index.ts b/packages/zui/src/transforms/zui-from-object/index.ts index 861f5836c93..9c3ec47a5d8 100644 --- a/packages/zui/src/transforms/zui-from-object/index.ts +++ b/packages/zui/src/transforms/zui-from-object/index.ts @@ -1,3 +1,4 @@ +import { PropertyPath } from '../../utils/property-path-utils' import * as z from '../../z' import * as errors from '../common/errors' @@ -15,8 +16,12 @@ export type ObjectToZuiOptions = { optional?: boolean; nullable?: boolean; passt * @returns A Zod schema representing the object. */ export const fromObject = (obj: object, opts?: ObjectToZuiOptions, isRoot = true): z.ZodType => { + return _fromObject(obj, new PropertyPath(), opts, isRoot) +} + +const _fromObject = (obj: object, path: PropertyPath, opts?: ObjectToZuiOptions, isRoot = true): z.ZodType => { if (typeof obj !== 'object') { - throw new errors.ObjectToZuiError('Input must be an object') + throw new errors.ObjectToZuiError('Input must be an object', path.toString()) } const applyOptions = (zodType: z.ZodType) => { @@ -34,6 +39,7 @@ export const fromObject = (obj: object, opts?: ObjectToZuiOptions, isRoot = true } const schema: z.ZodRawShape = Object.entries(obj).reduce((acc: z.ZodRawShape, [key, value]: [string, unknown]) => { + const _path = path.withIndexType('key', key) if (value === null) { acc[key] = applyOptions(z.null()) } else { @@ -53,17 +59,17 @@ export const fromObject = (obj: object, opts?: ObjectToZuiOptions, isRoot = true if (first === undefined || first === null) { acc[key] = applyOptions(z.array(z.unknown())) } else if (typeof first === 'object') { - acc[key] = applyOptions(z.array(fromObject(first, opts, false))) + acc[key] = applyOptions(z.array(_fromObject(first, _path.withIndexType('number'), opts, false))) } else if (typeof first === 'string' || typeof first === 'number' || typeof first === 'boolean') { const inner = _getInnerType(first) acc[key] = applyOptions(z.array(inner)) } } else { - acc[key] = applyOptions(fromObject(value, opts, false)) + acc[key] = applyOptions(_fromObject(value, _path, opts, false)) } break default: - throw new errors.ObjectToZuiError(`Unsupported type for key ${key}`) + throw new errors.ObjectToZuiError(`Unsupported type for key ${key}`, _path.toString()) } } return acc diff --git a/packages/zui/src/transforms/zui-from-object/object-to-zui.test.ts b/packages/zui/src/transforms/zui-from-object/object-to-zui.test.ts index 03bac6309fd..c2dfa464d68 100644 --- a/packages/zui/src/transforms/zui-from-object/object-to-zui.test.ts +++ b/packages/zui/src/transforms/zui-from-object/object-to-zui.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' import { fromObject } from '.' +import * as errs from '../common/errors' import { toJSONSchemaLegacy } from '../zui-to-json-schema-legacy' import { JSONSchema7, JSONSchema7Definition } from 'json-schema' import { fromJSONSchemaLegacy } from '../zui-from-json-schema-legacy' @@ -215,4 +216,28 @@ describe('object-to-zui', () => { expect(testSchema?.properties?.output).toHaveProperty('additionalProperties', true) expect(fixedSchema).toHaveProperty('additionalProperties', true) }) + + test('should add object keys to path', () => { + try { + fromObject({ + foo: { bar: Symbol('x') as unknown as string }, + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo.bar') + } + }) + + test('should add [number] to path for array item error', () => { + try { + fromObject({ + foo: [{ bar: Symbol('x') as unknown as string }], + }) + expect.fail('should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(errs.ZuiTransformError) + expect((e as errs.ZuiTransformError).path).toBe('#.foo[number].bar') + } + }) }) From b2684dae9a7773b2f6d518298dd4c4a66a4007c8 Mon Sep 17 00:00:00 2001 From: Sergei Kofman <52934469+sergeikofman444@users.noreply.github.com> Date: Fri, 29 May 2026 14:14:18 -0400 Subject: [PATCH 2/4] add n8n hub category (#15236) --- integrations/n8n/integration.definition.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/integrations/n8n/integration.definition.ts b/integrations/n8n/integration.definition.ts index 1b55e4cefb2..6382f361ee7 100644 --- a/integrations/n8n/integration.definition.ts +++ b/integrations/n8n/integration.definition.ts @@ -4,7 +4,7 @@ export default new IntegrationDefinition({ name: 'n8n', title: 'n8n', description: 'This integration allows you to interact with n8n workflows.', - version: '0.1.0', + version: '0.1.1', readme: 'hub.md', icon: 'icon.svg', configuration: { @@ -126,4 +126,8 @@ export default new IntegrationDefinition({ }), }, }, + attributes: { + category: 'Developer Tools', + repo: 'botpress', + }, }) From e60ac6ce49c9072e4d9c46a09ef9cd0ff1823809 Mon Sep 17 00:00:00 2001 From: Sergei Kofman <52934469+sergeikofman444@users.noreply.github.com> Date: Fri, 29 May 2026 14:46:18 -0400 Subject: [PATCH 3/4] add category (#15238) --- integrations/sharepoint/integration.definition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/sharepoint/integration.definition.ts b/integrations/sharepoint/integration.definition.ts index f167e592676..55af312e98e 100644 --- a/integrations/sharepoint/integration.definition.ts +++ b/integrations/sharepoint/integration.definition.ts @@ -4,7 +4,7 @@ import { actions, configuration, states } from './definitions' export default new sdk.IntegrationDefinition({ name: 'sharepoint', - version: '1.0.0', + version: '1.0.1', title: 'SharePoint', description: 'Sync SharePoint document libraries with Botpress knowledge bases.', readme: 'hub.md', @@ -13,7 +13,7 @@ export default new sdk.IntegrationDefinition({ states, actions, attributes: { - category: 'Knowledge Base', + category: 'File Management', repo: 'botpress', }, }).extend(filesReadonly, ({}) => ({ From fffb68ff279ca206226b33cb1dfa20427e65d597 Mon Sep 17 00:00:00 2001 From: DubeSebastien90 <100394271+DubeSebastien90@users.noreply.github.com> Date: Fri, 29 May 2026 16:27:16 -0400 Subject: [PATCH 4/4] feat(CLI): Path information on zod to json schema error (#15237) Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/cli/src/api/bot-body.ts | 79 ++++++--- packages/cli/src/api/integration-body.ts | 165 +++++++++++------- packages/cli/src/api/interface-body.ts | 139 +++++++++------ packages/cli/src/api/plugin-body.ts | 156 ++++++++++------- .../project-command.ts | 49 ++++-- 5 files changed, 359 insertions(+), 229 deletions(-) diff --git a/packages/cli/src/api/bot-body.ts b/packages/cli/src/api/bot-body.ts index 3b9b801f291..241239ac22e 100644 --- a/packages/cli/src/api/bot-body.ts +++ b/packages/cli/src/api/bot-body.ts @@ -1,5 +1,6 @@ import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' +import * as errors from '../errors' import * as utils from '../utils' import * as types from './types' @@ -9,50 +10,82 @@ export const prepareCreateBotBody = async (bot: sdk.BotDefinition): Promise ({ + ? await utils.records.mapValuesAsync(bot.actions, async (action, actionName) => ({ ...action, input: { ...action.input, - schema: await utils.schema.mapZodToJsonSchema(action.input, { - useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(action.input, { + useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for bot action ${actionName} input` + ) + }), }, output: { ...action.output, - schema: await utils.schema.mapZodToJsonSchema(action.output, { - useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(action.output, { + useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for bot action ${actionName} output` + ) + }), }, })) : undefined, configuration: bot.configuration ? { ...bot.configuration, - schema: await utils.schema.mapZodToJsonSchema(bot.configuration, { - useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(bot.configuration, { + useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, 'Failed to convert ZUI to JSON schema for bot configuration') + }), } : undefined, events: bot.events - ? await utils.records.mapValuesAsync(bot.events, async (event) => ({ + ? await utils.records.mapValuesAsync(bot.events, async (event, eventName) => ({ ...event, - schema: await utils.schema.mapZodToJsonSchema(event, { - useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(event, { + useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for bot event ${eventName}` + ) + }), })) : undefined, states: bot.states ? (utils.records.filterValues( - await utils.records.mapValuesAsync(bot.states, async (state) => ({ + await utils.records.mapValuesAsync(bot.states, async (state, stateName) => ({ ...state, - schema: await utils.schema.mapZodToJsonSchema(state, { - useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(state, { + useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for bot state ${stateName}` + ) + }), })), ({ type }) => type !== 'workflow' ) as types.CreateBotRequestBody['states']) diff --git a/packages/cli/src/api/integration-body.ts b/packages/cli/src/api/integration-body.ts index 4a2286cc72a..cb77985e81d 100644 --- a/packages/cli/src/api/integration-body.ts +++ b/packages/cli/src/api/integration-body.ts @@ -1,77 +1,108 @@ import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' +import * as errors from '../errors' import * as utils from '../utils' import * as types from './types' export const prepareCreateIntegrationBody = async ( integration: sdk.IntegrationDefinition -): Promise => ({ - name: integration.name, - version: integration.version, - title: 'title' in integration ? integration.title : undefined, - description: 'description' in integration ? integration.description : undefined, - user: integration.user, - events: integration.events - ? await utils.records.mapValuesAsync(integration.events, async (event) => ({ - ...event, - schema: await utils.schema.mapZodToJsonSchema(event, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - })) - : undefined, - actions: integration.actions - ? await utils.records.mapValuesAsync(integration.actions, async (action) => ({ - ...action, - input: { - ...action.input, - schema: await utils.schema.mapZodToJsonSchema(action.input, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - }, - output: { - ...action.output, - schema: await utils.schema.mapZodToJsonSchema(action.output, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - }, - })) - : undefined, - channels: integration.channels - ? await utils.records.mapValuesAsync(integration.channels, async (channel) => ({ - ...channel, - messages: await utils.records.mapValuesAsync(channel.messages, async (message) => ({ - ...message, - schema: await utils.schema.mapZodToJsonSchema(message, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - })), - })) - : undefined, - states: integration.states - ? await utils.records.mapValuesAsync(integration.states, async (state) => ({ - ...state, - schema: await utils.schema.mapZodToJsonSchema(state, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - })) - : undefined, - entities: integration.entities - ? await utils.records.mapValuesAsync(integration.entities, async (entity) => ({ - ...entity, - schema: await utils.schema.mapZodToJsonSchema(entity, { - useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, - }), - })) - : undefined, - attributes: integration.attributes, - extraOperations: '__advanced' in integration ? integration.__advanced?.extraOperations : undefined, -}) +): Promise => { + const base = `Failed to convert ZUI to JSON schema for integration ${integration.name}` + return { + name: integration.name, + version: integration.version, + title: 'title' in integration ? integration.title : undefined, + description: 'description' in integration ? integration.description : undefined, + user: integration.user, + events: integration.events + ? await utils.records.mapValuesAsync(integration.events, async (event, eventName) => ({ + ...event, + schema: await utils.schema + .mapZodToJsonSchema(event, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`) + }), + })) + : undefined, + actions: integration.actions + ? await utils.records.mapValuesAsync(integration.actions, async (action, actionName) => ({ + ...action, + input: { + ...action.input, + schema: await utils.schema + .mapZodToJsonSchema(action.input, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`) + }), + }, + output: { + ...action.output, + schema: await utils.schema + .mapZodToJsonSchema(action.output, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`) + }), + }, + })) + : undefined, + channels: integration.channels + ? await utils.records.mapValuesAsync(integration.channels, async (channel, channelName) => ({ + ...channel, + messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({ + ...message, + schema: await utils.schema + .mapZodToJsonSchema(message, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `${base} for channel ${channelName} for message ${messageName}` + ) + }), + })), + })) + : undefined, + states: integration.states + ? await utils.records.mapValuesAsync(integration.states, async (state, stateName) => ({ + ...state, + schema: await utils.schema + .mapZodToJsonSchema(state, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`) + }), + })) + : undefined, + entities: integration.entities + ? await utils.records.mapValuesAsync(integration.entities, async (entity, entityName) => ({ + ...entity, + schema: await utils.schema + .mapZodToJsonSchema(entity, { + useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`) + }), + })) + : undefined, + attributes: integration.attributes, + extraOperations: '__advanced' in integration ? integration.__advanced?.extraOperations : undefined, + } +} type UpdateIntegrationChannelsBody = NonNullable type UpdateIntegrationChannelBody = UpdateIntegrationChannelsBody[string] diff --git a/packages/cli/src/api/interface-body.ts b/packages/cli/src/api/interface-body.ts index e8e3139481d..44680655dfc 100644 --- a/packages/cli/src/api/interface-body.ts +++ b/packages/cli/src/api/interface-body.ts @@ -1,66 +1,93 @@ import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' +import * as errors from '../errors' import * as utils from '../utils' import * as types from './types' export const prepareCreateInterfaceBody = async ( intrface: sdk.InterfaceDefinition -): Promise => ({ - name: intrface.name, - version: intrface.version, - title: 'title' in intrface ? intrface.title : undefined, - description: 'description' in intrface ? intrface.description : undefined, - entities: intrface.entities - ? await utils.records.mapValuesAsync(intrface.entities, async (entity) => ({ - ...entity, - schema: await utils.schema.mapZodToJsonSchema(entity, { - useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, - }), - })) - : {}, - events: intrface.events - ? await utils.records.mapValuesAsync(intrface.events, async (event) => ({ - ...event, - schema: await utils.schema.mapZodToJsonSchema(event, { - useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, - }), - })) - : {}, - actions: intrface.actions - ? await utils.records.mapValuesAsync(intrface.actions, async (action) => ({ - ...action, - input: { - ...action.input, - schema: await utils.schema.mapZodToJsonSchema(action.input, { - useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, - }), - }, - output: { - ...action.output, - schema: await utils.schema.mapZodToJsonSchema(action.output, { - useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, - }), - }, - })) - : {}, - channels: intrface.channels - ? await utils.records.mapValuesAsync(intrface.channels, async (channel) => ({ - ...channel, - messages: await utils.records.mapValuesAsync(channel.messages, async (message) => ({ - ...message, - schema: await utils.schema.mapZodToJsonSchema(message, { - useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, - }), - })), - })) - : {}, - attributes: intrface.attributes, -}) +): Promise => { + const base = `Failed to convert ZUI to JSON schema for interface ${intrface.name}` + return { + name: intrface.name, + version: intrface.version, + title: 'title' in intrface ? intrface.title : undefined, + description: 'description' in intrface ? intrface.description : undefined, + entities: intrface.entities + ? await utils.records.mapValuesAsync(intrface.entities, async (entity, entityName) => ({ + ...entity, + schema: await utils.schema + .mapZodToJsonSchema(entity, { + useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`) + }), + })) + : {}, + events: intrface.events + ? await utils.records.mapValuesAsync(intrface.events, async (event, eventName) => ({ + ...event, + schema: await utils.schema + .mapZodToJsonSchema(event, { + useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`) + }), + })) + : {}, + actions: intrface.actions + ? await utils.records.mapValuesAsync(intrface.actions, async (action, actionName) => ({ + ...action, + input: { + ...action.input, + schema: await utils.schema + .mapZodToJsonSchema(action.input, { + useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`) + }), + }, + output: { + ...action.output, + schema: await utils.schema + .mapZodToJsonSchema(action.output, { + useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`) + }), + }, + })) + : {}, + channels: intrface.channels + ? await utils.records.mapValuesAsync(intrface.channels, async (channel, channelName) => ({ + ...channel, + messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({ + ...message, + schema: await utils.schema + .mapZodToJsonSchema(message, { + useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `${base} for channel ${channelName} for message ${messageName}` + ) + }), + })), + })) + : {}, + attributes: intrface.attributes, + } +} export const prepareUpdateInterfaceBody = ( localInterface: types.CreateInterfaceRequestBody & { id: string }, diff --git a/packages/cli/src/api/plugin-body.ts b/packages/cli/src/api/plugin-body.ts index 5a1b0e3c9f4..1c239616c86 100644 --- a/packages/cli/src/api/plugin-body.ts +++ b/packages/cli/src/api/plugin-body.ts @@ -1,75 +1,97 @@ import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' +import * as errors from '../errors' import * as utils from '../utils' import * as types from './types' -export const prepareCreatePluginBody = async ( - plugin: sdk.PluginDefinition -): Promise => ({ - name: plugin.name, - version: plugin.version, - title: 'title' in plugin ? plugin.title : undefined, - description: 'description' in plugin ? plugin.description : undefined, - user: { - tags: plugin.user?.tags ?? {}, - }, - conversation: { - tags: plugin.conversation?.tags ?? {}, - }, - message: { - tags: plugin.message?.tags ?? {}, - }, - configuration: plugin.configuration - ? { - ...plugin.configuration, - schema: await utils.schema.mapZodToJsonSchema(plugin.configuration, { - useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, - }), - } - : undefined, - events: plugin.events - ? await utils.records.mapValuesAsync(plugin.events, async (event) => ({ - ...event, - schema: await utils.schema.mapZodToJsonSchema(event, { - useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, - }), - })) - : undefined, - actions: plugin.actions - ? await utils.records.mapValuesAsync(plugin.actions, async (action) => ({ - ...action, - input: { - ...action.input, - schema: await utils.schema.mapZodToJsonSchema(action.input, { - useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, - }), - }, - output: { - ...action.output, - schema: await utils.schema.mapZodToJsonSchema(action.output, { - useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, - }), - }, - })) - : undefined, - states: plugin.states - ? (utils.records.filterValues( - await utils.records.mapValuesAsync(plugin.states, async (state) => ({ - ...state, - schema: await utils.schema.mapZodToJsonSchema(state, { - useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, - }), - })), - ({ type }) => type !== 'workflow' - ) as types.CreatePluginRequestBody['states']) - : undefined, - attributes: plugin.attributes, -}) +export const prepareCreatePluginBody = async (plugin: sdk.PluginDefinition): Promise => { + const base = `Failed to convert ZUI to JSON schema for plugin ${plugin.name}` + return { + name: plugin.name, + version: plugin.version, + title: 'title' in plugin ? plugin.title : undefined, + description: 'description' in plugin ? plugin.description : undefined, + user: { + tags: plugin.user?.tags ?? {}, + }, + conversation: { + tags: plugin.conversation?.tags ?? {}, + }, + message: { + tags: plugin.message?.tags ?? {}, + }, + configuration: plugin.configuration + ? { + ...plugin.configuration, + schema: await utils.schema + .mapZodToJsonSchema(plugin.configuration, { + useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for configuration`) + }), + } + : undefined, + events: plugin.events + ? await utils.records.mapValuesAsync(plugin.events, async (event, eventName) => ({ + ...event, + schema: await utils.schema + .mapZodToJsonSchema(event, { + useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`) + }), + })) + : undefined, + actions: plugin.actions + ? await utils.records.mapValuesAsync(plugin.actions, async (action, actionName) => ({ + ...action, + input: { + ...action.input, + schema: await utils.schema + .mapZodToJsonSchema(action.input, { + useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`) + }), + }, + output: { + ...action.output, + schema: await utils.schema + .mapZodToJsonSchema(action.output, { + useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`) + }), + }, + })) + : undefined, + states: plugin.states + ? (utils.records.filterValues( + await utils.records.mapValuesAsync(plugin.states, async (state, stateName) => ({ + ...state, + schema: await utils.schema + .mapZodToJsonSchema(state, { + useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`) + }), + })), + ({ type }) => type !== 'workflow' + ) as types.CreatePluginRequestBody['states']) + : undefined, + attributes: plugin.attributes, + } +} export const prepareUpdatePluginBody = ( localPlugin: types.UpdatePluginRequestBody, diff --git a/packages/cli/src/command-implementations/project-command.ts b/packages/cli/src/command-implementations/project-command.ts index faaff6938cf..7d176f6aa07 100644 --- a/packages/cli/src/command-implementations/project-command.ts +++ b/packages/cli/src/command-implementations/project-command.ts @@ -579,10 +579,17 @@ export abstract class ProjectCommand extends }, configuration: integrationDef.configuration ? { - schema: await utils.schema.mapZodToJsonSchema(integrationDef.configuration, { - useLegacyZuiTransformer: integrationDef.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integrationDef.__advanced?.toJSONSchemaOptions, - }), + schema: await utils.schema + .mapZodToJsonSchema(integrationDef.configuration, { + useLegacyZuiTransformer: integrationDef.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integrationDef.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for integration ${integrationDef.name} for configuration` + ) + }), identifier: { required: integrationDef.configuration.identifier?.required, linkTemplateScript: await this.readProjectFile( @@ -592,18 +599,28 @@ export abstract class ProjectCommand extends } : undefined, configurations: integrationDef.configurations - ? await utils.records.mapValuesAsync(integrationDef.configurations, async (configuration) => ({ - title: configuration.title, - description: configuration.description, - schema: await utils.schema.mapZodToJsonSchema(configuration, { - useLegacyZuiTransformer: integrationDef.__advanced?.useLegacyZuiTransformer, - toJSONSchemaOptions: integrationDef.__advanced?.toJSONSchemaOptions, - }), - identifier: { - required: configuration.identifier?.required, - linkTemplateScript: await this.readProjectFile(configuration.identifier?.linkTemplateScript), - }, - })) + ? await utils.records.mapValuesAsync( + integrationDef.configurations, + async (configuration, configurationName) => ({ + title: configuration.title, + description: configuration.description, + schema: await utils.schema + .mapZodToJsonSchema(configuration, { + useLegacyZuiTransformer: integrationDef.__advanced?.useLegacyZuiTransformer, + toJSONSchemaOptions: integrationDef.__advanced?.toJSONSchemaOptions, + }) + .catch((thrown) => { + throw errors.BotpressCLIError.wrap( + thrown, + `Failed to convert ZUI to JSON schema for integration ${integrationDef.name} for configuration ${configurationName}` + ) + }), + identifier: { + required: configuration.identifier?.required, + linkTemplateScript: await this.readProjectFile(configuration.identifier?.linkTemplateScript), + }, + }) + ) : undefined, } }