mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-13 03:47:34 +03:00
fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like filters.*.mask were never stripped from form data, leaving null raw_coordinates that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip hidden fields from the JSON schema itself as defense-in-depth.
This commit is contained in:
parent
379247dee6
commit
cd58329796
@ -538,6 +538,72 @@ function generateUiSchema(
|
||||
return uiSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes hidden field properties from the JSON schema itself so RJSF won't
|
||||
* validate them. The existing ui:widget=hidden approach only hides rendering
|
||||
* but still validates — fields with server-only values (e.g. raw_coordinates
|
||||
* serialized as null) cause spurious validation errors.
|
||||
*
|
||||
* Supports dotted paths ("mask"), nested paths ("genai.enabled_in_config"),
|
||||
* and wildcard segments ("filters.*.mask") where `*` matches
|
||||
* additionalProperties.
|
||||
*/
|
||||
function stripHiddenFieldsFromSchema(
|
||||
schema: RJSFSchema,
|
||||
hiddenFields: string[],
|
||||
): void {
|
||||
for (const pattern of hiddenFields) {
|
||||
if (!pattern) continue;
|
||||
const segments = pattern.split(".");
|
||||
removePropertyBySegments(schema, segments);
|
||||
}
|
||||
}
|
||||
|
||||
function removePropertyBySegments(
|
||||
schema: RJSFSchema,
|
||||
segments: string[],
|
||||
): void {
|
||||
if (segments.length === 0 || !isSchemaObject(schema)) return;
|
||||
|
||||
const [head, ...rest] = segments;
|
||||
const props = schema.properties as
|
||||
| Record<string, RJSFSchema>
|
||||
| undefined;
|
||||
|
||||
if (rest.length === 0) {
|
||||
// Terminal segment — delete the property
|
||||
if (head === "*") {
|
||||
// Wildcard at leaf: strip from additionalProperties
|
||||
if (isSchemaObject(schema.additionalProperties)) {
|
||||
// Nothing to delete — "*" as the last segment means "every dynamic key".
|
||||
// The parent's additionalProperties schema IS the dynamic value, not a
|
||||
// container. In practice hidden-field patterns always have a named leaf
|
||||
// after the wildcard (e.g. "filters.*.mask"), so this branch is a no-op.
|
||||
}
|
||||
} else if (props && head in props) {
|
||||
delete props[head];
|
||||
if (Array.isArray(schema.required)) {
|
||||
schema.required = (schema.required as string[]).filter(
|
||||
(r) => r !== head,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (head === "*") {
|
||||
// Wildcard segment — descend into additionalProperties
|
||||
if (isSchemaObject(schema.additionalProperties)) {
|
||||
removePropertyBySegments(
|
||||
schema.additionalProperties as RJSFSchema,
|
||||
rest,
|
||||
);
|
||||
}
|
||||
} else if (props && head in props && isSchemaObject(props[head])) {
|
||||
removePropertyBySegments(props[head], rest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a Pydantic JSON Schema to RJSF format
|
||||
* Resolves references and generates appropriate uiSchema
|
||||
@ -550,6 +616,11 @@ export function transformSchema(
|
||||
const cleanSchema = resolveAndCleanSchema(rawSchema);
|
||||
const normalizedSchema = normalizeNullableSchema(cleanSchema);
|
||||
|
||||
// Remove hidden fields from schema so RJSF won't validate them
|
||||
if (options.hiddenFields && options.hiddenFields.length > 0) {
|
||||
stripHiddenFieldsFromSchema(normalizedSchema, options.hiddenFields);
|
||||
}
|
||||
|
||||
// Generate uiSchema
|
||||
const uiSchema = generateUiSchema(normalizedSchema, options);
|
||||
|
||||
|
||||
@ -77,6 +77,8 @@ export const PROFILE_ELIGIBLE_SECTIONS = new Set([
|
||||
"audio",
|
||||
"birdseye",
|
||||
"detect",
|
||||
"face_recognition",
|
||||
"lpr",
|
||||
"motion",
|
||||
"notifications",
|
||||
"objects",
|
||||
@ -204,6 +206,32 @@ export function buildOverrides(
|
||||
// Normalize raw config data (strip internal fields) and remove any paths
|
||||
// listed in `hiddenFields` so they are not included in override computation.
|
||||
|
||||
// lodash `unset` treats `*` as a literal key. This helper expands wildcard
|
||||
// segments so that e.g. `"filters.*.mask"` unsets `filters.<each key>.mask`.
|
||||
function unsetWithWildcard(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): void {
|
||||
if (!path.includes("*")) {
|
||||
unset(obj, path);
|
||||
return;
|
||||
}
|
||||
const segments = path.split(".");
|
||||
const starIndex = segments.indexOf("*");
|
||||
const prefix = segments.slice(0, starIndex).join(".");
|
||||
const suffix = segments.slice(starIndex + 1).join(".");
|
||||
const parent = prefix ? get(obj, prefix) : obj;
|
||||
if (parent && typeof parent === "object") {
|
||||
for (const key of Object.keys(parent as Record<string, unknown>)) {
|
||||
const fullPath = suffix ? `${key}.${suffix}` : key;
|
||||
unsetWithWildcard(
|
||||
parent as Record<string, unknown>,
|
||||
fullPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeSectionData(
|
||||
data: ConfigSectionData,
|
||||
hiddenFields?: string[],
|
||||
@ -215,7 +243,7 @@ export function sanitizeSectionData(
|
||||
const cleaned = cloneDeep(normalized) as ConfigSectionData;
|
||||
hiddenFields.forEach((path) => {
|
||||
if (!path) return;
|
||||
unset(cleaned, path);
|
||||
unsetWithWildcard(cleaned as Record<string, unknown>, path);
|
||||
});
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user