Skip to content

Commit 3a1f467

Browse files
fix: pass experimental_customMergeAllOf through all resolution paths (#4779)
Co-authored-by: Heath C <[email protected]>
1 parent ba61a8a commit 3a1f467

File tree

2 files changed

+346
-0
lines changed

2 files changed

+346
-0
lines changed

packages/utils/src/schema/retrieveSchema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export function resolveSchema<T = any, S extends StrictRJSFSchema = RJSFSchema,
241241
expandAllBranches,
242242
recurseList,
243243
formData,
244+
experimental_customMergeAllOf,
244245
);
245246
if (updatedSchemas.length > 1 || updatedSchemas[0] !== schema) {
246247
// return the updatedSchemas array if it has either multiple schemas within it
@@ -255,6 +256,7 @@ export function resolveSchema<T = any, S extends StrictRJSFSchema = RJSFSchema,
255256
expandAllBranches,
256257
recurseList,
257258
formData,
259+
experimental_customMergeAllOf,
258260
);
259261
return resolvedSchemas.flatMap((s) => {
260262
return retrieveSchemaInternal<T, S, F>(

packages/utils/test/schema/retrieveSchemaTest.ts

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,5 +1926,349 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) {
19261926
]);
19271927
});
19281928
});
1929+
describe('resolveReference() with experimental_customMergeAllOf', () => {
1930+
it('should pass experimental_customMergeAllOf parameter to retrieveSchemaInternal', () => {
1931+
const schema: RJSFSchema = {
1932+
$ref: '#/definitions/testRef',
1933+
allOf: [
1934+
{
1935+
type: 'object',
1936+
properties: {
1937+
string: { type: 'string' },
1938+
},
1939+
},
1940+
{
1941+
type: 'object',
1942+
properties: {
1943+
number: { type: 'number' },
1944+
},
1945+
},
1946+
],
1947+
};
1948+
const rootSchema: RJSFSchema = {
1949+
definitions: {
1950+
testRef: {
1951+
type: 'object',
1952+
properties: {
1953+
base: { type: 'string' },
1954+
},
1955+
},
1956+
},
1957+
};
1958+
const customMergeAllOf = jest.fn().mockReturnValue({
1959+
type: 'object',
1960+
properties: {
1961+
base: { type: 'string' },
1962+
string: { type: 'string' },
1963+
number: { type: 'number' },
1964+
},
1965+
});
1966+
const result = retrieveSchema(testValidator, schema, rootSchema, {}, customMergeAllOf);
1967+
expect(customMergeAllOf).toHaveBeenCalled();
1968+
expect(result).toEqual({
1969+
type: 'object',
1970+
properties: {
1971+
base: { type: 'string' },
1972+
string: { type: 'string' },
1973+
number: { type: 'number' },
1974+
},
1975+
});
1976+
});
1977+
});
1978+
describe('resolveDependencies() with experimental_customMergeAllOf', () => {
1979+
it('should pass experimental_customMergeAllOf parameter through dependency resolution', () => {
1980+
const schema: RJSFSchema = {
1981+
type: 'object',
1982+
properties: {
1983+
a: { type: 'string' },
1984+
},
1985+
dependencies: {
1986+
a: {
1987+
allOf: [
1988+
{
1989+
type: 'object',
1990+
properties: {
1991+
string: { type: 'string' },
1992+
},
1993+
},
1994+
{
1995+
type: 'object',
1996+
properties: {
1997+
number: { type: 'number' },
1998+
},
1999+
},
2000+
],
2001+
},
2002+
},
2003+
};
2004+
const rootSchema: RJSFSchema = { definitions: {} };
2005+
const formData = { a: 'test' };
2006+
const customMergeAllOf = jest.fn().mockReturnValue({
2007+
type: 'object',
2008+
properties: {
2009+
string: { type: 'string' },
2010+
number: { type: 'number' },
2011+
},
2012+
});
2013+
const result = retrieveSchema(testValidator, schema, rootSchema, formData, customMergeAllOf);
2014+
expect(customMergeAllOf).toHaveBeenCalled();
2015+
expect(result).toEqual({
2016+
type: 'object',
2017+
properties: {
2018+
a: { type: 'string' },
2019+
string: { type: 'string' },
2020+
number: { type: 'number' },
2021+
},
2022+
});
2023+
});
2024+
});
2025+
describe('resolveSchema() integration with experimental_customMergeAllOf', () => {
2026+
it('should properly pass experimental_customMergeAllOf through all resolution paths', () => {
2027+
const schema: RJSFSchema = {
2028+
$ref: '#/definitions/baseSchema',
2029+
dependencies: {
2030+
trigger: {
2031+
allOf: [
2032+
{
2033+
type: 'object',
2034+
properties: {
2035+
prop1: { type: 'string' },
2036+
},
2037+
},
2038+
{
2039+
type: 'object',
2040+
properties: {
2041+
prop2: { type: 'number' },
2042+
},
2043+
},
2044+
],
2045+
},
2046+
},
2047+
};
2048+
const rootSchema: RJSFSchema = {
2049+
definitions: {
2050+
baseSchema: {
2051+
type: 'object',
2052+
properties: {
2053+
base: { type: 'string' },
2054+
},
2055+
allOf: [
2056+
{
2057+
type: 'object',
2058+
properties: {
2059+
additional: { type: 'boolean' },
2060+
},
2061+
},
2062+
],
2063+
},
2064+
},
2065+
};
2066+
const formData = { trigger: 'value' };
2067+
const customMergeAllOf = jest.fn().mockImplementation((schema) => {
2068+
// Custom merge logic that combines all properties
2069+
const allProperties: any = {};
2070+
if (schema.properties) {
2071+
Object.assign(allProperties, schema.properties);
2072+
}
2073+
if (schema.allOf) {
2074+
schema.allOf.forEach((subSchema: any) => {
2075+
if (subSchema.properties) {
2076+
Object.assign(allProperties, subSchema.properties);
2077+
}
2078+
});
2079+
}
2080+
return {
2081+
...schema,
2082+
properties: allProperties,
2083+
allOf: undefined,
2084+
};
2085+
});
2086+
const result = retrieveSchema(testValidator, schema, rootSchema, formData, customMergeAllOf);
2087+
// Verify that customMergeAllOf was called multiple times (for different allOf blocks)
2088+
expect(customMergeAllOf).toHaveBeenCalledTimes(3);
2089+
expect(result).toEqual({
2090+
type: 'object',
2091+
properties: {
2092+
base: { type: 'string' },
2093+
additional: { type: 'boolean' },
2094+
},
2095+
allOf: undefined,
2096+
});
2097+
});
2098+
it('should handle experimental_customMergeAllOf with nested $ref resolution', () => {
2099+
const schema: RJSFSchema = {
2100+
$ref: '#/definitions/nestedRef',
2101+
};
2102+
const rootSchema: RJSFSchema = {
2103+
definitions: {
2104+
nestedRef: {
2105+
$ref: '#/definitions/finalSchema',
2106+
allOf: [
2107+
{
2108+
type: 'object',
2109+
properties: {
2110+
nested: { type: 'string' },
2111+
},
2112+
},
2113+
],
2114+
},
2115+
finalSchema: {
2116+
type: 'object',
2117+
properties: {
2118+
final: { type: 'number' },
2119+
},
2120+
},
2121+
},
2122+
};
2123+
const customMergeAllOf = jest.fn().mockReturnValue({
2124+
type: 'object',
2125+
properties: {
2126+
final: { type: 'number' },
2127+
nested: { type: 'string' },
2128+
},
2129+
});
2130+
const result = retrieveSchema(testValidator, schema, rootSchema, {}, customMergeAllOf);
2131+
expect(customMergeAllOf).toHaveBeenCalled();
2132+
expect(result).toEqual({
2133+
type: 'object',
2134+
properties: {
2135+
final: { type: 'number' },
2136+
nested: { type: 'string' },
2137+
},
2138+
});
2139+
});
2140+
});
2141+
describe('Edge cases for experimental_customMergeAllOf fix', () => {
2142+
it('should handle undefined experimental_customMergeAllOf parameter gracefully', () => {
2143+
const schema: RJSFSchema = {
2144+
$ref: '#/definitions/testRef',
2145+
dependencies: {
2146+
trigger: {
2147+
allOf: [
2148+
{
2149+
type: 'object',
2150+
properties: {
2151+
prop: { type: 'string' },
2152+
},
2153+
},
2154+
],
2155+
},
2156+
},
2157+
};
2158+
const rootSchema: RJSFSchema = {
2159+
definitions: {
2160+
testRef: {
2161+
type: 'object',
2162+
properties: {
2163+
base: { type: 'string' },
2164+
},
2165+
},
2166+
},
2167+
};
2168+
const formData = { trigger: 'value' };
2169+
// Test with undefined experimental_customMergeAllOf (should use default mergeAllOf)
2170+
const result = retrieveSchema(testValidator, schema, rootSchema, formData, undefined);
2171+
expect(result).toEqual({
2172+
type: 'object',
2173+
properties: {
2174+
base: { type: 'string' },
2175+
},
2176+
});
2177+
});
2178+
it('should handle experimental_customMergeAllOf that throws an error', () => {
2179+
const schema: RJSFSchema = {
2180+
allOf: [
2181+
{
2182+
type: 'object',
2183+
properties: {
2184+
string: { type: 'string' },
2185+
},
2186+
},
2187+
{
2188+
type: 'object',
2189+
properties: {
2190+
number: { type: 'number' },
2191+
},
2192+
},
2193+
],
2194+
};
2195+
const rootSchema: RJSFSchema = { definitions: {} };
2196+
const customMergeAllOf = jest.fn().mockImplementation(() => {
2197+
throw new Error('Custom merge failed');
2198+
});
2199+
const result = retrieveSchema(testValidator, schema, rootSchema, {}, customMergeAllOf);
2200+
// Should fall back to default behavior when custom merge fails
2201+
expect(result).toEqual({});
2202+
expect(consoleWarnSpy).toHaveBeenCalledWith('could not merge subschemas in allOf:\n', expect.any(Error));
2203+
});
2204+
it('should pass experimental_customMergeAllOf through complex nested resolution chains', () => {
2205+
const schema: RJSFSchema = {
2206+
$ref: '#/definitions/level1',
2207+
};
2208+
const rootSchema: RJSFSchema = {
2209+
definitions: {
2210+
level1: {
2211+
$ref: '#/definitions/level2',
2212+
dependencies: {
2213+
dep1: {
2214+
allOf: [
2215+
{
2216+
type: 'object',
2217+
properties: {
2218+
nested1: { type: 'string' },
2219+
},
2220+
},
2221+
],
2222+
},
2223+
},
2224+
},
2225+
level2: {
2226+
type: 'object',
2227+
properties: {
2228+
base: { type: 'string' },
2229+
},
2230+
allOf: [
2231+
{
2232+
type: 'object',
2233+
properties: {
2234+
additional: { type: 'number' },
2235+
},
2236+
},
2237+
],
2238+
},
2239+
},
2240+
};
2241+
const formData = { dep1: 'value' };
2242+
const customMergeAllOf = jest.fn().mockImplementation((schema) => {
2243+
const allProperties: any = {};
2244+
if (schema.properties) {
2245+
Object.assign(allProperties, schema.properties);
2246+
}
2247+
if (schema.allOf) {
2248+
schema.allOf.forEach((subSchema: any) => {
2249+
if (subSchema.properties) {
2250+
Object.assign(allProperties, subSchema.properties);
2251+
}
2252+
});
2253+
}
2254+
return {
2255+
...schema,
2256+
properties: allProperties,
2257+
allOf: undefined,
2258+
};
2259+
});
2260+
const result = retrieveSchema(testValidator, schema, rootSchema, formData, customMergeAllOf);
2261+
// Should be called for both allOf blocks (level2 and dependency)
2262+
expect(customMergeAllOf).toHaveBeenCalledTimes(3);
2263+
expect(result).toEqual({
2264+
type: 'object',
2265+
properties: {
2266+
base: { type: 'string' },
2267+
additional: { type: 'number' },
2268+
},
2269+
allOf: undefined,
2270+
});
2271+
});
2272+
});
19292273
});
19302274
}

0 commit comments

Comments
 (0)