Skip to content
Open
9 changes: 9 additions & 0 deletions packages/core/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import type AJV from 'ajv';
import type { ErrorObject } from 'ajv';
import type { ValidatorOption } from '../util/formValidator';
import { JsonSchema, UISchemaElement } from '../models';
import { generateDefaultUISchema, generateJsonSchema } from '../generators';

Expand Down Expand Up @@ -145,7 +146,15 @@ export interface UpdateCoreAction {
}

export interface InitActionOptions {
/** The AJV instance to validate with. Ignored for form validation when `validator` is set. */
ajv?: AJV;
/**
* A custom Form Validator: a factory producing one per schema, or a
* validator already bound to the schema. Takes precedence over `ajv`.
* Passing `undefined` explicitly switches back to AJV; leaving the key out
* keeps the validator currently in use.
*/
validator?: ValidatorOption;
validationMode?: ValidationMode;
additionalErrors?: ErrorObject[];
}
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/mappers/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import type { JsonSchema } from '../models';
import {
AnyAction,
Dispatch,
getAjv,
getRuleValidator,
getConfig,
getData,
getErrorAt,
Expand Down Expand Up @@ -116,7 +116,13 @@ export const mapStateToCellProps = (
const visible =
ownProps.visible !== undefined
? ownProps.visible
: isVisible(uischema, rootData, undefined, getAjv(state), config);
: isVisible(
uischema,
rootData,
undefined,
getRuleValidator(state),
config
);

const rootSchema = getSchema(state);

Expand Down
54 changes: 17 additions & 37 deletions packages/core/src/mappers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import {
} from '../util';
import {
Translator,
getAjv,
getRuleValidator,
getCells,
getConfig,
getData,
Expand All @@ -84,6 +84,7 @@ import {
getUiSchema,
} from '../store';
import { isInherentlyEnabled, isInherentlyReadonly } from './util';
import { isStructuralMatch } from '../util/structural';
import { CombinatorKeyword } from './combinators';
import isEqual from 'lodash/isEqual';

Expand Down Expand Up @@ -596,7 +597,13 @@ export const mapStateToControlProps = (

const visible: boolean =
ownProps.visible === undefined || hasShowRule(uischema)
? isVisible(uischema, rootData, ownProps.path, getAjv(state), config)
? isVisible(
uischema,
rootData,
ownProps.path,
getRuleValidator(state),
config
)
: ownProps.visible;
const controlElement = uischema as ControlElement;
const id = ownProps.id;
Expand Down Expand Up @@ -1066,7 +1073,7 @@ export const mapStateToLayoutProps = (
ownProps.uischema,
rootData,
ownProps.path,
getAjv(state),
getRuleValidator(state),
getConfig(state)
)
: ownProps.visible;
Expand Down Expand Up @@ -1163,41 +1170,14 @@ export const mapStateToCombinatorRendererProps = (
const { data, schema, rootSchema, i18nKeyPrefix, label, ...props } =
mapStateToControlProps(state, ownProps);

const ajv = state.jsonforms.core.ajv;
const structuralKeywords = [
'required',
'additionalProperties',
'type',
'enum',
'const',
];
const dataIsValid = (errors: ErrorObject[]): boolean => {
return (
!errors ||
errors.length === 0 ||
!errors.find((e) => structuralKeywords.indexOf(e.keyword) !== -1)
);
};
// Pick the first branch whose structural keywords fit the data. This is a
// pure structural check (see isStructuralMatch): no validator instance and
// no code generation are involved, so it also works under a strict CSP.
let indexOfFittingSchema: number;
// TODO instead of compiling the combinator subschemas we can compile the original schema
// without the combinator alternatives and then revalidate and check the errors for the
// element
for (let i = 0; i < schema[keyword]?.length; i++) {
try {
let _schema = schema[keyword][i];
if (_schema.$ref) {
_schema = Resolve.schema(rootSchema, _schema.$ref, rootSchema);
}
const valFn = ajv.compile(_schema);
valFn(data);
if (dataIsValid(valFn.errors)) {
indexOfFittingSchema = i;
break;
}
} catch (error) {
console.debug(
"Combinator subschema is not self contained, can't hand it over to AJV"
);
if (isStructuralMatch(schema[keyword][i], data, rootSchema)) {
indexOfFittingSchema = i;
break;
}
}

Expand Down Expand Up @@ -1319,7 +1299,7 @@ export const mapStateToLabelProps = (
props.uischema,
getData(state),
props.path,
getAjv(state),
getRuleValidator(state),
getConfig(state)
)
: props.visible;
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/mappers/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JsonSchema, UISchemaElement } from '../models';
import { JsonFormsState, getAjv } from '../store';
import { JsonFormsState, getRuleValidator } from '../store';
import { hasEnableRule, hasReadonlyRule, isEnabled, isReadonly } from '../util';

/**
Expand All @@ -19,7 +19,13 @@ export const isInherentlyEnabled = (
return false;
}
if (uischema && hasEnableRule(uischema)) {
return isEnabled(uischema, rootData, ownProps?.path, getAjv(state), config);
return isEnabled(
uischema,
rootData,
ownProps?.path,
getRuleValidator(state),
config
);
}
if (!config?.separateReadonlyFromDisabled) {
if (typeof uischema?.options?.readonly === 'boolean') {
Expand Down Expand Up @@ -66,7 +72,7 @@ export const isInherentlyReadonly = (
uischema,
rootData,
ownProps?.path,
getAjv(state),
getRuleValidator(state),
config
);
}
Expand Down
Loading