From b86dd5ac55b8401c4ffd27b079425ef02c51164f Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 13 Mar 2026 15:29:04 +0000 Subject: [PATCH 1/9] feat: variables in nested records and arrays --- .../api/src/parsers/attachVariableResolver.ts | 73 ++++++++++++++--- packages/api/test/Data.test.ts | 53 ++++++++++++- .../api/test/__snapshots__/Data.test.ts.snap | 78 +++++++++++++++++++ 3 files changed, 191 insertions(+), 13 deletions(-) diff --git a/packages/api/src/parsers/attachVariableResolver.ts b/packages/api/src/parsers/attachVariableResolver.ts index 239790397..2cded4f08 100644 --- a/packages/api/src/parsers/attachVariableResolver.ts +++ b/packages/api/src/parsers/attachVariableResolver.ts @@ -1,6 +1,8 @@ import { + array, type output, pipe, + record, safeExtend, transform, type ZodMiniAny, @@ -15,7 +17,7 @@ import { type VariableStringParser, variableStringParser, } from './DataItemVariableResolver.ts' -import type { $ZodObject, $ZodType } from 'zod/v4/core' +import type { $ZodArray, $ZodObject, $ZodRecord, $ZodType } from 'zod/v4/core' import { any, type ZodObject } from 'zod' import { type ZodSwitch, zodSwitch } from './switch.ts' import type { VariablesRegistry } from './variablesRegistry.ts' @@ -33,6 +35,18 @@ export function attachVariableResolver< parser as unknown as (ZodObject | ZodMiniObject), ) as any + case 'record': + return recordVariableResolverParser( + variablesRegistry, + parser as unknown as $ZodRecord, + ) as RecursiveVariableResolver + + case 'array': + return arrayVariableResolverParser( + variablesRegistry, + parser as unknown as $ZodArray, + ) as RecursiveVariableResolver + default: return variableResolverParser( variablesRegistry, @@ -68,6 +82,35 @@ function variableResolverParser< ])) as WithVariableResolver } +function arrayVariableResolverParser( + variablesRegistry: VariablesRegistry, + arrayParser: $ZodArray, +): RecursiveVariableResolver<$ZodArray> { + const $arrayParser = array( + attachVariableResolver(variablesRegistry, arrayParser._zod.def.element), + ) + + return variableResolverParser( + variablesRegistry, + $arrayParser, + ) as RecursiveVariableResolver<$ZodArray> +} + +function recordVariableResolverParser( + variablesRegistry: VariablesRegistry, + recordParser: $ZodRecord, +): RecursiveVariableResolver<$ZodRecord> { + const $recordParser = record( + recordParser._zod.def.keyType as any, + attachVariableResolver(variablesRegistry, recordParser._zod.def.valueType), + ) + + return variableResolverParser( + variablesRegistry, + $recordParser, + ) as RecursiveVariableResolver<$ZodRecord> +} + function objectVariableResolverParser< Parser extends ZodObject | ZodMiniObject, >( @@ -109,16 +152,28 @@ type WithVariableResolver< export type RecursiveVariableResolver< Parser extends $ZodType, -> = Parser extends $ZodObject ? WithVariableResolver< - $ZodObject< - { - [K in keyof Parser['_zod']['def']['shape']]: RecursiveVariableResolver< - Parser['_zod']['def']['shape'][K] - > - }, - ZodObjectConfig +> = Parser extends $ZodArray ? WithVariableResolver< + $ZodArray< + RecursiveVariableResolver > > + : Parser extends $ZodRecord ? WithVariableResolver< + $ZodRecord< + Parser['_zod']['def']['keyType'], + RecursiveVariableResolver + > + > + : Parser extends $ZodObject ? WithVariableResolver< + $ZodObject< + { + [K in keyof Parser['_zod']['def']['shape']]: + RecursiveVariableResolver< + Parser['_zod']['def']['shape'][K] + > + }, + ZodObjectConfig + > + > : WithVariableResolver type ZodObjectConfig = T extends $ZodObject diff --git a/packages/api/test/Data.test.ts b/packages/api/test/Data.test.ts index a93e0d109..1627bef3a 100644 --- a/packages/api/test/Data.test.ts +++ b/packages/api/test/Data.test.ts @@ -589,9 +589,9 @@ Deno.test('variables', async (t) => { ], }) - assertSnapshot(t, await data.getPayload('foo', { channel: 'bar' })) + await assertSnapshot(t, await data.getPayload('foo', { channel: 'bar' })) - assertSnapshot(t, await data.getPayload('foo')) + await assertSnapshot(t, await data.getPayload('foo')) }) Deno.test('variables using fallthrough targeting', async (t) => { @@ -646,7 +646,52 @@ Deno.test('variables using fallthrough targeting', async (t) => { ], }) - assertSnapshot(t, await data.getPayload('foo', { channel: 'bar' })) + await assertSnapshot(t, await data.getPayload('foo', { channel: 'bar' })) +}) + +Deno.test('variables in records', async (t) => { + const data = await Data.create() + .usePayload({ + foo: z.record(z.string(), z.array(z.number())), + }) + .addRules('foo', { + variables: { + a: [{ payload: [1, 2, 3] }], + }, + rules: [{ payload: { a: '{{a}}' } }], + }) + + await assertSnapshot( + t, + data.data, + ) +}) + +Deno.test('variables in arrays', async (t) => { + const data = await Data.create() + .usePayload({ + foo: z.array(z.number()), + bar: z.array(z.strictObject({ + b: z.number(), + c: z.string(), + })), + }).addRules('foo', { + variables: { + a: [{ payload: 1 }], + }, + rules: [{ payload: ['{{a}}'] }], + }).addRules('bar', { + variables: { + b: [{ payload: 2 }], + c: [{ payload: '3' }], + }, + rules: [{ payload: [{ b: '{{b}}', c: '{{c}}' }] }], + }) + + await assertSnapshot( + t, + data.data, + ) }) Deno.test('errors when using variables with incorrect types', async (t) => { @@ -661,7 +706,7 @@ Deno.test('errors when using variables with incorrect types', async (t) => { }), }) - assertSnapshot( + await assertSnapshot( t, await data.addRules('foo', { variables: { diff --git a/packages/api/test/__snapshots__/Data.test.ts.snap b/packages/api/test/__snapshots__/Data.test.ts.snap index b2f4c9533..2ff0c3ff4 100644 --- a/packages/api/test/__snapshots__/Data.test.ts.snap +++ b/packages/api/test/__snapshots__/Data.test.ts.snap @@ -377,6 +377,84 @@ snapshot[`variables using fallthrough targeting 1`] = ` } `; +snapshot[`variables in records 1`] = ` +{ + foo: { + rules: [ + { + payload: { + a: [Function: resolver] { + "\$\$resolver\$\$": true, + }, + }, + }, + ], + variables: { + a: [ + { + payload: [ + 1, + 2, + 3, + ], + }, + ], + }, + }, +} +`; + +snapshot[`variables in arrays 1`] = ` +{ + bar: { + rules: [ + { + payload: [ + { + b: [Function: resolver] { + "\$\$resolver\$\$": true, + }, + c: [Function: resolver] { + "\$\$resolver\$\$": true, + }, + }, + ], + }, + ], + variables: { + b: [ + { + payload: 2, + }, + ], + c: [ + { + payload: "3", + }, + ], + }, + }, + foo: { + rules: [ + { + payload: [ + [Function: resolver] { + "\$\$resolver\$\$": true, + }, + ], + }, + ], + variables: { + a: [ + { + payload: 1, + }, + ], + }, + }, +} +`; + snapshot[`errors when using variables with incorrect types 1`] = ` [ { From a606e6bba16fdc569bc15da35c114f8a61fbcb61 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 13 Mar 2026 15:47:28 +0000 Subject: [PATCH 2/9] fix: attach variables to existing schemas --- .../api/src/parsers/attachVariableResolver.ts | 25 ++++++---- .../test/__snapshots__/index.test.ts.snap | 47 ++++++++++++++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/packages/api/src/parsers/attachVariableResolver.ts b/packages/api/src/parsers/attachVariableResolver.ts index 2cded4f08..9154c8bb0 100644 --- a/packages/api/src/parsers/attachVariableResolver.ts +++ b/packages/api/src/parsers/attachVariableResolver.ts @@ -1,8 +1,6 @@ import { - array, type output, pipe, - record, safeExtend, transform, type ZodMiniAny, @@ -17,7 +15,13 @@ import { type VariableStringParser, variableStringParser, } from './DataItemVariableResolver.ts' -import type { $ZodArray, $ZodObject, $ZodRecord, $ZodType } from 'zod/v4/core' +import { + type $ZodArray, + type $ZodObject, + type $ZodRecord, + type $ZodType, + clone, +} from 'zod/v4/core' import { any, type ZodObject } from 'zod' import { type ZodSwitch, zodSwitch } from './switch.ts' import type { VariablesRegistry } from './variablesRegistry.ts' @@ -86,10 +90,11 @@ function arrayVariableResolverParser( variablesRegistry: VariablesRegistry, arrayParser: $ZodArray, ): RecursiveVariableResolver<$ZodArray> { - const $arrayParser = array( - attachVariableResolver(variablesRegistry, arrayParser._zod.def.element), + const $arrayParser = clone(arrayParser) + $arrayParser._zod.def.element = attachVariableResolver( + variablesRegistry, + $arrayParser._zod.def.element, ) - return variableResolverParser( variablesRegistry, $arrayParser, @@ -100,11 +105,11 @@ function recordVariableResolverParser( variablesRegistry: VariablesRegistry, recordParser: $ZodRecord, ): RecursiveVariableResolver<$ZodRecord> { - const $recordParser = record( - recordParser._zod.def.keyType as any, - attachVariableResolver(variablesRegistry, recordParser._zod.def.valueType), + const $recordParser = clone(recordParser) + $recordParser._zod.def.valueType = attachVariableResolver( + variablesRegistry, + $recordParser._zod.def.valueType, ) - return variableResolverParser( variablesRegistry, $recordParser, diff --git a/packages/json-schema/test/__snapshots__/index.test.ts.snap b/packages/json-schema/test/__snapshots__/index.test.ts.snap index 78fbe6c05..65d9ea8d7 100644 --- a/packages/json-schema/test/__snapshots__/index.test.ts.snap +++ b/packages/json-schema/test/__snapshots__/index.test.ts.snap @@ -172,6 +172,51 @@ snapshot[`json schema for simple data object 1`] = ` ], }, b: { + "\$defs": { + __schema0: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + type: "string", + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, anyOf: [ { pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", @@ -179,7 +224,7 @@ snapshot[`json schema for simple data object 1`] = ` }, { items: { - type: "string", + "\$ref": "#/\$defs/__schema0", }, type: "array", }, From 59e22143c59d8e0bf7f5a5727e7e7fc7cd8fe1e8 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:10:18 +0000 Subject: [PATCH 3/9] fix: mutations of recursive variable resolvers --- .../src/parsers/DataItemVariableResolver.ts | 5 +- .../api/src/parsers/attachVariableResolver.ts | 56 +++--- packages/api/src/parsers/switch.ts | 118 +++++++----- packages/json-schema/src/index.ts | 45 ++--- .../test/__snapshots__/index.test.ts.snap | 176 +++++++++++++----- packages/json-schema/test/fixtures/data.ts | 10 +- 6 files changed, 255 insertions(+), 155 deletions(-) diff --git a/packages/api/src/parsers/DataItemVariableResolver.ts b/packages/api/src/parsers/DataItemVariableResolver.ts index d719584e3..26a9a6e7a 100644 --- a/packages/api/src/parsers/DataItemVariableResolver.ts +++ b/packages/api/src/parsers/DataItemVariableResolver.ts @@ -16,7 +16,8 @@ import type { import { objectMap } from '../util.ts' import type { VariablesRegistry } from './variablesRegistry.ts' -export const variableStringParser = templateLiteral(['{{', string(), '}}']) +export const variableStringParser = () => + templateLiteral(['{{', string(), '}}']) export type VariableStringParser = ZodMiniTemplateLiteral<`{{${string}}}`> export function DataItemVariableResolverParser( @@ -24,7 +25,7 @@ export function DataItemVariableResolverParser( parser: $ZodType, ): DataItemVariableResolverParser { return pipe( - variableStringParser, + variableStringParser(), transform((input, ctx) => stringToVariableResolver(registry, parser, input, ctx) ), diff --git a/packages/api/src/parsers/attachVariableResolver.ts b/packages/api/src/parsers/attachVariableResolver.ts index 9154c8bb0..d25e8c438 100644 --- a/packages/api/src/parsers/attachVariableResolver.ts +++ b/packages/api/src/parsers/attachVariableResolver.ts @@ -15,13 +15,7 @@ import { type VariableStringParser, variableStringParser, } from './DataItemVariableResolver.ts' -import { - type $ZodArray, - type $ZodObject, - type $ZodRecord, - type $ZodType, - clone, -} from 'zod/v4/core' +import type { $ZodArray, $ZodObject, $ZodRecord, $ZodType } from 'zod/v4/core' import { any, type ZodObject } from 'zod' import { type ZodSwitch, zodSwitch } from './switch.ts' import type { VariablesRegistry } from './variablesRegistry.ts' @@ -37,7 +31,7 @@ export function attachVariableResolver< return objectVariableResolverParser( variablesRegistry, parser as unknown as (ZodObject | ZodMiniObject), - ) as any + ) as unknown as RecursiveVariableResolver case 'record': return recordVariableResolverParser( @@ -79,41 +73,45 @@ function variableResolverParser< ) : zodSwitch([ [ - variableStringParser, + variableStringParser(), DataItemVariableResolverParser(variablesRegistry, parser), ], [any(), parser], ])) as WithVariableResolver } -function arrayVariableResolverParser( +function arrayVariableResolverParser( variablesRegistry: VariablesRegistry, - arrayParser: $ZodArray, -): RecursiveVariableResolver<$ZodArray> { - const $arrayParser = clone(arrayParser) - $arrayParser._zod.def.element = attachVariableResolver( - variablesRegistry, - $arrayParser._zod.def.element, - ) + arrayParser: Parser, +): RecursiveVariableResolver { + const $arrayParser = new arrayParser._zod.constr({ + ...arrayParser._zod.def, + element: attachVariableResolver( + variablesRegistry, + arrayParser._zod.def.element, + ), + }) return variableResolverParser( variablesRegistry, $arrayParser, - ) as RecursiveVariableResolver<$ZodArray> + ) as RecursiveVariableResolver } -function recordVariableResolverParser( +function recordVariableResolverParser( variablesRegistry: VariablesRegistry, - recordParser: $ZodRecord, -): RecursiveVariableResolver<$ZodRecord> { - const $recordParser = clone(recordParser) - $recordParser._zod.def.valueType = attachVariableResolver( - variablesRegistry, - $recordParser._zod.def.valueType, - ) + recordParser: Parser, +): RecursiveVariableResolver { + const $recordParser = new recordParser._zod.constr({ + ...recordParser._zod.def, + valueType: attachVariableResolver( + variablesRegistry, + recordParser._zod.def.valueType, + ), + }) return variableResolverParser( variablesRegistry, $recordParser, - ) as RecursiveVariableResolver<$ZodRecord> + ) as RecursiveVariableResolver } function objectVariableResolverParser< @@ -159,13 +157,13 @@ export type RecursiveVariableResolver< Parser extends $ZodType, > = Parser extends $ZodArray ? WithVariableResolver< $ZodArray< - RecursiveVariableResolver + RecursiveVariableResolver > > : Parser extends $ZodRecord ? WithVariableResolver< $ZodRecord< Parser['_zod']['def']['keyType'], - RecursiveVariableResolver + RecursiveVariableResolver > > : Parser extends $ZodObject ? WithVariableResolver< diff --git a/packages/api/src/parsers/switch.ts b/packages/api/src/parsers/switch.ts index b68fc9074..df11b02d6 100644 --- a/packages/api/src/parsers/switch.ts +++ b/packages/api/src/parsers/switch.ts @@ -1,39 +1,85 @@ +import { safeParse, union, type ZodMiniUnion } from 'zod/mini' import { - custom, - registry, - safeParse, - superRefine, - union, - type ZodMiniCustom, - type ZodMiniUnion, -} from 'zod/mini' -import type { $ZodRegistry, $ZodType } from 'zod/v4/core' + $constructor, + type $ZodCustomDef, + type $ZodCustomInternals, + $ZodType, + type $ZodType as $ZodTypeType, + NEVER, +} from 'zod/v4/core' /** * A tuple array representing switch case mappings. * Each entry is a [condition, parser] pair where the condition determines which parser to use. */ -export type $ZodSwitchMap = [condition: $ZodType, parser: $ZodType][] +export type $ZodSwitchMap = [condition: $ZodTypeType, parser: $ZodTypeType][] /** - * A Zod custom schema type that evaluates conditions and applies the corresponding parser. - * The output and input types are derived from the parser schemas in the switch map. + * Definition for a ZodSwitch schema. + * Extends the base custom def with a switchMap and precomputed union. + */ +export interface ZodSwitchDef extends $ZodCustomDef { + switchMap: $ZodSwitchMap + union: ZodMiniUnion +} + +/** + * Internal state for a ZodSwitch schema. + */ +export interface ZodSwitchInternals + extends $ZodCustomInternals { + def: ZodSwitchDef +} + +/** + * A Zod schema that evaluates conditions and applies the corresponding parser. + * The union metadata is stored directly on the instance (no external registry). * * @template SwitchMap - The switch map defining condition-parser pairs */ -export type ZodSwitch = - ZodMiniCustom< +export interface ZodSwitch + extends $ZodType { + _zod: ZodSwitchInternals< SwitchMap[number][1]['_zod']['output'], SwitchMap[number][1]['_zod']['input'] > +} + +export const ZodSwitch: $constructor = $constructor( + 'ZodSwitch', + (inst, def) => { + $ZodType.init(inst, def) + inst._zod.parse = (payload) => { + const input = payload.value + payload.value = NEVER + for (const [condition, parser] of def.switchMap) { + const conditionResult = safeParse(condition, input) + if (conditionResult.success) { + const parseResult = safeParse(parser, conditionResult.data) + if (parseResult.success) { + payload.value = parseResult.data + } else { + for (const issue of parseResult.error.issues) { + payload.issues.push({ + ...issue, + input: input as any, + }) + } + } + break + } + } + return payload + } + }, +) /** - * Registry for switch schemas that stores metadata about the union of all possible parsers. - * Used internally by zodSwitch to register schema information. + * Check if a Zod schema is a ZodSwitch instance. */ -export const switchRegistry: $ZodRegistry<{ - union: ZodMiniUnion -}, ZodSwitch<$ZodSwitchMap>> = registry() +export function isZodSwitch(parser: $ZodTypeType): parser is ZodSwitch { + return parser instanceof ZodSwitch +} /** * Create a conditional Zod parser that evaluates different schemas based on conditions. @@ -57,30 +103,12 @@ export const switchRegistry: $ZodRegistry<{ export function zodSwitch( switchMap: SwitchMap, ): ZodSwitch { - return custom< - SwitchMap[number][1]['_zod']['output'], - SwitchMap[number][1]['_zod']['input'] - >().check( - superRefine((input, payload) => { - for (const [condition, parser] of switchMap) { - const conditionResult = safeParse(condition, input) - if (conditionResult.success) { - const parseResult = safeParse(parser, conditionResult.data) - if (parseResult.success) payload.value = parseResult.data - else { - for (const issue of parseResult.error.issues) { - payload.addIssue({ - ...issue, - input: input as any, - }) - } - } - break - } - } - }), - ) - .register(switchRegistry, { - union: union(switchMap.map(([, type]) => type)) as any, - }) + return new ZodSwitch({ + type: 'custom', + check: 'custom', + fn: () => true, + abort: true, + switchMap, + union: union(switchMap.map(([, type]) => type)) as ZodMiniUnion, + }) as ZodSwitch } diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 33c36394f..314d640ca 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -3,11 +3,10 @@ import { DataItemParser, DataItemsParser, type DT, - switchRegistry, - type ZodSwitch, + isZodSwitch, } from '@targetd/api' import { extend, optional, string, toJSONSchema } from 'zod/mini' -import type { $ZodType, JSONSchema } from 'zod/v4/core' +import type { JSONSchema } from 'zod/v4/core' /** * Generate JSON Schema for all data items in a Data instance. @@ -76,44 +75,26 @@ export function dataJSONSchema( ) } -type _ToJSONSchemaParams = NonNullable[1]> +type ToJSONSchemaParams = NonNullable[1]> -export type ToJSONSchemaParams = _ToJSONSchemaParams & { - override?: ( - ...args: Parameters['override']> - ) => void | boolean -} - -function toJSONSchemaParams(params?: ToJSONSchemaParams): _ToJSONSchemaParams { +function toJSONSchemaParams(params?: ToJSONSchemaParams): ToJSONSchemaParams { return { io: 'input', reused: 'ref', unrepresentable: 'any', override(ctx) { - if (params?.override?.(ctx)) return if (isZodSwitch(ctx.zodSchema)) { - const union = switchRegistry.get(ctx.zodSchema) - ?.union - if (union) { - const schema = toJSONSchema( - union as any, - toJSONSchemaParams(params), - ) - // Mutate the jsonSchema in place - ctx.jsonSchema is a reference - // to the actual schema object, so we must modify it directly - for (const key in ctx.jsonSchema) { - delete (ctx.jsonSchema as Record)[key] - } - Object.assign(ctx.jsonSchema, schema) - delete (ctx.jsonSchema as Record).$schema - } + Object.assign( + ctx.jsonSchema, + toJSONSchema(ctx.zodSchema._zod.def.union, { + ...toJSONSchemaParams(), + reused: 'inline', + }), + ) + delete ctx.jsonSchema.$schema } + params?.override?.(ctx) }, ...(params && omit(params, ['override'])), } } - -function isZodSwitch(parser: $ZodType): parser is ZodSwitch { - return parser._zod.def.type === 'custom' && - switchRegistry.has(parser as ZodSwitch) -} diff --git a/packages/json-schema/test/__snapshots__/index.test.ts.snap b/packages/json-schema/test/__snapshots__/index.test.ts.snap index 65d9ea8d7..5577ffc9a 100644 --- a/packages/json-schema/test/__snapshots__/index.test.ts.snap +++ b/packages/json-schema/test/__snapshots__/index.test.ts.snap @@ -15,6 +15,67 @@ snapshot[`json schema for simple data object 1`] = ` }, type: "object", }, + __schema10: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, + __schema11: { + type: "string", + }, + __schema12: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + {}, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema13", + }, + { + items: { + "\$ref": "#/\$defs/__schema13", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + __schema13: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, __schema1: { items: { type: "string", @@ -172,51 +233,6 @@ snapshot[`json schema for simple data object 1`] = ` ], }, b: { - "\$defs": { - __schema0: { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - type: "string", - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - }, anyOf: [ { pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", @@ -224,7 +240,7 @@ snapshot[`json schema for simple data object 1`] = ` }, { items: { - "\$ref": "#/\$defs/__schema0", + type: "string", }, type: "array", }, @@ -275,6 +291,74 @@ snapshot[`json schema for simple data object 1`] = ` ], type: "object", }, + car: { + additionalProperties: false, + properties: { + rules: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + additionalProperties: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + type: "number", + }, + ], + }, + propertyNames: { + type: "string", + }, + type: "object", + }, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema10", + }, + { + items: { + "\$ref": "#/\$defs/__schema10", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + variables: { + additionalProperties: { + "\$ref": "#/\$defs/__schema12", + }, + propertyNames: { + "\$ref": "#/\$defs/__schema11", + }, + type: "object", + }, + }, + required: [ + "rules", + ], + type: "object", + }, foo: { additionalProperties: false, properties: { diff --git a/packages/json-schema/test/fixtures/data.ts b/packages/json-schema/test/fixtures/data.ts index c2c40c491..3c2b98c1c 100644 --- a/packages/json-schema/test/fixtures/data.ts +++ b/packages/json-schema/test/fixtures/data.ts @@ -1,5 +1,5 @@ import { Data, targetIncludes } from '@targetd/api' -import z from 'zod' +import { z } from 'zod/mini' export const data = await Data.create() .usePayload( @@ -9,6 +9,7 @@ export const data = await Data.create() a: z.number(), b: z.array(z.string()), }), + car: z.record(z.string(), z.number()), }, ) .useTargeting({ @@ -57,3 +58,10 @@ export const data = await Data.create() }, }, ]) + .addRules('car', [ + { + payload: { + a: 123, + }, + }, + ]) From 14814537cbe1ed48f353811247fe2bbacae91997 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:14:40 +0000 Subject: [PATCH 4/9] fix: recursively resolve array variables --- .../api/src/parsers/DataItemVariableResolver.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/api/src/parsers/DataItemVariableResolver.ts b/packages/api/src/parsers/DataItemVariableResolver.ts index 26a9a6e7a..12beca1a0 100644 --- a/packages/api/src/parsers/DataItemVariableResolver.ts +++ b/packages/api/src/parsers/DataItemVariableResolver.ts @@ -66,8 +66,10 @@ export function isVariableResolver(x: unknown): x is VariableResolver { } export function resolveVariables(variables: Record, x: unknown) { - return typeof x === 'object' && x !== null && !Array.isArray(x) - ? recursivelyResolveVariables(variables, x as Record) + return Array.isArray(x) + ? recursivelyResolveArrayVariables(variables, x) + : typeof x === 'object' && x !== null + ? recursivelyResolveObjectVariables(variables, x as Record) : resolveVariable(variables, x) } @@ -81,7 +83,14 @@ function resolveVariable(variables: Record, x: unknown) { return isVariableResolver(x) ? x(variables) : x } -function recursivelyResolveVariables( +function recursivelyResolveArrayVariables( + variables: Record, + x: unknown[], +): unknown[] { + return x.map((value) => resolveVariables(variables, value)) +} + +function recursivelyResolveObjectVariables( variables: Record, x: Record, ): Record { From 121a9c8371f4a4193d3a129ee960c6bfc6df9c51 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:16:24 +0000 Subject: [PATCH 5/9] test(json-schema): arrays --- .../test/__snapshots__/index.test.ts.snap | 194 +++++++++++++++--- packages/json-schema/test/fixtures/data.ts | 5 +- 2 files changed, 163 insertions(+), 36 deletions(-) diff --git a/packages/json-schema/test/__snapshots__/index.test.ts.snap b/packages/json-schema/test/__snapshots__/index.test.ts.snap index 5577ffc9a..a1d5ad45c 100644 --- a/packages/json-schema/test/__snapshots__/index.test.ts.snap +++ b/packages/json-schema/test/__snapshots__/index.test.ts.snap @@ -76,6 +76,67 @@ snapshot[`json schema for simple data object 1`] = ` }, type: "object", }, + __schema14: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, + __schema15: { + type: "string", + }, + __schema16: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + {}, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema17", + }, + { + items: { + "\$ref": "#/\$defs/__schema17", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + __schema17: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, __schema1: { items: { type: "string", @@ -205,6 +266,71 @@ snapshot[`json schema for simple data object 1`] = ` "\$schema": { type: "string", }, + array: { + additionalProperties: false, + properties: { + rules: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + items: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + type: "number", + }, + ], + }, + type: "array", + }, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema14", + }, + { + items: { + "\$ref": "#/\$defs/__schema14", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + variables: { + additionalProperties: { + "\$ref": "#/\$defs/__schema16", + }, + propertyNames: { + "\$ref": "#/\$defs/__schema15", + }, + type: "object", + }, + }, + required: [ + "rules", + ], + type: "object", + }, bar: { additionalProperties: false, properties: { @@ -291,7 +417,7 @@ snapshot[`json schema for simple data object 1`] = ` ], type: "object", }, - car: { + foo: { additionalProperties: false, properties: { rules: { @@ -299,38 +425,16 @@ snapshot[`json schema for simple data object 1`] = ` additionalProperties: false, properties: { payload: { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - additionalProperties: { - anyOf: [ - { - pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", - type: "string", - }, - { - type: "number", - }, - ], - }, - propertyNames: { - type: "string", - }, - type: "object", - }, - ], + type: "string", }, targeting: { anyOf: [ { - "\$ref": "#/\$defs/__schema10", + "\$ref": "#/\$defs/__schema0", }, { items: { - "\$ref": "#/\$defs/__schema10", + "\$ref": "#/\$defs/__schema0", }, type: "array", }, @@ -346,10 +450,10 @@ snapshot[`json schema for simple data object 1`] = ` }, variables: { additionalProperties: { - "\$ref": "#/\$defs/__schema12", + "\$ref": "#/\$defs/__schema4", }, propertyNames: { - "\$ref": "#/\$defs/__schema11", + "\$ref": "#/\$defs/__schema3", }, type: "object", }, @@ -359,7 +463,7 @@ snapshot[`json schema for simple data object 1`] = ` ], type: "object", }, - foo: { + record: { additionalProperties: false, properties: { rules: { @@ -367,16 +471,38 @@ snapshot[`json schema for simple data object 1`] = ` additionalProperties: false, properties: { payload: { - type: "string", + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + additionalProperties: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + type: "number", + }, + ], + }, + propertyNames: { + type: "string", + }, + type: "object", + }, + ], }, targeting: { anyOf: [ { - "\$ref": "#/\$defs/__schema0", + "\$ref": "#/\$defs/__schema10", }, { items: { - "\$ref": "#/\$defs/__schema0", + "\$ref": "#/\$defs/__schema10", }, type: "array", }, @@ -392,10 +518,10 @@ snapshot[`json schema for simple data object 1`] = ` }, variables: { additionalProperties: { - "\$ref": "#/\$defs/__schema4", + "\$ref": "#/\$defs/__schema12", }, propertyNames: { - "\$ref": "#/\$defs/__schema3", + "\$ref": "#/\$defs/__schema11", }, type: "object", }, diff --git a/packages/json-schema/test/fixtures/data.ts b/packages/json-schema/test/fixtures/data.ts index 3c2b98c1c..b4bd7946c 100644 --- a/packages/json-schema/test/fixtures/data.ts +++ b/packages/json-schema/test/fixtures/data.ts @@ -9,7 +9,8 @@ export const data = await Data.create() a: z.number(), b: z.array(z.string()), }), - car: z.record(z.string(), z.number()), + record: z.record(z.string(), z.number()), + array: z.array(z.number()), }, ) .useTargeting({ @@ -58,7 +59,7 @@ export const data = await Data.create() }, }, ]) - .addRules('car', [ + .addRules('record', [ { payload: { a: 123, From f5a91222e4ab27693c01286626bc441e36daccb5 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:17:12 +0000 Subject: [PATCH 6/9] test: vigilant Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/api/test/Data.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/api/test/Data.test.ts b/packages/api/test/Data.test.ts index 1627bef3a..9f039d926 100644 --- a/packages/api/test/Data.test.ts +++ b/packages/api/test/Data.test.ts @@ -1,4 +1,8 @@ -import { assertRejects, assertStrictEquals } from 'jsr:@std/assert' +import { + assertEquals, + assertRejects, + assertStrictEquals, +} from 'jsr:@std/assert' import { assertSnapshot } from 'jsr:@std/testing/snapshot' import { setTimeout } from 'node:timers/promises' import z, { type ZodError } from 'zod' @@ -692,6 +696,16 @@ Deno.test('variables in arrays', async (t) => { t, data.data, ) + + assertEquals( + await data.getPayload('foo'), + [1], + ) + + assertEquals( + await data.getPayload('bar'), + [{ b: 2, c: '3' }], + ) }) Deno.test('errors when using variables with incorrect types', async (t) => { From 23257acf269a4da944c6e164a595cd8afe06ff94 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:18:45 +0000 Subject: [PATCH 7/9] test: vigilant --- packages/api/test/Data.test.ts | 6 ++--- .../api/test/__snapshots__/Data.test.ts.snap | 27 ------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/api/test/Data.test.ts b/packages/api/test/Data.test.ts index 9f039d926..974a2270d 100644 --- a/packages/api/test/Data.test.ts +++ b/packages/api/test/Data.test.ts @@ -665,10 +665,8 @@ Deno.test('variables in records', async (t) => { rules: [{ payload: { a: '{{a}}' } }], }) - await assertSnapshot( - t, - data.data, - ) + const payload = await data.getPayload('foo') + assertEquals(payload, { a: [1, 2, 3] }) }) Deno.test('variables in arrays', async (t) => { diff --git a/packages/api/test/__snapshots__/Data.test.ts.snap b/packages/api/test/__snapshots__/Data.test.ts.snap index 2ff0c3ff4..20cd38c2f 100644 --- a/packages/api/test/__snapshots__/Data.test.ts.snap +++ b/packages/api/test/__snapshots__/Data.test.ts.snap @@ -377,33 +377,6 @@ snapshot[`variables using fallthrough targeting 1`] = ` } `; -snapshot[`variables in records 1`] = ` -{ - foo: { - rules: [ - { - payload: { - a: [Function: resolver] { - "\$\$resolver\$\$": true, - }, - }, - }, - ], - variables: { - a: [ - { - payload: [ - 1, - 2, - 3, - ], - }, - ], - }, - }, -} -`; - snapshot[`variables in arrays 1`] = ` { bar: { From a7ad013de210a091eeace52b483bc5f1567a600c Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:20:05 +0000 Subject: [PATCH 8/9] chore: remove unused variable --- packages/api/test/Data.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/test/Data.test.ts b/packages/api/test/Data.test.ts index 974a2270d..f16e78374 100644 --- a/packages/api/test/Data.test.ts +++ b/packages/api/test/Data.test.ts @@ -653,7 +653,7 @@ Deno.test('variables using fallthrough targeting', async (t) => { await assertSnapshot(t, await data.getPayload('foo', { channel: 'bar' })) }) -Deno.test('variables in records', async (t) => { +Deno.test('variables in records', async () => { const data = await Data.create() .usePayload({ foo: z.record(z.string(), z.array(z.number())), From 36601e9fca505cac0296690cbff313d38f4e9995 Mon Sep 17 00:00:00 2001 From: John Wright Date: Sat, 14 Mar 2026 12:20:40 +0000 Subject: [PATCH 9/9] fix: export ToJSONSchemaParams Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/json-schema/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 314d640ca..40a0dc3ab 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -75,7 +75,7 @@ export function dataJSONSchema( ) } -type ToJSONSchemaParams = NonNullable[1]> +export type ToJSONSchemaParams = NonNullable[1]> function toJSONSchemaParams(params?: ToJSONSchemaParams): ToJSONSchemaParams { return {