Skip to content

Commit

Permalink
0.5.0. (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
b4rtaz authored Jul 1, 2023
1 parent 5b14906 commit 9fd55a8
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 82 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 0.5.0

The `DefinitionValidator` class supports the validation of the whole definition. Use the `validate` method to validate a definition deeply. This method will validate all steps in the definition at once. Now you may easily validate a definition in the back-end before saving it to the storage.

```ts
const validator = DefinitionValidator.create(definitionModel, definitionWalker);
if (validator.validate(definition)) {
throw new Error('Invalid definition');
}
```

**Breaking changes:**

* Renamed the `ModelValidator` class to `DefinitionValidator`.

## 0.4.1

* The model validator is improved. Now the validator validates the `name` field of the step as well.
Expand Down
8 changes: 4 additions & 4 deletions demos/webpack-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
},
"dependencies": {
"xstate": "^4.37.2",
"sequential-workflow-model": "^0.1.3",
"sequential-workflow-designer": "^0.13.3",
"sequential-workflow-model": "^0.1.4",
"sequential-workflow-designer": "^0.13.4",
"sequential-workflow-machine": "^0.2.0",
"sequential-workflow-editor-model": "^0.4.1",
"sequential-workflow-editor": "^0.4.1"
"sequential-workflow-editor-model": "^0.5.0",
"sequential-workflow-editor": "^0.5.0"
},
"devDependencies": {
"ts-loader": "^9.4.2",
Expand Down
10 changes: 5 additions & 5 deletions editor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sequential-workflow-editor",
"version": "0.4.1",
"version": "0.5.0",
"type": "module",
"main": "./lib/esm/index.js",
"types": "./lib/index.d.ts",
Expand Down Expand Up @@ -46,12 +46,12 @@
"prettier:fix": "prettier --write ./src ./css"
},
"dependencies": {
"sequential-workflow-editor-model": "^0.4.1",
"sequential-workflow-model": "^0.1.3"
"sequential-workflow-editor-model": "^0.5.0",
"sequential-workflow-model": "^0.1.4"
},
"peerDependencies": {
"sequential-workflow-editor-model": "^0.4.1",
"sequential-workflow-model": "^0.1.3"
"sequential-workflow-editor-model": "^0.5.0",
"sequential-workflow-model": "^0.1.4"
},
"devDependencies": {
"rollup": "^3.20.2",
Expand Down
10 changes: 5 additions & 5 deletions editor/src/editor-provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model';
import { Editor } from './editor';
import { DefinitionContext, DefinitionModel, ModelActivator, ModelValidator, Path } from 'sequential-workflow-editor-model';
import { DefinitionContext, DefinitionModel, ModelActivator, DefinitionValidator, Path } from 'sequential-workflow-editor-model';
import { EditorServices, ValueEditorEditorFactoryResolver } from './value-editors';
import {
GlobalEditorContext,
Expand All @@ -22,7 +22,7 @@ export class EditorProvider<TDefinition extends Definition> {
): EditorProvider<TDef> {
const definitionWalker = configuration.definitionWalker ?? new DefinitionWalker();
const activator = ModelActivator.create(definitionModel, configuration.uidGenerator);
const validator = ModelValidator.create(definitionModel, definitionWalker);
const validator = DefinitionValidator.create(definitionModel, definitionWalker);
return new EditorProvider(activator, validator, definitionModel, definitionWalker, configuration);
}

Expand All @@ -33,7 +33,7 @@ export class EditorProvider<TDefinition extends Definition> {

private constructor(
private readonly activator: ModelActivator<TDefinition>,
private readonly validator: ModelValidator,
private readonly validator: DefinitionValidator,
private readonly definitionModel: DefinitionModel,
private readonly definitionWalker: DefinitionWalker,
private readonly configuration: EditorProviderConfiguration
Expand Down Expand Up @@ -85,13 +85,13 @@ export class EditorProvider<TDefinition extends Definition> {

public createStepValidator(): StepValidator {
return (step: Step, _: unknown, definition: Definition): boolean => {
return this.validator.validateStep(step, definition);
return this.validator.validateStep(step, definition) === null;
};
}

public createRootValidator(): RootValidator {
return (definition: Definition): boolean => {
return this.validator.validateRoot(definition);
return this.validator.validateRoot(definition) === null;
};
}

Expand Down
6 changes: 3 additions & 3 deletions model/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sequential-workflow-editor-model",
"version": "0.4.1",
"version": "0.5.0",
"homepage": "https://nocode-js.com/",
"author": {
"name": "NoCode JS",
Expand Down Expand Up @@ -45,10 +45,10 @@
"test": "jest --clearCache && jest --watchAll"
},
"dependencies": {
"sequential-workflow-model": "^0.1.3"
"sequential-workflow-model": "^0.1.4"
},
"peerDependencies": {
"sequential-workflow-model": "^0.1.3"
"sequential-workflow-model": "^0.1.4"
},
"devDependencies": {
"typescript": "^4.9.5",
Expand Down
4 changes: 2 additions & 2 deletions model/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export interface CustomValidator<TValue extends PropertyValue = PropertyValue, T
validate(context: CustomValidatorContext<TValue, TProperties>): string | null;
}

export type ValidationResult = Record<string, string | null> | null;
export type ValidationSingleError = Record<'$', string>;
export type ValidationError = Record<string, string | null>;
export type ValidationResult = ValidationError | null;

export function createValidationSingleError(error: string): ValidationResult {
return {
Expand Down
158 changes: 158 additions & 0 deletions model/src/validator/definition-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model';
import { createDefinitionModel, createRootModel, createStepModel } from '../builders';
import { numberValueModel } from '../value-models';
import { DefinitionValidator } from './definition-validator';

interface FooDefinition extends Definition {
properties: {
velocity: number;
};
}

interface FooStep extends Step {
properties: {
delta: number;
};
}

describe('DefinitionValidator', () => {
const model = createDefinitionModel<FooDefinition>(builder => {
builder.root(
createRootModel(root => {
root.property('velocity').value(
numberValueModel({
min: 0
})
);
})
);

builder.steps([
createStepModel<FooStep>('move', 'task', step => {
step.property('delta').value(
numberValueModel({
max: 0
})
);
})
]);
});
const walker = new DefinitionWalker();
const validator = DefinitionValidator.create(model, walker);

it('returns error when root is invalid', () => {
const def: FooDefinition = {
sequence: [],
properties: {
velocity: -1 // invalid
}
};

const error = validator.validate(def);

expect(error?.stepId).toEqual(null);
expect(error?.propertyPath.toString()).toEqual('properties/velocity');
expect(error?.error.$).toEqual('The value must be at least 0.');
});

it('returns error when step has invalid delta value', () => {
const def: FooDefinition = {
sequence: [
{
type: 'move',
componentType: 'task',
id: '0x000000',
name: 'Correct',
properties: {
delta: -100
}
},
{
type: 'move',
componentType: 'task',
id: '0xFFFFFF',
name: 'Invalid!',
properties: {
delta: 1 // invalid
}
}
],
properties: {
velocity: 100
}
};

const error = validator.validate(def);

expect(error?.stepId).toEqual('0xFFFFFF');
expect(error?.propertyPath.toString()).toEqual('properties/delta');
expect(error?.error.$).toEqual('The value must be at most 0.');
});

it('returns error when step has invalid name', () => {
const def: FooDefinition = {
sequence: [
{
type: 'move',
componentType: 'task',
id: '0xAAAAAA',
name: '', // invalid
properties: {
delta: 1
}
}
],
properties: {
velocity: 100
}
};

const error = validator.validate(def);

expect(error?.stepId).toEqual('0xAAAAAA');
expect(error?.propertyPath.toString()).toEqual('name');
expect(error?.error.$).toEqual('The value must be at least 1 characters long.');
});

it('returns null when definition is valid', () => {
const def: FooDefinition = {
sequence: [
{
type: 'move',
componentType: 'task',
id: '0x000000',
name: 'Right',
properties: {
delta: -100
}
}
],
properties: {
velocity: 100
}
};

const error = validator.validate(def);

expect(error).toBeNull();
});

it('throws error when step type is not supported', () => {
const def: FooDefinition = {
sequence: [
{
type: 'not_supported_type',
componentType: 'task',
id: '0x000000',
name: 'Right',
properties: {}
}
],
properties: {
velocity: 100
}
};

expect(() => validator.validate(def)).toThrowError('Cannot find model for step type: not_supported_type');
});
});
106 changes: 106 additions & 0 deletions model/src/validator/definition-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model';
import { DefinitionModel, PropertyModel, PropertyModels, ValidationError, ValidationResult, createValidationSingleError } from '../model';
import { DefinitionContext, ValueContext } from '../context';
import { CustomValidatorContext } from './custom-validator-context';
import { Path } from '../core';

export class DefinitionValidator {
public static create(definitionModel: DefinitionModel, definitionWalker: DefinitionWalker): DefinitionValidator {
return new DefinitionValidator(definitionModel, definitionWalker);
}

private constructor(private readonly model: DefinitionModel, private readonly walker: DefinitionWalker) {}

/**
* Deeply validates the given definition.
* @param definition The definition to validate.
* @returns `null` if the definition is valid, otherwise an object describing the validation error.
*/
public validate(definition: Definition): DefinitionValidationError | null {
const rootError = this.validateRoot(definition);
if (rootError) {
return {
...rootError,
stepId: null
};
}

let result: DefinitionValidationError | null = null;
this.walker.forEach(definition, step => {
const stepError = this.validateStep(step, definition);
if (stepError) {
result = {
...stepError,
stepId: step.id
};
return false; // stop walking
}
});
return result;
}

public validateStep(step: Step, definition: Definition): PropertyValidationError | null {
const definitionContext = DefinitionContext.createForStep(step, definition, this.model, this.walker);

const stepModel = this.model.steps[step.type];
if (!stepModel) {
throw new Error(`Cannot find model for step type: ${step.type}`);
}

const nameError = this.validateProperty(stepModel.name, definitionContext);
if (nameError) {
return {
propertyPath: stepModel.name.path,
error: nameError
};
}
return this.validateProperties(stepModel.properties, definitionContext);
}

public validateRoot(definition: Definition): PropertyValidationError | null {
const definitionContext = DefinitionContext.createForRoot(definition, this.model, this.walker);
return this.validateProperties(this.model.root.properties, definitionContext);
}

private validateProperties(properties: PropertyModels, definitionContext: DefinitionContext): PropertyValidationError | null {
for (const propertyName of properties) {
const error = this.validateProperty(propertyName, definitionContext);
if (error) {
return {
propertyPath: propertyName.path,
error
};
}
}
return null;
}

private validateProperty(propertyModel: PropertyModel, definitionContext: DefinitionContext): ValidationResult {
const valueContext = ValueContext.create(propertyModel.value, propertyModel, definitionContext);
const valueError = propertyModel.value.validate(valueContext);
if (valueError) {
return valueError;
}

if (propertyModel.customValidator) {
const customContext = CustomValidatorContext.create(propertyModel, definitionContext);
const customError = propertyModel.customValidator.validate(customContext);
if (customError) {
return createValidationSingleError(customError);
}
}
return null;
}
}

export interface PropertyValidationError {
propertyPath: Path;
error: ValidationError;
}

export interface DefinitionValidationError extends PropertyValidationError {
/**
* Step id. If it is `null` then the error is related to the root.
*/
stepId: string | null;
}
Loading

0 comments on commit 9fd55a8

Please sign in to comment.