From a6c711e1752601066b3aa67c3512d32001556d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augustin=20=C5=A0ulc?= Date: Sun, 2 May 2021 19:56:43 +0200 Subject: [PATCH] Old validation package replaced by the new one --- lerna-bootstrap-fix.cmd | 1 - .../datascreens/src/filteredListViewModel.ts | 8 +- packages/validation/README.md | 176 ---------------- .../aggregateEntityValidator.test.ts | 0 .../automaticEntityValidator.test.ts | 199 ++++++------------ .../validation/__tests__/integration.test.ts | 83 -------- .../__tests__/manualEntityValidator.test.ts | 127 ++++++----- .../__tests__/serverEntityValidator.test.ts | 0 .../__tests__/testHelpers.ts | 0 .../__tests__/tsconfig.json | 0 .../src/aggregateEntityValidator.ts | 0 .../src/automaticEntityValidator.ts | 133 +++++++----- .../src/automaticValidatorTypes.ts | 6 +- .../src/configuration.ts | 2 +- .../src/entityValidatorBase.ts | 0 packages/validation/src/helpers.ts | 79 ------- packages/validation/src/index.ts | 18 +- .../validation/src/manualEntityValidator.ts | 85 ++++---- .../src/serverEntityValidator.ts | 0 packages/validation/src/types.ts | 107 +++------- .../{validation2 => validation}/src/utils.ts | 20 ++ .../validation/src/validatorsRepository.ts | 35 --- .../automaticEntityValidator.test.ts | 96 --------- .../__tests__/manualEntityValidator.test.ts | 97 --------- packages/validation2/package.json | 43 ---- .../src/automaticEntityValidator.ts | 92 -------- packages/validation2/src/index.ts | 9 - .../validation2/src/manualEntityValidator.ts | 53 ----- packages/validation2/src/types.ts | 37 ---- packages/validation2/tsconfig.json | 11 - packages/validation2/yarn.lock | 4 - stories/package.json | 1 - stories/src/bootstrap.stories.tsx | 18 +- .../automaticEntityValidator-deep.stories.tsx | 2 +- .../automaticEntityValidator.stories.tsx | 6 +- .../manualEntityValidator.stories.tsx | 2 +- stories/src/validation2/validationErrors.tsx | 2 +- stories/src/validation2/validityIndicator.tsx | 2 +- 38 files changed, 324 insertions(+), 1230 deletions(-) delete mode 100644 packages/validation/README.md rename packages/{validation2 => validation}/__tests__/aggregateEntityValidator.test.ts (100%) delete mode 100644 packages/validation/__tests__/integration.test.ts rename packages/{validation2 => validation}/__tests__/serverEntityValidator.test.ts (100%) rename packages/{validation2 => validation}/__tests__/testHelpers.ts (100%) rename packages/{validation2 => validation}/__tests__/tsconfig.json (100%) rename packages/{validation2 => validation}/src/aggregateEntityValidator.ts (100%) rename packages/{validation2 => validation}/src/automaticValidatorTypes.ts (87%) rename packages/{validation2 => validation}/src/configuration.ts (84%) rename packages/{validation2 => validation}/src/entityValidatorBase.ts (100%) delete mode 100644 packages/validation/src/helpers.ts rename packages/{validation2 => validation}/src/serverEntityValidator.ts (100%) rename packages/{validation2 => validation}/src/utils.ts (70%) delete mode 100644 packages/validation/src/validatorsRepository.ts delete mode 100644 packages/validation2/__tests__/automaticEntityValidator.test.ts delete mode 100644 packages/validation2/__tests__/manualEntityValidator.test.ts delete mode 100644 packages/validation2/package.json delete mode 100644 packages/validation2/src/automaticEntityValidator.ts delete mode 100644 packages/validation2/src/index.ts delete mode 100644 packages/validation2/src/manualEntityValidator.ts delete mode 100644 packages/validation2/src/types.ts delete mode 100644 packages/validation2/tsconfig.json delete mode 100644 packages/validation2/yarn.lock diff --git a/lerna-bootstrap-fix.cmd b/lerna-bootstrap-fix.cmd index bcd8fa77..254ed993 100644 --- a/lerna-bootstrap-fix.cmd +++ b/lerna-bootstrap-fix.cmd @@ -10,7 +10,6 @@ call yarn lerna bootstrap --scope=@frui.ts/helpers --force-local call yarn lerna bootstrap --scope=@frui.ts/htmlcontrols --force-local call yarn lerna bootstrap --scope=@frui.ts/screens --force-local call yarn lerna bootstrap --scope=@frui.ts/validation --force-local -call yarn lerna bootstrap --scope=@frui.ts/validation2 --force-local call yarn lerna bootstrap --scope=@frui.ts/views --force-local call yarn lerna bootstrap --scope=@frui.ts/example-complexdemo --force-local call yarn lerna bootstrap --scope=@frui.ts/examples-simpletodolist --force-local diff --git a/packages/datascreens/src/filteredListViewModel.ts b/packages/datascreens/src/filteredListViewModel.ts index 911b23b6..10ff4705 100644 --- a/packages/datascreens/src/filteredListViewModel.ts +++ b/packages/datascreens/src/filteredListViewModel.ts @@ -3,11 +3,11 @@ import { IPagingFilter, SortingDirection } from "@frui.ts/data"; import { attachAutomaticDirtyWatcher, IHasDirtyWatcher, resetDirty } from "@frui.ts/dirtycheck"; import { bound } from "@frui.ts/helpers"; import { ScreenBase } from "@frui.ts/screens"; -import { IHasValidation, validate } from "@frui.ts/validation"; import { action, isObservableArray, observable } from "mobx"; import ListViewModel from "./listViewModel"; +import { validate } from "@frui.ts/validation"; -type OmitValidationAndDirtyWatcher = Omit | keyof IHasValidation>; +type OmitValidationAndDirtyWatcher = Omit>; export default abstract class FilteredListViewModel< TEntity, @@ -17,7 +17,7 @@ export default abstract class FilteredListViewModel< static defaultPageSize = 30; /** Currently edited filter */ - @observable filter: TFilter & IHasDirtyWatcher & Partial>; + @observable filter: TFilter & IHasDirtyWatcher; /** Currently edited paging filter */ @observable pagingFilter: IPagingFilter; @@ -46,7 +46,7 @@ export default abstract class FilteredListViewModel< } protected cloneFilterForApply(filter: TFilter): OmitValidationAndDirtyWatcher { - const { __dirtycheck, __validation, ...clonedFilter } = this.filter; + const { __dirtycheck, ...clonedFilter } = this.filter; // we need to clone array properties so that they are not shared with the original filter Object.entries(clonedFilter).forEach(([key, value]) => { diff --git a/packages/validation/README.md b/packages/validation/README.md deleted file mode 100644 index 9fec6d9d..00000000 --- a/packages/validation/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# `@frui.ts/validation` - -Validation is based on the idea that validation of a particular property of an entity (i.e., the result of validation as a list of validation errors) can be handled as a computed value referring to the property and applying validation rules. Validator is thus just a factory that can generate such computed properties based on validation rules. - -A validator maintains not only validation errors, but also information if the errors should be displayed to the user (the `isErrorsVisible` property). For example, you don't want to display validation errors when creating a new entity until the user clicks Save for the first time. You can set the `isErrorsVisible` property when instantiating / attaching a validator or anytime later. The visibility is also turned on when `validate()` is called for the first time. - -## AutomaticEntityValidator - -Validator that automatically observes validated entity's properties and maintains validation errors. - -The validator uses validation functions defined in `validatorsRepository`. - -### Usage - -```ts -// create the 'required' validation rule -validatorsRepository.set("required", (value, propertyName, entity, params) => (value == null) ? undefined : `${propertyName} is required.`); -``` - - -```ts -// validation definition for an entity -const validationRules = { - firstName: { - required: true // the 'true' value is passed to the validator as 'params' - } -} -``` - -```ts -// direct usage -const target = { firstName: "John" }; - -const validator = new AutomaticEntityValidator(target, validationRules, false); - -let isEntityValid = validator.isValid; // true -let firstNameError = validator.errors.firstName; // undefined - -target.firstName = ""; - -isEntityValid = validator.isValid; // false -firstNameError = validator.errors.firstName; // "firstName is required" -``` - -```ts -// with helpers -import { - attachAutomaticValidator, - hasVisibleErrors, - isValid, - getValidationMessage, - validate -} from "@frui.ts/validation"; - -const target = { firstName: "John" }; -attachAutomaticValidator(target, validationRules); - -// you can also use: -// const target = attachAutomaticValidator({ firstName: "John" }, validationRules); - -let isEntityValid = isValid(target); // true -let isFirstNameValid = isValid(target, "firstName"); // true -let firstNameError = getValidationMessage(target, "firstName"); // undefined -let errorsVisible = hasVisibleErrors(target); // false - -target.firstName = ""; - -isEntityValid = isValid(target); // false -isFirstNameValid = isValid(target, "firstName"); // false -firstNameError = getValidationMessage(target, "firstName"); // undefined!! - the field is not valid -errorsVisible = hasVisibleErrors(target); // false - -isEntityValid = validate(target); // false - sets isErrorsVisible to true and returns isValid value - -firstNameError = getValidationMessage(target, "firstName"); // "firstName is required" -errorsVisible = hasVisibleErrors(target); // true -``` - -## ManualEntityValidator - -Basic implementation of `IValidator` interface where you need to manually add validation errors. This is useful when the validation is handled by the server. - -### Usage - -```ts -// direct usage -const target = { firstName: "John" }; - -const validator = new ManualEntityValidator(false); - -let isEntityValid = validator.isValid; // true -let firstNameError = validator.errors.firstName; // undefined - -validator.addError("firstName", "First name is wrong"); - -isEntityValid = validator.isValid; // false -firstNameError = validator.errors.firstName; // "First name is wrong" - -validator.clearErrors(); -``` - -```ts -// with helpers -import { - addError, - attachManualValidator, - clearErrors, - hasVisibleErrors, - isValid, - getValidationMessage, - validate -} from "@frui.ts/validation"; - -const target = { firstName: "John" }; -attachManualValidator(target); - -// you can also use: -// const target = attachManualValidator({ firstName: "John" }); - -let isEntityValid = isValid(target); // true -let isFirstNameValid = isValid(target, "firstName"); // true -let firstNameError = getValidationMessage(target, "firstName"); // undefined -let errorsVisible = hasVisibleErrors(target); // false - -addError(target, "firstName", "First name is wrong"); - -isEntityValid = isValid(target); // false -isFirstNameValid = isValid(target, "firstName"); // false -firstNameError = getValidationMessage(target, "firstName"); // undefined!! - the field is not valid -errorsVisible = hasVisibleErrors(target); // false - -isEntityValid = validate(target); // false - sets isErrorsVisible to true and returns isValid value - -firstNameError = getValidationMessage(target, "firstName"); // "First name is wrong" -errorsVisible = hasVisibleErrors(target); // true - -clearErrors(target); -``` - -## Combined validation - -In order to combine automatic and manual validation, you can create an automatic validation rule called `manualValidation` that accepts a manual validator as its params. With that, you can add and remove errors through the manual validator and they will be reflected through the automatic validator as well. Thus, you should attach the automatic validator to the entity (so that all errors are properly picked by any inputs and other observers), and keep the reference to the manual validator only for custom changes. - -```ts -import { get } from "mobx"; - -// define the 'manualErrors' validation rule -validatorsRepository.set("manualValidation", (value, propertyName, entity, params) => - // note that the 'params' argument here is the manual validator - get(params.errors, propertyName); -); -``` - -You can then use the rule as follows. Keep in mind that each target entity should have its own instance of the manual validator as you probably don't want to share validation errors among multiple entities. - -```ts -const target = { firstName: "John" }; - -const customManualValidator = new ManualEntityValidator(false); - -const validationRules = { - firstName: { - required: true, - manualValidation: customManualValidator - } -} -attachAutomaticValidator(target, validationRules); - -// later -customManualValidator.addError("firstName", "First name is wrong"); -``` - -Frui.ts v2 will include a refactored architecture of validators that will bring a more straightforward solution using composed validators. - -## TODO - - Async validators for long validation calls. diff --git a/packages/validation2/__tests__/aggregateEntityValidator.test.ts b/packages/validation/__tests__/aggregateEntityValidator.test.ts similarity index 100% rename from packages/validation2/__tests__/aggregateEntityValidator.test.ts rename to packages/validation/__tests__/aggregateEntityValidator.test.ts diff --git a/packages/validation/__tests__/automaticEntityValidator.test.ts b/packages/validation/__tests__/automaticEntityValidator.test.ts index b8f33d55..e8367397 100644 --- a/packages/validation/__tests__/automaticEntityValidator.test.ts +++ b/packages/validation/__tests__/automaticEntityValidator.test.ts @@ -1,15 +1,40 @@ import { observable } from "mobx"; -import AutomaticEntityValidator, { createPropertyValidatorFromRules } from "../src/automaticEntityValidator"; -import validatorsRepository from "../src/validatorsRepository"; +import AutomaticEntityValidator from "../src/automaticEntityValidator"; +import configuration from "../src/configuration"; +import { testCoreValidatorFunctions, expectInvalid, expectValid } from "./testHelpers"; beforeAll(() => { - validatorsRepository.set("required", value => (value ? undefined : "Value is required")); - validatorsRepository.set("mustBeJohn", (value, propertyName) => - value === "John" ? undefined : `${propertyName} must be John` - ); + configuration.valueValidators.set("required", value => ({ code: "required", isValid: !!value })); + configuration.valueValidators.set("mustBeJohn", value => ({ code: "mustBeJohn", isValid: value === "John" })); }); describe("AutomaticEntityValidator", () => { + testCoreValidatorFunctions( + () => { + const target = observable({ + firstName: "John", + }); + return new AutomaticEntityValidator(target, { + firstName: { required: true }, + }); + }, + () => { + const target = observable({ + firstName: "", + }); + return new AutomaticEntityValidator(target, { + firstName: { required: true }, + }); + }, + + () => { + const target = observable({ + firstName: "John", + }); + return new AutomaticEntityValidator(target, {}); + } + ); + it("works on empty validations", () => { const target = observable({ firstName: "John", @@ -18,160 +43,54 @@ describe("AutomaticEntityValidator", () => { const validator = new AutomaticEntityValidator(target, {}, false); expect(validator.isValid).toBeTruthy(); - expect(validator.errors.firstName).toBeUndefined(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); target.firstName = "Peter"; expect(validator.isValid).toBeTruthy(); - expect(validator.errors.firstName).toBeUndefined(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); }); - it("maintains value on empty string", () => { + it("initializes validations", () => { const target = observable({ firstName: "", }); - const validationRules = { - firstName: { - mustBeJohn: true, - }, - }; - - new AutomaticEntityValidator(target, validationRules, false); - expect(target.firstName).toBe(""); - }); - - it("evaluates validation on value change", () => { - const target = observable({ - firstName: "John", - }); - - const validationRules = { - firstName: { - mustBeJohn: true, + const validator = new AutomaticEntityValidator( + target, + { + firstName: { required: true }, }, - }; - - const validator = new AutomaticEntityValidator(target, validationRules, false); - - expect(validator.isValid).toBeTruthy(); - expect(validator.errors.firstName).toBeUndefined(); - - target.firstName = "Peter"; + false + ); expect(validator.isValid).toBeFalsy(); - expect(validator.errors.firstName).toBe("firstName must be John"); - }); - - it("initializes validation on empty field", () => { - const target = observable({}) as any; - - const validationRules = { - firstName: { - mustBeJohn: true, - }, - }; - - const validator = new AutomaticEntityValidator(target, validationRules, false); - - expect(validator.isValid).toBeFalsy(); - expect(validator.errors.firstName).toBe("firstName must be John"); - - target.firstName = "John"; - - expect(validator.isValid).toBeTruthy(); - expect(validator.errors.firstName).toBeUndefined(); - }); - - it("initializes validation on a non-observable entity", () => { - const target = {} as any; - - const validationRules = { - firstName: { - mustBeJohn: true, - }, - }; - - const validator = new AutomaticEntityValidator(target, validationRules, false); + expect(validator.checkValid("firstName")).toBeFalsy(); + expectInvalid(validator.getResults("firstName")); - expect(validator.isValid).toBeFalsy(); - expect(validator.errors.firstName).toBe("firstName must be John"); - - target.firstName = "John"; + target.firstName = "Peter"; expect(validator.isValid).toBeTruthy(); - expect(validator.errors.firstName).toBeUndefined(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); }); -}); -describe("createPropertyValidatorFromRules()", () => { - test("empty property rules", () => { - const entity = { - firstName: "John", - }; - - const validator = createPropertyValidatorFromRules("firstName", {}); - const validationResult = validator(entity.firstName, entity); - - expect(validationResult).toBeUndefined(); - }); - - test("validation error", () => { - const entity = { + it("returns validation code", () => { + const target = observable({ firstName: "", - }; - - const validator = createPropertyValidatorFromRules("firstName", { required: true }); - const validationResult = validator(entity.firstName, entity); - - expect(validationResult).toBe("Value is required"); - }); - - test("no validation error", () => { - const entity = { - firstName: "John", - }; - - const validator = createPropertyValidatorFromRules("firstName", { required: true }); - const validationResult = validator(entity.firstName, entity); - - expect(validationResult).toBeUndefined(); - }); - - test("all validators are called", () => { - const mockValidator1 = jest.fn(value => undefined); - validatorsRepository.set("mock1", mockValidator1); - - const mockValidator2 = jest.fn(value => undefined); - validatorsRepository.set("mock2", mockValidator2); - - const entity = { - firstName: "John", - }; - - const validator = createPropertyValidatorFromRules("firstName", { mock1: true, mock2: true }); - validator(entity.firstName, entity); - - expect(mockValidator1.mock.calls.length).toBe(1); - expect(mockValidator2.mock.calls.length).toBe(1); - }); - - test("parameters are passed to validator", () => { - const mockValidator = jest.fn(value => undefined); - validatorsRepository.set("mock", mockValidator); - - const entity = { - firstName: "John", - }; + }); - const params = { value: "val" }; - const validator = createPropertyValidatorFromRules("firstName", { mock: params }); - validator(entity.firstName, entity); + const validator = new AutomaticEntityValidator( + target, + { + firstName: { required: true }, + }, + false + ); - const calledParameters = mockValidator.mock.calls[0] as any[]; - expect(calledParameters[0]).toBe("John"); - expect(calledParameters[1]).toBe("firstName"); - expect(calledParameters[2]).toBe(entity); - expect(calledParameters[3]).toBe(params); + const validationErrors = validator.getResults("firstName"); + expect(validationErrors?.[0].code).toBe("required"); }); }); diff --git a/packages/validation/__tests__/integration.test.ts b/packages/validation/__tests__/integration.test.ts deleted file mode 100644 index 1c757853..00000000 --- a/packages/validation/__tests__/integration.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { observable } from "mobx"; -import * as Validation from "../src"; -import validatorsRepository from "../src/validatorsRepository"; - -class TestEntity { - @observable firstName: string; - @observable lastName: string; -} - -beforeAll(() => { - validatorsRepository.set("required", (value, propertyName, entity, params) => - !params || value ? undefined : `${propertyName} is required` - ); - validatorsRepository.set("equals", (value, propertyName, entity, params) => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - value === params.expectedValue ? undefined : `${propertyName} should be '${params.expectedValue}'` - ); -}); - -test("attachManualValidator()", () => { - const entity = new TestEntity(); - Validation.attachManualValidator(entity, false); - expect(Validation.isValid(entity)).toBeTruthy(); - expect(Validation.isValid(entity, "firstName")).toBeTruthy(); - - Validation.addError(entity, "firstName", "First name is required"); - expect(Validation.isValid(entity)).toBeFalsy(); - expect(Validation.isValid(entity, "firstName")).toBeFalsy(); - expect(Validation.isValid(entity, "lastName")).toBeTruthy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBeFalsy(); - - expect(Validation.validate(entity)).toBeFalsy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBe("First name is required"); - - Validation.addError(entity, "lastName", "Last name is required"); - expect(Validation.isValid(entity)).toBeFalsy(); - expect(Validation.isValid(entity, "lastName")).toBeFalsy(); - expect(Validation.getValidationMessage(entity, "lastName")).toBe("Last name is required"); - - Validation.removeError(entity, "firstName"); - expect(Validation.isValid(entity)).toBeFalsy(); - expect(Validation.isValid(entity, "firstName")).toBeTruthy(); - expect(Validation.isValid(entity, "lastName")).toBeFalsy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBeUndefined(); - - Validation.clearErrors(entity); - expect(Validation.isValid(entity)).toBeTruthy(); - expect(Validation.isValid(entity, "firstName")).toBeTruthy(); - expect(Validation.isValid(entity, "lastName")).toBeTruthy(); - expect(Validation.getValidationMessage(entity, "lastName")).toBeUndefined(); -}); - -test("attachAutomaticValidator()", () => { - const entity = new TestEntity(); - - const validationRules = { - firstName: { - equals: { expectedValue: "John" }, - required: true, - }, - lastName: { - required: false, - }, - }; - - Validation.attachAutomaticValidator(entity, validationRules, false); - expect(Validation.isValid(entity)).toBeFalsy(); - expect(Validation.isValid(entity, "firstName")).toBeFalsy(); - expect(Validation.isValid(entity, "lastName")).toBeTruthy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBeFalsy(); - expect(Validation.getValidationMessage(entity, "lastName")).toBeFalsy(); - - expect(Validation.validate(entity)).toBeFalsy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBe("firstName is required"); - - entity.firstName = "Jane"; - expect(Validation.getValidationMessage(entity, "firstName")).toBe("firstName should be 'John'"); - - entity.firstName = "John"; - expect(Validation.isValid(entity)).toBeTruthy(); - expect(Validation.isValid(entity, "firstName")).toBeTruthy(); - expect(Validation.getValidationMessage(entity, "firstName")).toBeFalsy(); -}); diff --git a/packages/validation/__tests__/manualEntityValidator.test.ts b/packages/validation/__tests__/manualEntityValidator.test.ts index d40635f1..3584b297 100644 --- a/packages/validation/__tests__/manualEntityValidator.test.ts +++ b/packages/validation/__tests__/manualEntityValidator.test.ts @@ -1,103 +1,96 @@ -import { autorun, get } from "mobx"; -import { attachManualValidator, getValidationMessage, validate } from "../src/helpers"; -import ManualEntityValidator, { addError, removeError } from "../src/manualEntityValidator"; +import { autorun } from "mobx"; +import ManualEntityValidator from "../src/manualEntityValidator"; +import { ValidationResult } from "../src/types"; +import { testCoreValidatorFunctions, expectInvalid, expectValid } from "./testHelpers"; interface ITarget { firstName: string; } describe("ManualEntityValidator", () => { - test("initial state is valid", () => { - const validator = new ManualEntityValidator(false); - - expect(validator.errors.firstName).toBeUndefined(); - expect(validator.isValid).toBeTruthy(); - }); + testCoreValidatorFunctions( + () => { + const validator = new ManualEntityValidator(); + validator.setResult("firstName", { code: "required", isValid: true }); + return validator; + }, + () => { + const validator = new ManualEntityValidator(); + validator.setResult("firstName", { code: "required", isValid: false }); + return validator; + }, + + () => { + return new ManualEntityValidator(); + } + ); test("adding and removing errors changes valid state", () => { const validator = new ManualEntityValidator(false); - validator.addError("firstName", "First name is wrong"); - expect(validator.errors.firstName).toBe("First name is wrong"); + validator.setResult("firstName", { code: "nameCheck", isValid: true }); + expect(validator.isValid).toBeTruthy(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); + + validator.setResult("firstName", { code: "nameCheck", isValid: false }); expect(validator.isValid).toBeFalsy(); + expect(validator.checkValid("firstName")).toBeFalsy(); + expectInvalid(validator.getResults("firstName")); - validator.removeError("firstName"); - expect(validator.errors.firstName).toBeUndefined(); + validator.clearResult("firstName", "nameCheck"); expect(validator.isValid).toBeTruthy(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); }); - test("clearErrors() removes all validation errors", () => { + test("clearResults(propertyName) removes validation results for property", () => { const validator = new ManualEntityValidator(false); - validator.addError("firstName", "First name is wrong"); - expect(validator.errors.firstName).toBe("First name is wrong"); + validator.setResult("firstName", { code: "nameCheck", isValid: false }); expect(validator.isValid).toBeFalsy(); + expect(validator.checkValid("firstName")).toBeFalsy(); + expectInvalid(validator.getResults("firstName")); - validator.clearErrors(); - expect(validator.errors.firstName).toBeUndefined(); + validator.clearResults("firstName"); expect(validator.isValid).toBeTruthy(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); }); - test("Reaction works without validator initialization", () => { + test("clearResults() removes all validation results", () => { const validator = new ManualEntityValidator(false); - let lastError = "Unknown" as string | undefined; - - const dispose = autorun(() => (lastError = get(validator.errors, "firstName"))); - expect(lastError).toBeUndefined(); - - validator.addError("firstName", "one"); - expect(lastError).toBe("one"); - - validator.addError("firstName", "two"); - expect(lastError).toBe("two"); - - validator.removeError("firstName"); - expect(lastError).toBeUndefined(); - - validator.addError("firstName", "three"); - expect(lastError).toBe("three"); + validator.setResult("firstName", { code: "nameCheck", isValid: false }); + expect(validator.isValid).toBeFalsy(); + expect(validator.checkValid("firstName")).toBeFalsy(); + expectInvalid(validator.getResults("firstName")); - dispose(); + validator.clearResults(); + expect(validator.isValid).toBeTruthy(); + expect(validator.checkValid("firstName")).toBeTruthy(); + expectValid(validator.getResults("firstName")); }); - test("Github Issue #11", () => { - const target = { - firstName: "John", - }; - - attachManualValidator(target, false); - expect(validate(target)).toBeTruthy(); - - addError(target, "firstName", "This is not valid"); - expect(validate(target)).toBeFalsy(); - - removeError(target, "firstName"); - expect(validate(target)).toBeTruthy(); - - addError(target, "firstName", "This is not valid again"); - expect(validate(target)).toBeFalsy(); - }); + test("Reaction works without validator initialization", () => { + const validator = new ManualEntityValidator(false); - test("Github Issue #27", () => { - const target = { - firstName: "John", - }; + let lastResults: Iterable | undefined = undefined; - let lastError = "Unknown" as string | undefined; + const dispose = autorun(() => (lastResults = validator.getResults("firstName"))); + expectValid(lastResults); - attachManualValidator(target, true); - const dispose = autorun(() => (lastError = getValidationMessage(target, "firstName"))); - expect(lastError).toBeUndefined(); + validator.setResult("firstName", { code: "nameCheck", isValid: false }); + expectInvalid(lastResults); - addError(target, "firstName", "This is not valid"); - expect(lastError).toBe("This is not valid"); + validator.setResult("firstName", { code: "nameCheck", isValid: true }); + expectValid(lastResults); - removeError(target, "firstName"); - expect(lastError).toBeUndefined(); + validator.setResult("firstName", { code: "required", isValid: false }); + expectInvalid(lastResults); - addError(target, "firstName", "This is not valid again"); - expect(lastError).toBe("This is not valid again"); + validator.clearResults(); + expectValid(lastResults); dispose(); }); diff --git a/packages/validation2/__tests__/serverEntityValidator.test.ts b/packages/validation/__tests__/serverEntityValidator.test.ts similarity index 100% rename from packages/validation2/__tests__/serverEntityValidator.test.ts rename to packages/validation/__tests__/serverEntityValidator.test.ts diff --git a/packages/validation2/__tests__/testHelpers.ts b/packages/validation/__tests__/testHelpers.ts similarity index 100% rename from packages/validation2/__tests__/testHelpers.ts rename to packages/validation/__tests__/testHelpers.ts diff --git a/packages/validation2/__tests__/tsconfig.json b/packages/validation/__tests__/tsconfig.json similarity index 100% rename from packages/validation2/__tests__/tsconfig.json rename to packages/validation/__tests__/tsconfig.json diff --git a/packages/validation2/src/aggregateEntityValidator.ts b/packages/validation/src/aggregateEntityValidator.ts similarity index 100% rename from packages/validation2/src/aggregateEntityValidator.ts rename to packages/validation/src/aggregateEntityValidator.ts diff --git a/packages/validation/src/automaticEntityValidator.ts b/packages/validation/src/automaticEntityValidator.ts index ae0a32f4..2cd25bf3 100644 --- a/packages/validation/src/automaticEntityValidator.ts +++ b/packages/validation/src/automaticEntityValidator.ts @@ -1,65 +1,102 @@ -import { ensureObservableProperty } from "@frui.ts/helpers"; -import { computed, extendObservable, get, observable } from "mobx"; -import { IEntityValidationRules, IEntityValidator, IPropertyValidationRules, ValidationErrors } from "./types"; -import validatorsRepository from "./validatorsRepository"; +import { PropertyName } from "@frui.ts/helpers"; +import { get, observable } from "mobx"; +import { EntityValidationRules, ValidationFunction } from "./automaticValidatorTypes"; +import DefaultConfiguration from "./configuration"; +import EntityValidatorBase, { emptyResults } from "./entityValidatorBase"; +import { ValidationResult } from "./types"; +import { attachValidator } from "./utils"; -type IPropertyBoundValidator = (propertyValue: any, entity: any) => string | undefined; -const TrueValidator: IPropertyBoundValidator = _ => undefined; +interface AutomaticEntityValidatorConfiguration { + valueValidators: Map; + resultMiddleware?: (result: ValidationResult) => ValidationResult; +} -/** Creates actual validator function for a particular property based on the provided validation rules */ -export function createPropertyValidatorFromRules(propertyName: string, propertyRules: IPropertyValidationRules) { - let finalValidator: IPropertyBoundValidator | undefined; +export default class AutomaticEntityValidator extends EntityValidatorBase { + private _results: Readonly, ValidationResult[]>>>; + private _validatedProperties: PropertyName[] = []; - for (const ruleName in propertyRules) { - const validator = validatorsRepository.get(ruleName); - if (!validator) { - throw new Error(`Unknown validator ${ruleName}. The validator must be registered in 'validatorsRepository'`); - } + constructor( + target: TEntity, + validationRules: EntityValidationRules, + isVisible = false, + private configuration: AutomaticEntityValidatorConfiguration = DefaultConfiguration + ) { + super(isVisible); - const params = propertyRules[ruleName] as unknown; - if (finalValidator) { - const temp = finalValidator; - finalValidator = (propertyValue, entity) => - validator(propertyValue, propertyName, entity, params) || temp(propertyValue, entity); - } else { - finalValidator = (propertyValue, entity) => validator(propertyValue, propertyName, entity, params); - } + this.buildObservableResults(target, validationRules); } - return finalValidator || TrueValidator; -} + *getAllResults(): Iterable<[PropertyName, Iterable]> { + if (!this.isEnabled) { + return; + } -/** Entity validator implementation that automatically observes validated entity's properties and maintains validation errors */ -export default class AutomaticEntityValidator> implements IEntityValidator { - @observable isErrorsVisible: boolean; + for (const propertyName of this._validatedProperties) { + const propertyResults = get(this._results, propertyName) as ValidationResult[] | undefined; + if (propertyResults) { + yield [propertyName, propertyResults]; + } + } + } - @observable errors: ValidationErrors = {}; - private validatedProperties: string[] = []; + getResults(propertyName: PropertyName): ValidationResult[] { + return (this.isEnabled && (get(this._results, propertyName) as ValidationResult[])) || emptyResults; + } - constructor(target: TTarget, entityValidationRules: IEntityValidationRules, isErrorsVisible: boolean) { - this.isErrorsVisible = isErrorsVisible; + private buildObservableResults(target: TEntity, entityValidationRules: EntityValidationRules) { + const errors: Partial, ValidationResult[]>> = {}; - for (const propertyName in entityValidationRules) { - const rules = (entityValidationRules as Record)[propertyName]; + for (const [name, propertyRules] of Object.entries | undefined>(entityValidationRules)) { + if (propertyRules) { + const propertyName = name as PropertyName; + this._validatedProperties.push(propertyName); - const validator = createPropertyValidatorFromRules(propertyName, rules); - if (!validator) { - continue; + Object.defineProperty(errors, propertyName, { + get: buildAggregatedErrorGetter(target, propertyName, propertyRules, this.configuration), + }); } + } - // TODO just add warning that the target property is not observable - ensureObservableProperty(target, propertyName, target[propertyName]); - this.validatedProperties.push(propertyName); + this._results = observable(errors); + } +} + +export function buildAggregatedErrorGetter( + entity: TEntity, + propertyName: PropertyName, + rules: Record, + { valueValidators, resultMiddleware }: AutomaticEntityValidatorConfiguration +): () => ValidationResult[] { + const validationCalls = Object.entries(rules).map(([validatorName, parameters]) => { + const validator = valueValidators.get(validatorName) as ValidationFunction | undefined; - extendObservable(this.errors, { - get [propertyName]() { - return validator(get(target, propertyName), target); - }, - }); + if (!validator) { + throw new Error( + `Unknown validator '${validatorName}'. The validator must be registered in 'configuration.valueValidators'` + ); } - } - @computed get isValid() { - return this.validatedProperties.every(prop => !get(this.errors, prop)); - } + const validationContext = { + parameters, + entity, + propertyName, + }; + + return (value: unknown) => validator(value, validationContext); + }); + + return () => { + const value = get(entity, propertyName) as unknown; + const results = validationCalls.flatMap(x => x(value)).filter((x): x is ValidationResult => !!x); // TODO optimize with Iterable? + return resultMiddleware ? results.map(resultMiddleware) : results; + }; +} + +export function attachAutomaticValidator( + target: TEntity, + validationRules: EntityValidationRules, + isVisible = false +) { + const automaticValidator = new AutomaticEntityValidator(target, validationRules, isVisible); + attachValidator(target, automaticValidator); } diff --git a/packages/validation2/src/automaticValidatorTypes.ts b/packages/validation/src/automaticValidatorTypes.ts similarity index 87% rename from packages/validation2/src/automaticValidatorTypes.ts rename to packages/validation/src/automaticValidatorTypes.ts index 919e5062..9b32937f 100644 --- a/packages/validation2/src/automaticValidatorTypes.ts +++ b/packages/validation/src/automaticValidatorTypes.ts @@ -1,17 +1,17 @@ import { PropertyName } from "@frui.ts/helpers"; import { AsyncValidationResult, ValidationResult } from "./types"; -export interface ValidationFunctionContext> { +export interface ValidationFunctionContext> { readonly parameters: TParameters; readonly entity: TEntity; readonly propertyName: TProperty; } -export interface ValidationFunction { +export interface ValidationFunction { (value: TValue, context: ValidationFunctionContext): ValidationResult | ValidationResult[] | undefined; } -export interface AsyncValidationFunction { +export interface AsyncValidationFunction { (value: TValue, context: ValidationFunctionContext): Promise< AsyncValidationResult | AsyncValidationResult[] | undefined >; diff --git a/packages/validation2/src/configuration.ts b/packages/validation/src/configuration.ts similarity index 84% rename from packages/validation2/src/configuration.ts rename to packages/validation/src/configuration.ts index 440790df..d5e632b1 100644 --- a/packages/validation2/src/configuration.ts +++ b/packages/validation/src/configuration.ts @@ -1,4 +1,4 @@ -import type { ValidationFunction, AsyncValidationFunction } from "./automaticValidatorTypes"; +import type { AsyncValidationFunction, ValidationFunction } from "./automaticValidatorTypes"; const ValidationLoading = Symbol("Validation loading"); diff --git a/packages/validation2/src/entityValidatorBase.ts b/packages/validation/src/entityValidatorBase.ts similarity index 100% rename from packages/validation2/src/entityValidatorBase.ts rename to packages/validation/src/entityValidatorBase.ts diff --git a/packages/validation/src/helpers.ts b/packages/validation/src/helpers.ts deleted file mode 100644 index dd6d623c..00000000 --- a/packages/validation/src/helpers.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { PropertyName } from "@frui.ts/helpers"; -import { get, runInAction } from "mobx"; -import AutomaticEntityValidator from "./automaticEntityValidator"; -import ManualEntityValidator from "./manualEntityValidator"; -import { IEntityValidationRules, IHasManualValidation, IHasValidation } from "./types"; - -/** - * Attaches a new [[AutomaticEntityValidator]] to the entity and returns the entity typed as [[IHasValidation]] - * @returns The target entity instance with `IHasValidation` implemented with the attached validator - */ -export function attachAutomaticValidator( - target: TTarget, - entityValidationRules: IEntityValidationRules, - errorsImmediatelyVisible = false -) { - const typedTarget = target as TTarget & IHasValidation; - typedTarget.__validation = new AutomaticEntityValidator(target, entityValidationRules, errorsImmediatelyVisible); - return typedTarget; -} - -/** - * Attaches a new [[ManualEntityValidator]] to the entity and returns the entity typed as [[IHasManualValidation]] - * @returns The target entity instance with `IHasManualValidation` implemented with the attached validator - */ -export function attachManualValidator(target: TTarget, errorsImmediatelyVisible = false) { - const typedTarget = target as TTarget & IHasManualValidation; - typedTarget.__validation = new ManualEntityValidator(errorsImmediatelyVisible); - return typedTarget; -} - -export function hasValidation(target: any): target is IHasValidation { - return (target as IHasValidation)?.__validation !== undefined; -} - -export function getValidationMessage(target: TTarget, propertyName: PropertyName): string | undefined { - if (hasValidation(target) && target.__validation.isErrorsVisible) { - return get(target.__validation.errors, propertyName) as string | undefined; - } - return undefined; -} - -export function isValid(target: TTarget, propertyName?: PropertyName) { - if (hasValidation(target)) { - return propertyName ? !get(target.__validation.errors, propertyName) : target.__validation.isValid; - } else { - return true; - } -} - -export function hasVisibleErrors(target: any) { - if (hasValidation(target)) { - return target.__validation.isErrorsVisible && !target.__validation.isValid; - } else { - return false; - } -} - -export function hasErrorsVisibilityEnabled(target: any) { - return hasValidation(target) && target.__validation.isErrorsVisible; -} - -export function hideValidationErrors(target: any) { - if (hasValidation(target) && target.__validation.isErrorsVisible) { - runInAction(() => (target.__validation.isErrorsVisible = false)); - } -} - -export function validate(target: any) { - if (hasValidation(target)) { - runInAction(() => (target.__validation.isErrorsVisible = true)); - return target.__validation.isValid; - } else { - return true; - } -} - -export function validateAll(items: any[]) { - return items.reduce((acc: boolean, item) => validate(item) && acc, true); -} diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index b45b7c5d..78b14396 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -1,11 +1,9 @@ -export { default as AutomaticEntityValidator } from "./automaticEntityValidator"; -export * from "./helpers"; -export { - addError, - clearErrors, - default as ManualEntityValidator, - hasManualEntityValidator, - removeError, -} from "./manualEntityValidator"; +export { default as AggregateEntityValidator } from "./aggregateEntityValidator"; +export { default as AutomaticEntityValidator, attachAutomaticValidator } from "./automaticEntityValidator"; +export * from "./automaticValidatorTypes"; +export { default as Configuration, ValidationLoading } from "./configuration"; +export { default as EntityValidatorBase } from "./entityValidatorBase"; +export { default as ManualEntityValidator } from "./manualEntityValidator"; +export { default as ServerEntityValidator } from "./serverEntityValidator"; export * from "./types"; -export { default as validatorsRepository, IValidator, IValidatorsRepository } from "./validatorsRepository"; +export * from "./utils"; diff --git a/packages/validation/src/manualEntityValidator.ts b/packages/validation/src/manualEntityValidator.ts index 7e9c12f6..49b3b8f7 100644 --- a/packages/validation/src/manualEntityValidator.ts +++ b/packages/validation/src/manualEntityValidator.ts @@ -1,58 +1,53 @@ import { PropertyName } from "@frui.ts/helpers"; -import { action, computed, observable, remove, set, values, keys } from "mobx"; -import { IHasManualValidation, IManualEntityValidator, ValidationErrors } from "./types"; +import { action, observable } from "mobx"; +import EntityValidatorBase, { emptyResults } from "./entityValidatorBase"; +import { ValidationResult } from "./types"; -/** Entity validator implementation acting as a simple validation errors list that needs to be manually maintained */ -export default class ManualEntityValidator implements IManualEntityValidator { - @observable isErrorsVisible: boolean; - @observable errors: ValidationErrors = {}; +export default class ManualEntityValidator extends EntityValidatorBase { + protected validationResults = observable.map, ValidationResult[]>(); - constructor(isErrorsVisible: boolean) { - this.isErrorsVisible = isErrorsVisible; + getAllResults(): Iterable<[PropertyName, ValidationResult[]]> { + return (this.isEnabled && this.validationResults.entries()) || emptyResults; } - @action - clearErrors() { - keys(this.errors).forEach(prop => void remove(this.errors, prop)); + getResults(propertyName: PropertyName): Iterable { + return (this.isEnabled && this.validationResults.get(propertyName)) || emptyResults; } @action - addError(propertyName: PropertyName, message: string) { - set(this.errors, propertyName, message); + setResult(propertyName: PropertyName, result: ValidationResult) { + const results = this.validationResults.get(propertyName); + if (results) { + const index = results.findIndex(x => x.code === result.code); + if (index < 0) { + results.push(result); + } else { + results.splice(index, 1, result); + } + } else { + this.validationResults.set(propertyName, observable.array([result])); + } } @action - removeError(propertyName: PropertyName) { - remove(this.errors, propertyName); - } - - @computed get isValid() { - return values(this.errors).every(x => !x); - } -} - -export function hasManualEntityValidator(target: any): target is IHasManualValidation { - return ( - !!target && - (target as IHasManualValidation).__validation !== undefined && - typeof (target as IHasManualValidation).__validation.addError === "function" - ); -} - -export function clearErrors(target: TTarget) { - if (hasManualEntityValidator(target)) { - target.__validation.clearErrors(); - } -} - -export function addError(target: TTarget, propertyName: PropertyName, message: string) { - if (hasManualEntityValidator(target)) { - target.__validation.addError(propertyName, message); - } -} - -export function removeError(target: TTarget, propertyName: PropertyName) { - if (hasManualEntityValidator(target)) { - target.__validation.removeError(propertyName); + clearResult(propertyName: PropertyName, resultCode: string) { + const results = this.validationResults.get(propertyName); + if (results) { + const index = results.findIndex(x => x.code === resultCode); + if (index >= 0) { + results.splice(index, 1); + } + } + } + + clearResults(): void; + clearResults(propertyName: PropertyName): void; + @action + clearResults(propertyName?: PropertyName) { + if (propertyName) { + this.validationResults.delete(propertyName); + } else { + this.validationResults.clear(); + } } } diff --git a/packages/validation2/src/serverEntityValidator.ts b/packages/validation/src/serverEntityValidator.ts similarity index 100% rename from packages/validation2/src/serverEntityValidator.ts rename to packages/validation/src/serverEntityValidator.ts diff --git a/packages/validation/src/types.ts b/packages/validation/src/types.ts index e95b2e5a..15b4dcf2 100644 --- a/packages/validation/src/types.ts +++ b/packages/validation/src/types.ts @@ -1,94 +1,37 @@ -import { PropertyName } from "@frui.ts/helpers"; +import type { PropertyName } from "@frui.ts/helpers"; +import type { ValidationLoading } from "./configuration"; -/** - * Contains validation errors for an entity. - * Each key is a property name and value is error message for the respective property. - * - * Example: - * ```ts - * { - * firstName: "The value is required", - * age: "Age must be a positive number" - * } - * ``` - */ -export type ValidationErrors = Partial, string>>; +export interface ValidationResult { + readonly code: string; + readonly isValid: boolean; + readonly isLoading?: boolean; -/** Validator attached to an entity reponsible for maintaining validation errors */ -export interface IEntityValidator { - /** Returns `true` when the validated entity has no validation errors, otherwise `false` */ - isValid: boolean; - - /** Indicates whether existing validation errors should be displayed to the user */ - isErrorsVisible: boolean; - - /** Validation errors for the validated entity */ - errors: Readonly>; + message?: string; + messageParameters?: Record; } -/** Validator with manually maintained validation errors */ -export interface IManualEntityValidator extends IEntityValidator { - addError(propertyName: PropertyName, message: string): void; - removeError(propertyName: PropertyName): void; - clearErrors(): void; +export interface AsyncValidationResult extends ValidationResult { + readonly isLoading: false; } -/** - * Contains validation definition for the whole entity. - * Each key represents a single validated property. - * - * Example: - * ```ts - * { - * firstName: { - * required: true, - * maxLength: 20 - * }, - * age: { - * range: { min: 0,, max: 99 } - * } - * } - * ``` - */ -export type IEntityValidationRules = Partial< - Record, IPropertyValidationRules> ->; +export type AggregatedValidationResult = boolean | typeof ValidationLoading; -/** - * Contains validation definition for the whole entity. - * Each key represents a single validated property. - */ -export type ITypedEntityValidationRules = Partial< - Record, ITypedPropertyValidationRules> ->; +export type EntityValidationResults = Partial, ValidationResult[]>>; -/** - * Contains validation rules for a single property. - * Each key represents a validation rule, with rule-specific params as value. - * You can restrict the list of possible keys - * - * Example: - * ```ts - * { - * required: true, - * range: { min: 0,, max: 99 } - * } - * ``` - */ -export type IPropertyValidationRules = Partial>; +export interface EntityValidator { + isEnabled: boolean; + isVisible: boolean; + readonly isValid: AggregatedValidationResult; -/** - * Contains validation rules for a single property, - * based on a TRules type with all rules and parameters. - */ -export type ITypedPropertyValidationRules = IPropertyValidationRules & Partial; + getAllResults(): Iterable<[PropertyName, Iterable]>; + getResults(propertyName: PropertyName): Iterable; -/** Represents an entity with attached entity validator */ -export interface IHasValidation { - __validation: IEntityValidator; -} + getAllVisibleResults(): Iterable<[PropertyName, Iterable]>; + getVisibleResults(propertyName: PropertyName): Iterable; + + checkValid(): AggregatedValidationResult; + checkValid(propertyName: PropertyName): AggregatedValidationResult; -/** Represents an entity with attached manual entity validator */ -export interface IHasManualValidation extends IHasValidation { - __validation: IManualEntityValidator; + checkValidAsync(): Promise; + checkValidAsync(propertyName: PropertyName): Promise; } diff --git a/packages/validation2/src/utils.ts b/packages/validation/src/utils.ts similarity index 70% rename from packages/validation2/src/utils.ts rename to packages/validation/src/utils.ts index 3d41e479..b58b2dff 100644 --- a/packages/validation2/src/utils.ts +++ b/packages/validation/src/utils.ts @@ -1,3 +1,4 @@ +import { PropertyName } from "@frui.ts/helpers"; import { runInAction } from "mobx"; import { Configuration } from "."; import { AggregatedValidationResult, EntityValidator } from "./types"; @@ -12,6 +13,25 @@ export function getValidator(target: TEntity) { return (target as any)[Configuration.validatorAttachedProperty] as EntityValidator | undefined; } +/** + * @deprecated Used for back-compatibility with the previous version of @frui.ts/validation + */ +export function getValidationMessage(target: TEntity, propertyName: PropertyName): string | undefined { + const validator = getValidator(target); + if (validator) { + for (const result of validator.getVisibleResults(propertyName)) { + if (!result.isValid) { + const message = result.message || result.code; + if (message) { + return message; + } + } + } + } + + return undefined; +} + export function checkValid(target: unknown): AggregatedValidationResult { const validator = getValidator(target); return validator ? validator.checkValid() : true; diff --git a/packages/validation/src/validatorsRepository.ts b/packages/validation/src/validatorsRepository.ts deleted file mode 100644 index 45f5ef4c..00000000 --- a/packages/validation/src/validatorsRepository.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Validator function - * - * @param propertyValue The actual value that should be validated - * @param propertyName Name of the validated property. This might be useful for error message. - * @param entity The whole entity being validated. This might be useful for validations related to other properties. - * @param params Parameters defined in the validation rule - * - * @returns A `string` with validation error message when the validation fails, otherwise `undefined` - */ -export type IValidator = (propertyValue: any, propertyName: string, entity: any, params: any) => string | undefined; - -export type IValidatorsRepository = Map; - -const validatorsRepository = new Map() as IValidatorsRepository; - -/** - * Repository of validators used by [[AutomaticEntityValidator]] - * - * You can add a custom validator: - * ```ts - * validatorsRepository.set("required", (value, propertyName, entity, params) => (value == null) ? undefined : `${propertyName} is required.`); - * ``` - * And then use it in validation rules: - * ``` - * { - * firstName: { - * required: true // the 'true' value is passed to the validator as 'params' - * } - * } - * ``` - * - * @see [[IEntityValidationRules]] [[IPropertyValidationRules]] - */ -export default validatorsRepository; diff --git a/packages/validation2/__tests__/automaticEntityValidator.test.ts b/packages/validation2/__tests__/automaticEntityValidator.test.ts deleted file mode 100644 index e8367397..00000000 --- a/packages/validation2/__tests__/automaticEntityValidator.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { observable } from "mobx"; -import AutomaticEntityValidator from "../src/automaticEntityValidator"; -import configuration from "../src/configuration"; -import { testCoreValidatorFunctions, expectInvalid, expectValid } from "./testHelpers"; - -beforeAll(() => { - configuration.valueValidators.set("required", value => ({ code: "required", isValid: !!value })); - configuration.valueValidators.set("mustBeJohn", value => ({ code: "mustBeJohn", isValid: value === "John" })); -}); - -describe("AutomaticEntityValidator", () => { - testCoreValidatorFunctions( - () => { - const target = observable({ - firstName: "John", - }); - return new AutomaticEntityValidator(target, { - firstName: { required: true }, - }); - }, - () => { - const target = observable({ - firstName: "", - }); - return new AutomaticEntityValidator(target, { - firstName: { required: true }, - }); - }, - - () => { - const target = observable({ - firstName: "John", - }); - return new AutomaticEntityValidator(target, {}); - } - ); - - it("works on empty validations", () => { - const target = observable({ - firstName: "John", - }); - - const validator = new AutomaticEntityValidator(target, {}, false); - - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - - target.firstName = "Peter"; - - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - }); - - it("initializes validations", () => { - const target = observable({ - firstName: "", - }); - - const validator = new AutomaticEntityValidator( - target, - { - firstName: { required: true }, - }, - false - ); - - expect(validator.isValid).toBeFalsy(); - expect(validator.checkValid("firstName")).toBeFalsy(); - expectInvalid(validator.getResults("firstName")); - - target.firstName = "Peter"; - - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - }); - - it("returns validation code", () => { - const target = observable({ - firstName: "", - }); - - const validator = new AutomaticEntityValidator( - target, - { - firstName: { required: true }, - }, - false - ); - - const validationErrors = validator.getResults("firstName"); - expect(validationErrors?.[0].code).toBe("required"); - }); -}); diff --git a/packages/validation2/__tests__/manualEntityValidator.test.ts b/packages/validation2/__tests__/manualEntityValidator.test.ts deleted file mode 100644 index 3584b297..00000000 --- a/packages/validation2/__tests__/manualEntityValidator.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { autorun } from "mobx"; -import ManualEntityValidator from "../src/manualEntityValidator"; -import { ValidationResult } from "../src/types"; -import { testCoreValidatorFunctions, expectInvalid, expectValid } from "./testHelpers"; - -interface ITarget { - firstName: string; -} - -describe("ManualEntityValidator", () => { - testCoreValidatorFunctions( - () => { - const validator = new ManualEntityValidator(); - validator.setResult("firstName", { code: "required", isValid: true }); - return validator; - }, - () => { - const validator = new ManualEntityValidator(); - validator.setResult("firstName", { code: "required", isValid: false }); - return validator; - }, - - () => { - return new ManualEntityValidator(); - } - ); - - test("adding and removing errors changes valid state", () => { - const validator = new ManualEntityValidator(false); - - validator.setResult("firstName", { code: "nameCheck", isValid: true }); - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - - validator.setResult("firstName", { code: "nameCheck", isValid: false }); - expect(validator.isValid).toBeFalsy(); - expect(validator.checkValid("firstName")).toBeFalsy(); - expectInvalid(validator.getResults("firstName")); - - validator.clearResult("firstName", "nameCheck"); - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - }); - - test("clearResults(propertyName) removes validation results for property", () => { - const validator = new ManualEntityValidator(false); - - validator.setResult("firstName", { code: "nameCheck", isValid: false }); - expect(validator.isValid).toBeFalsy(); - expect(validator.checkValid("firstName")).toBeFalsy(); - expectInvalid(validator.getResults("firstName")); - - validator.clearResults("firstName"); - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - }); - - test("clearResults() removes all validation results", () => { - const validator = new ManualEntityValidator(false); - - validator.setResult("firstName", { code: "nameCheck", isValid: false }); - expect(validator.isValid).toBeFalsy(); - expect(validator.checkValid("firstName")).toBeFalsy(); - expectInvalid(validator.getResults("firstName")); - - validator.clearResults(); - expect(validator.isValid).toBeTruthy(); - expect(validator.checkValid("firstName")).toBeTruthy(); - expectValid(validator.getResults("firstName")); - }); - - test("Reaction works without validator initialization", () => { - const validator = new ManualEntityValidator(false); - - let lastResults: Iterable | undefined = undefined; - - const dispose = autorun(() => (lastResults = validator.getResults("firstName"))); - expectValid(lastResults); - - validator.setResult("firstName", { code: "nameCheck", isValid: false }); - expectInvalid(lastResults); - - validator.setResult("firstName", { code: "nameCheck", isValid: true }); - expectValid(lastResults); - - validator.setResult("firstName", { code: "required", isValid: false }); - expectInvalid(lastResults); - - validator.clearResults(); - expectValid(lastResults); - - dispose(); - }); -}); diff --git a/packages/validation2/package.json b/packages/validation2/package.json deleted file mode 100644 index 5c3d91d7..00000000 --- a/packages/validation2/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@frui.ts/validation2", - "publishConfig": { - "access": "public" - }, - "version": "999.0.0", - "description": "Observable validation", - "keywords": [ - "front-end", - "framework", - "mvvm", - "validation", - "mobx" - ], - "homepage": "https://github.com/eManPrague/frui.ts", - "repository": { - "type": "git", - "url": "git+https://github.com/eManPrague/frui.ts.git" - }, - "bugs": { - "url": "https://github.com/eManPrague/frui.ts/issues" - }, - "author": "Augustin Šulc ", - "license": "MIT", - "main": "dist/index.js", - "directories": { - "lib": "dist", - "test": "__tests__" - }, - "files": [ - "dist" - ], - "scripts": { - "clean": "rimraf dist", - "build": "tsc" - }, - "dependencies": { - "@frui.ts/helpers": "^999.0.0" - }, - "peerDependencies": { - "mobx": "^4.0.0 || ^5.0.0" - } -} diff --git a/packages/validation2/src/automaticEntityValidator.ts b/packages/validation2/src/automaticEntityValidator.ts deleted file mode 100644 index ca006154..00000000 --- a/packages/validation2/src/automaticEntityValidator.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { PropertyName } from "@frui.ts/helpers"; -import { get, observable } from "mobx"; -import { EntityValidationRules, ValidationFunction } from "./automaticValidatorTypes"; -import DefaultConfiguration from "./configuration"; -import EntityValidatorBase, { emptyResults } from "./entityValidatorBase"; -import { ValidationResult } from "./types"; - -interface AutomaticEntityValidatorConfiguration { - valueValidators: Map; - resultMiddleware?: (result: ValidationResult) => ValidationResult; -} - -export default class AutomaticEntityValidator extends EntityValidatorBase { - private _results: Readonly, ValidationResult[]>>>; - private _validatedProperties: PropertyName[] = []; - - constructor( - target: TEntity, - entityValidationRules: EntityValidationRules, - isVisible = false, - private configuration: AutomaticEntityValidatorConfiguration = DefaultConfiguration - ) { - super(isVisible); - - this.buildObservableResults(target, entityValidationRules); - } - - *getAllResults(): Iterable<[PropertyName, Iterable]> { - if (!this.isEnabled) { - return; - } - - for (const propertyName of this._validatedProperties) { - const propertyResults = get(this._results, propertyName) as ValidationResult[] | undefined; - if (propertyResults) { - yield [propertyName, propertyResults]; - } - } - } - - getResults(propertyName: PropertyName): ValidationResult[] { - return (this.isEnabled && (get(this._results, propertyName) as ValidationResult[])) || emptyResults; - } - - private buildObservableResults(target: TEntity, entityValidationRules: EntityValidationRules) { - const errors: Partial, ValidationResult[]>> = {}; - - for (const [name, propertyRules] of Object.entries | undefined>(entityValidationRules)) { - if (propertyRules) { - const propertyName = name as PropertyName; - this._validatedProperties.push(propertyName); - - Object.defineProperty(errors, propertyName, { - get: buildAggregatedErrorGetter(target, propertyName, propertyRules, this.configuration), - }); - } - } - - this._results = observable(errors); - } -} - -export function buildAggregatedErrorGetter( - entity: TEntity, - propertyName: PropertyName, - rules: Record, - { valueValidators, resultMiddleware }: AutomaticEntityValidatorConfiguration -): () => ValidationResult[] { - const validationCalls = Object.entries(rules).map(([validatorName, parameters]) => { - const validator = valueValidators.get(validatorName) as ValidationFunction | undefined; - - if (!validator) { - throw new Error( - `Unknown validator '${validatorName}'. The validator must be registered in 'configuration.valueValidators'` - ); - } - - const validationContext = { - parameters, - entity, - propertyName, - }; - - return (value: unknown) => validator(value, validationContext); - }); - - return () => { - const value = get(entity, propertyName) as unknown; - const results = validationCalls.flatMap(x => x(value)).filter((x): x is ValidationResult => !!x); // TODO optimize with Iterable? - return resultMiddleware ? results.map(resultMiddleware) : results; - }; -} diff --git a/packages/validation2/src/index.ts b/packages/validation2/src/index.ts deleted file mode 100644 index e2b482bd..00000000 --- a/packages/validation2/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default as AggregateEntityValidator } from "./aggregateEntityValidator"; -export { default as AutomaticEntityValidator } from "./automaticEntityValidator"; -export * from "./automaticValidatorTypes"; -export { default as Configuration, ValidationLoading } from "./configuration"; -export { default as EntityValidatorBase } from "./entityValidatorBase"; -export { default as ManualEntityValidator } from "./manualEntityValidator"; -export { default as ServerEntityValidator } from "./serverEntityValidator"; -export * from "./types"; -export * from "./utils"; diff --git a/packages/validation2/src/manualEntityValidator.ts b/packages/validation2/src/manualEntityValidator.ts deleted file mode 100644 index 49b3b8f7..00000000 --- a/packages/validation2/src/manualEntityValidator.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PropertyName } from "@frui.ts/helpers"; -import { action, observable } from "mobx"; -import EntityValidatorBase, { emptyResults } from "./entityValidatorBase"; -import { ValidationResult } from "./types"; - -export default class ManualEntityValidator extends EntityValidatorBase { - protected validationResults = observable.map, ValidationResult[]>(); - - getAllResults(): Iterable<[PropertyName, ValidationResult[]]> { - return (this.isEnabled && this.validationResults.entries()) || emptyResults; - } - - getResults(propertyName: PropertyName): Iterable { - return (this.isEnabled && this.validationResults.get(propertyName)) || emptyResults; - } - - @action - setResult(propertyName: PropertyName, result: ValidationResult) { - const results = this.validationResults.get(propertyName); - if (results) { - const index = results.findIndex(x => x.code === result.code); - if (index < 0) { - results.push(result); - } else { - results.splice(index, 1, result); - } - } else { - this.validationResults.set(propertyName, observable.array([result])); - } - } - - @action - clearResult(propertyName: PropertyName, resultCode: string) { - const results = this.validationResults.get(propertyName); - if (results) { - const index = results.findIndex(x => x.code === resultCode); - if (index >= 0) { - results.splice(index, 1); - } - } - } - - clearResults(): void; - clearResults(propertyName: PropertyName): void; - @action - clearResults(propertyName?: PropertyName) { - if (propertyName) { - this.validationResults.delete(propertyName); - } else { - this.validationResults.clear(); - } - } -} diff --git a/packages/validation2/src/types.ts b/packages/validation2/src/types.ts deleted file mode 100644 index 15b4dcf2..00000000 --- a/packages/validation2/src/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PropertyName } from "@frui.ts/helpers"; -import type { ValidationLoading } from "./configuration"; - -export interface ValidationResult { - readonly code: string; - readonly isValid: boolean; - readonly isLoading?: boolean; - - message?: string; - messageParameters?: Record; -} - -export interface AsyncValidationResult extends ValidationResult { - readonly isLoading: false; -} - -export type AggregatedValidationResult = boolean | typeof ValidationLoading; - -export type EntityValidationResults = Partial, ValidationResult[]>>; - -export interface EntityValidator { - isEnabled: boolean; - isVisible: boolean; - readonly isValid: AggregatedValidationResult; - - getAllResults(): Iterable<[PropertyName, Iterable]>; - getResults(propertyName: PropertyName): Iterable; - - getAllVisibleResults(): Iterable<[PropertyName, Iterable]>; - getVisibleResults(propertyName: PropertyName): Iterable; - - checkValid(): AggregatedValidationResult; - checkValid(propertyName: PropertyName): AggregatedValidationResult; - - checkValidAsync(): Promise; - checkValidAsync(propertyName: PropertyName): Promise; -} diff --git a/packages/validation2/tsconfig.json b/packages/validation2/tsconfig.json deleted file mode 100644 index 08539848..00000000 --- a/packages/validation2/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": true, - "inlineSources": true, - "outDir": "./dist" - }, - "include": [ - "./src" - ] -} diff --git a/packages/validation2/yarn.lock b/packages/validation2/yarn.lock deleted file mode 100644 index fb57ccd1..00000000 --- a/packages/validation2/yarn.lock +++ /dev/null @@ -1,4 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - diff --git a/stories/package.json b/stories/package.json index d1168b60..5f33e482 100644 --- a/stories/package.json +++ b/stories/package.json @@ -37,7 +37,6 @@ "@frui.ts/htmlcontrols": "^999.0.0", "@frui.ts/screens": "^999.0.0", "@frui.ts/validation": "^999.0.0", - "@frui.ts/validation2": "^999.0.0", "@frui.ts/views": "^999.0.0", "bootstrap": "^4.5.3", "mobx": "^4.15.7", diff --git a/stories/src/bootstrap.stories.tsx b/stories/src/bootstrap.stories.tsx index 4a59c4a8..49b25eb2 100644 --- a/stories/src/bootstrap.stories.tsx +++ b/stories/src/bootstrap.stories.tsx @@ -1,7 +1,7 @@ import "!style-loader!css-loader!bootstrap/dist/css/bootstrap.css"; import { Check, Input, Select } from "@frui.ts/bootstrap"; import { attachAutomaticDirtyWatcher } from "@frui.ts/dirtycheck"; -import { attachAutomaticValidator, IEntityValidationRules, validatorsRepository } from "@frui.ts/validation"; +import { attachAutomaticValidator, Configuration, EntityValidationRules } from "@frui.ts/validation"; import { storiesOf } from "@storybook/react"; import { action, observable } from "mobx"; import { Observer } from "mobx-react-lite"; @@ -21,10 +21,18 @@ const observableTarget = observable({ isInactive: false, isThreeState: null, selectedItemKey: undefined as string | undefined, - selectedItem: undefined as any, + selectedItem: undefined as unknown, }); -const validationRules: IEntityValidationRules = { +Configuration.valueValidators.set("required", (value, context) => { + if (value) { + return undefined; + } else { + return { code: "required", isValid: false, message: `${context.propertyName} is required` }; + } +}); + +const validationRules: EntityValidationRules = { name: { required: true, }, @@ -32,9 +40,7 @@ const validationRules: IEntityValidationRules = { required: true, }, }; -validatorsRepository.set("required", (value, propertyName, entity, params) => - !params || value ? undefined : `${propertyName} is required.` -); + attachAutomaticValidator(observableTarget, validationRules, true); attachAutomaticDirtyWatcher(observableTarget, true); diff --git a/stories/src/validation2/automaticEntityValidator-deep.stories.tsx b/stories/src/validation2/automaticEntityValidator-deep.stories.tsx index e4d08bf0..6f508e75 100644 --- a/stories/src/validation2/automaticEntityValidator-deep.stories.tsx +++ b/stories/src/validation2/automaticEntityValidator-deep.stories.tsx @@ -1,5 +1,5 @@ import { Check, Input } from "@frui.ts/bootstrap"; -import { AutomaticEntityValidator, Configuration, EntityValidationRules, ValidationResult } from "@frui.ts/validation2"; +import { AutomaticEntityValidator, Configuration, EntityValidationRules, ValidationResult } from "@frui.ts/validation"; import { storiesOf } from "@storybook/react"; import { action, observable } from "mobx"; import { Observer } from "mobx-react-lite"; diff --git a/stories/src/validation2/automaticEntityValidator.stories.tsx b/stories/src/validation2/automaticEntityValidator.stories.tsx index 4cd62883..c01dbdd2 100644 --- a/stories/src/validation2/automaticEntityValidator.stories.tsx +++ b/stories/src/validation2/automaticEntityValidator.stories.tsx @@ -1,5 +1,5 @@ import { Check, Input } from "@frui.ts/bootstrap"; -import { AutomaticEntityValidator, Configuration, EntityValidationRules, ValidationResult } from "@frui.ts/validation2"; +import { AutomaticEntityValidator, Configuration, EntityValidationRules, ValidationResult } from "@frui.ts/validation"; import { storiesOf } from "@storybook/react"; import { action, observable } from "mobx"; import { Observer } from "mobx-react-lite"; @@ -12,9 +12,9 @@ import "!style-loader!css-loader!./style.css"; Configuration.valueValidators.set("required", (value, context) => { console.log("validating 'required'", value, context); if (value) { - return { code: "required", isValid: true }; - } else { return undefined; + } else { + return { code: "required", isValid: false }; } }); Configuration.valueValidators.set("isJohn", (value, context) => { diff --git a/stories/src/validation2/manualEntityValidator.stories.tsx b/stories/src/validation2/manualEntityValidator.stories.tsx index 834de6b2..e290efb7 100644 --- a/stories/src/validation2/manualEntityValidator.stories.tsx +++ b/stories/src/validation2/manualEntityValidator.stories.tsx @@ -1,5 +1,5 @@ import { Check, Input } from "@frui.ts/bootstrap"; -import { ManualEntityValidator } from "@frui.ts/validation2"; +import { ManualEntityValidator } from "@frui.ts/validation"; import { storiesOf } from "@storybook/react"; import { observable } from "mobx"; import React from "react"; diff --git a/stories/src/validation2/validationErrors.tsx b/stories/src/validation2/validationErrors.tsx index 47e55d5c..c6d5048d 100644 --- a/stories/src/validation2/validationErrors.tsx +++ b/stories/src/validation2/validationErrors.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react-lite"; -import { EntityValidator } from "@frui.ts/validation2"; +import { EntityValidator } from "@frui.ts/validation"; import React from "react"; function validationErrors({ validator, property }: { validator: EntityValidator; property: string }) { diff --git a/stories/src/validation2/validityIndicator.tsx b/stories/src/validation2/validityIndicator.tsx index 29e61448..1f55426b 100644 --- a/stories/src/validation2/validityIndicator.tsx +++ b/stories/src/validation2/validityIndicator.tsx @@ -1,4 +1,4 @@ -import { EntityValidator } from "@frui.ts/validation2"; +import { EntityValidator } from "@frui.ts/validation"; import { observer } from "mobx-react-lite"; import React from "react";