Skip to content

Commit 315239c

Browse files
fix(orchestrator): scope SchemaUpdater replacements (#2725)
Limit SchemaUpdater replacements to the originating scope, with robust path resolution for nested and array schemas, plus tests covering scoping cases. Made-with: Cursor
1 parent 91013e2 commit 315239c

6 files changed

Lines changed: 412 additions & 14 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
3+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-api': patch
4+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch
5+
---
6+
7+
Scope SchemaUpdater replacements to the originating step and improve scope resolution.

workspaces/orchestrator/plugins/orchestrator-form-api/report.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export type OrchestratorFormContextProps = {
4646
export type OrchestratorFormDecorator = (FormComponent: React.ComponentType<FormDecoratorProps>) => React.ComponentType<OrchestratorFormContextProps>;
4747

4848
// @public
49-
export type OrchestratorFormSchemaUpdater = (chunks: SchemaChunksResponse) => void;
49+
export type OrchestratorFormSchemaUpdater = (chunks: SchemaChunksResponse, scopeId?: string) => void;
5050

5151
// @public
5252
export type SchemaChunksResponse = {

workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type SchemaChunksResponse = {
113113
*/
114114
export type OrchestratorFormSchemaUpdater = (
115115
chunks: SchemaChunksResponse,
116+
scopeId?: string,
116117
) => void;
117118

118119
/**

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const SchemaUpdater: Widget<
104104
});
105105

106106
try {
107-
updateSchema(typedData);
107+
updateSchema(typedData, props.id);
108108
} catch (err) {
109109
// eslint-disable-next-line no-console
110110
console.error('Error when updating schema', props.id, err);
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { JSONSchema7 } from 'json-schema';
18+
19+
import { getSchemaUpdater } from './schemaUpdater';
20+
21+
describe('getSchemaUpdater', () => {
22+
it('updates only within the scoped step when id is provided', () => {
23+
const schema = {
24+
type: 'object',
25+
properties: {
26+
'app-registration': {
27+
type: 'object',
28+
properties: {
29+
schemaUpdater: { type: 'string' },
30+
placeholderTwo: { type: 'string', title: 'App placeholder' },
31+
},
32+
},
33+
'caas-namespace': {
34+
type: 'object',
35+
properties: {
36+
schemaUpdater: { type: 'string' },
37+
placeholderTwo: { type: 'string', title: 'CaaS placeholder' },
38+
},
39+
},
40+
},
41+
} as JSONSchema7;
42+
43+
const setSchema = jest.fn();
44+
const updateSchema = getSchemaUpdater(schema, setSchema);
45+
46+
updateSchema(
47+
{
48+
placeholderTwo: {
49+
type: 'string',
50+
title: 'CaaS placeholder (updated)',
51+
},
52+
},
53+
'root_caas-namespace_schemaUpdater',
54+
);
55+
56+
expect(setSchema).toHaveBeenCalledTimes(1);
57+
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
58+
expect(
59+
(updatedSchema.properties?.['app-registration'] as JSONSchema7).properties
60+
?.placeholderTwo,
61+
).toEqual({ type: 'string', title: 'App placeholder' });
62+
expect(
63+
(updatedSchema.properties?.['caas-namespace'] as JSONSchema7).properties
64+
?.placeholderTwo,
65+
).toEqual({ type: 'string', title: 'CaaS placeholder (updated)' });
66+
});
67+
68+
it('scopes updates to nested objects when SchemaUpdater is nested', () => {
69+
const schema = {
70+
type: 'object',
71+
properties: {
72+
step: {
73+
type: 'object',
74+
properties: {
75+
placeholderTwo: { type: 'string', title: 'Step placeholder' },
76+
nested: {
77+
type: 'object',
78+
properties: {
79+
schemaUpdater: { type: 'string' },
80+
placeholderTwo: { type: 'string', title: 'Nested placeholder' },
81+
},
82+
},
83+
},
84+
},
85+
},
86+
} as JSONSchema7;
87+
88+
const setSchema = jest.fn();
89+
const updateSchema = getSchemaUpdater(schema, setSchema);
90+
91+
updateSchema(
92+
{
93+
placeholderTwo: {
94+
type: 'string',
95+
title: 'Nested placeholder (updated)',
96+
},
97+
},
98+
'root_step_nested_schemaUpdater',
99+
);
100+
101+
expect(setSchema).toHaveBeenCalledTimes(1);
102+
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
103+
const stepProps = (updatedSchema.properties?.step as JSONSchema7)
104+
.properties;
105+
expect(stepProps?.placeholderTwo).toEqual({
106+
type: 'string',
107+
title: 'Step placeholder',
108+
});
109+
expect(
110+
(stepProps?.nested as JSONSchema7).properties?.placeholderTwo,
111+
).toEqual({ type: 'string', title: 'Nested placeholder (updated)' });
112+
});
113+
114+
it('supports dot-notation ids for scoping', () => {
115+
const schema = {
116+
type: 'object',
117+
properties: {
118+
step: {
119+
type: 'object',
120+
properties: {
121+
placeholderTwo: { type: 'string', title: 'Step placeholder' },
122+
nested: {
123+
type: 'object',
124+
properties: {
125+
schemaUpdater: { type: 'string' },
126+
placeholderTwo: { type: 'string', title: 'Nested placeholder' },
127+
},
128+
},
129+
},
130+
},
131+
},
132+
} as JSONSchema7;
133+
134+
const setSchema = jest.fn();
135+
const updateSchema = getSchemaUpdater(schema, setSchema);
136+
137+
updateSchema(
138+
{
139+
placeholderTwo: {
140+
type: 'string',
141+
title: 'Nested placeholder (dot updated)',
142+
},
143+
},
144+
'root.step.nested.schemaUpdater',
145+
);
146+
147+
expect(setSchema).toHaveBeenCalledTimes(1);
148+
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
149+
const stepProps = (updatedSchema.properties?.step as JSONSchema7)
150+
.properties;
151+
expect(stepProps?.placeholderTwo).toEqual({
152+
type: 'string',
153+
title: 'Step placeholder',
154+
});
155+
expect(
156+
(stepProps?.nested as JSONSchema7).properties?.placeholderTwo,
157+
).toEqual({ type: 'string', title: 'Nested placeholder (dot updated)' });
158+
});
159+
160+
it('resolves scope through array items', () => {
161+
const schema = {
162+
type: 'object',
163+
properties: {
164+
step: {
165+
type: 'object',
166+
properties: {
167+
items: {
168+
type: 'array',
169+
items: {
170+
type: 'object',
171+
properties: {
172+
schemaUpdater: { type: 'string' },
173+
placeholderTwo: {
174+
type: 'string',
175+
title: 'Array placeholder',
176+
},
177+
},
178+
},
179+
},
180+
},
181+
},
182+
},
183+
} as JSONSchema7;
184+
185+
const setSchema = jest.fn();
186+
const updateSchema = getSchemaUpdater(schema, setSchema);
187+
188+
updateSchema(
189+
{
190+
placeholderTwo: {
191+
type: 'string',
192+
title: 'Array placeholder (updated)',
193+
},
194+
},
195+
'root_step_items_schemaUpdater',
196+
);
197+
198+
expect(setSchema).toHaveBeenCalledTimes(1);
199+
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
200+
const arrayProps = (
201+
(updatedSchema.properties?.step as JSONSchema7).properties
202+
?.items as JSONSchema7
203+
).items as JSONSchema7;
204+
expect(arrayProps.properties?.placeholderTwo).toEqual({
205+
type: 'string',
206+
title: 'Array placeholder (updated)',
207+
});
208+
});
209+
210+
it('prefers longest matching keys when scoping', () => {
211+
const schema = {
212+
type: 'object',
213+
properties: {
214+
app: {
215+
type: 'object',
216+
properties: {
217+
schemaUpdater: { type: 'string' },
218+
placeholderTwo: { type: 'string', title: 'App placeholder' },
219+
},
220+
},
221+
'app-registration': {
222+
type: 'object',
223+
properties: {
224+
schemaUpdater: { type: 'string' },
225+
placeholderTwo: {
226+
type: 'string',
227+
title: 'App-registration placeholder',
228+
},
229+
},
230+
},
231+
},
232+
} as JSONSchema7;
233+
234+
const setSchema = jest.fn();
235+
const updateSchema = getSchemaUpdater(schema, setSchema);
236+
237+
updateSchema(
238+
{
239+
placeholderTwo: {
240+
type: 'string',
241+
title: 'App-registration placeholder (updated)',
242+
},
243+
},
244+
'root_app-registration_schemaUpdater',
245+
);
246+
247+
expect(setSchema).toHaveBeenCalledTimes(1);
248+
const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
249+
expect(
250+
(updatedSchema.properties?.app as JSONSchema7).properties?.placeholderTwo,
251+
).toEqual({ type: 'string', title: 'App placeholder' });
252+
expect(
253+
(updatedSchema.properties?.['app-registration'] as JSONSchema7).properties
254+
?.placeholderTwo,
255+
).toEqual({
256+
type: 'string',
257+
title: 'App-registration placeholder (updated)',
258+
});
259+
});
260+
});

0 commit comments

Comments
 (0)