mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
905 lines
20 KiB
TypeScript
905 lines
20 KiB
TypeScript
/* 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<z.ZodError[]>(
|
|
(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<z.ZodError[]>(
|
|
(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);
|
|
});
|
|
});
|