mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-21 07:38:22 +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;
|
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
|
* Transforms a Pydantic JSON Schema to RJSF format
|
||||||
* Resolves references and generates appropriate uiSchema
|
* Resolves references and generates appropriate uiSchema
|
||||||
@ -550,6 +616,11 @@ export function transformSchema(
|
|||||||
const cleanSchema = resolveAndCleanSchema(rawSchema);
|
const cleanSchema = resolveAndCleanSchema(rawSchema);
|
||||||
const normalizedSchema = normalizeNullableSchema(cleanSchema);
|
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
|
// Generate uiSchema
|
||||||
const uiSchema = generateUiSchema(normalizedSchema, options);
|
const uiSchema = generateUiSchema(normalizedSchema, options);
|
||||||
|
|
||||||
|
|||||||
@ -77,6 +77,8 @@ export const PROFILE_ELIGIBLE_SECTIONS = new Set([
|
|||||||
"audio",
|
"audio",
|
||||||
"birdseye",
|
"birdseye",
|
||||||
"detect",
|
"detect",
|
||||||
|
"face_recognition",
|
||||||
|
"lpr",
|
||||||
"motion",
|
"motion",
|
||||||
"notifications",
|
"notifications",
|
||||||
"objects",
|
"objects",
|
||||||
@ -204,6 +206,32 @@ export function buildOverrides(
|
|||||||
// Normalize raw config data (strip internal fields) and remove any paths
|
// Normalize raw config data (strip internal fields) and remove any paths
|
||||||
// listed in `hiddenFields` so they are not included in override computation.
|
// 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(
|
export function sanitizeSectionData(
|
||||||
data: ConfigSectionData,
|
data: ConfigSectionData,
|
||||||
hiddenFields?: string[],
|
hiddenFields?: string[],
|
||||||
@ -215,7 +243,7 @@ export function sanitizeSectionData(
|
|||||||
const cleaned = cloneDeep(normalized) as ConfigSectionData;
|
const cleaned = cloneDeep(normalized) as ConfigSectionData;
|
||||||
hiddenFields.forEach((path) => {
|
hiddenFields.forEach((path) => {
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
unset(cleaned, path);
|
unsetWithWildcard(cleaned as Record<string, unknown>, path);
|
||||||
});
|
});
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user