feat(workflow) - Add validation layer (#21422)
Add workflow validation framework and consolidate output schema types/search logic into twenty-shared This PR introduces a comprehensive workflow validation system that catches configuration errors at build-time, and consolidates the fragmented output-schema type definitions and variable-search logic from the front-end into twenty-shared **Workflow validation** — A new system that checks workflows for errors before activation: graph connectivity (unreachable steps, dangling references), step parameter schemas (via Zod), variable references (typos, wrong step order), and workspace metadata (non-existent objects). Returns structured errors/warnings with "did you mean?" suggestions. Runs automatically after create_complete_workflow and update_workflow_version_step, and is also available as a standalone validate_workflow tool. **Output schema consolidation** — Moves all output schema types and the variable-search logic from scattered front-end files into twenty-shared, replacing ~800 lines of duplicated per-schema-type code with a single unified searchVariableInOutputSchema dispatcher. To do : - validation on CODE and AGENT step --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,28 +1,30 @@
|
||||
import { checkUrlType } from '~/utils/checkUrlType';
|
||||
import { LinkType } from 'twenty-ui-deprecated/navigation';
|
||||
|
||||
describe('checkUrlType', () => {
|
||||
it('should return "linkedin", if linkedin url', () => {
|
||||
expect(checkUrlType('https://www.linkedin.com/in/håkan-fisk')).toBe(
|
||||
'linkedin',
|
||||
it('should detect LinkedIn urls', () => {
|
||||
expect(checkUrlType('https://www.linkedin.com/in/john')).toBe(
|
||||
LinkType.LinkedIn,
|
||||
);
|
||||
expect(checkUrlType('http://www.linkedin.com/in/håkan-fisk')).toBe(
|
||||
'linkedin',
|
||||
expect(checkUrlType('linkedin.com/company/twenty')).toBe(LinkType.LinkedIn);
|
||||
});
|
||||
|
||||
it('should detect Twitter urls', () => {
|
||||
expect(checkUrlType('https://twitter.com/twenty')).toBe(LinkType.Twitter);
|
||||
});
|
||||
|
||||
it('should detect X urls as Twitter', () => {
|
||||
expect(checkUrlType('https://x.com/twenty')).toBe(LinkType.Twitter);
|
||||
});
|
||||
|
||||
it('should detect Facebook urls', () => {
|
||||
expect(checkUrlType('https://www.facebook.com/twenty')).toBe(
|
||||
LinkType.Facebook,
|
||||
);
|
||||
expect(checkUrlType('https://linkedin.com/in/håkan-fisk')).toBe('linkedin');
|
||||
expect(checkUrlType('http://linkedin.com/in/håkan-fisk')).toBe('linkedin');
|
||||
expect(checkUrlType('linkedin.com/in/håkan-fisk')).toBe('linkedin');
|
||||
});
|
||||
|
||||
it('should return "twitter", if twitter url', () => {
|
||||
expect(checkUrlType('https://www.twitter.com/john-doe')).toBe('twitter');
|
||||
expect(checkUrlType('https://www.x.com/john-doe')).toBe('twitter');
|
||||
});
|
||||
|
||||
it('should return "url", if neither linkedin nor twitter url', () => {
|
||||
expect(checkUrlType('https://www.example.com')).toBe('url');
|
||||
});
|
||||
|
||||
it('should return "facebook", if facebook url', () => {
|
||||
expect(checkUrlType('https://www.facebook.com/john-doe')).toBe('facebook');
|
||||
it('should fall back to a generic url type', () => {
|
||||
expect(checkUrlType('https://example.com')).toBe(LinkType.Url);
|
||||
expect(checkUrlType('not-a-url')).toBe(LinkType.Url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { compareNonEmptyStrings } from '~/utils/compareNonEmptyStrings';
|
||||
|
||||
describe('compareNonEmptyStrings', () => {
|
||||
it('should return true when both values are empty or nullish', () => {
|
||||
expect(compareNonEmptyStrings(null, undefined)).toBe(true);
|
||||
expect(compareNonEmptyStrings('', null)).toBe(true);
|
||||
expect(compareNonEmptyStrings('', '')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when both values are equal non-empty strings', () => {
|
||||
expect(compareNonEmptyStrings('foo', 'foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when values differ', () => {
|
||||
expect(compareNonEmptyStrings('foo', 'bar')).toBe(false);
|
||||
expect(compareNonEmptyStrings('foo', '')).toBe(false);
|
||||
expect(compareNonEmptyStrings('foo', null)).toBe(false);
|
||||
});
|
||||
});
|
||||
+12
-37
@@ -1,49 +1,24 @@
|
||||
import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
|
||||
|
||||
describe('compareStrictlyExceptForNullAndUndefined', () => {
|
||||
it('should return true for undefined === null', () => {
|
||||
it('should return true when both values are nullish', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(null, undefined)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(compareStrictlyExceptForNullAndUndefined(undefined, null)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for null === undefined', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(null, undefined)).toBe(
|
||||
true,
|
||||
);
|
||||
it('should compare strictly when at least one value is defined', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(1, 1)).toBe(true);
|
||||
expect(compareStrictlyExceptForNullAndUndefined('a', 'a')).toBe(true);
|
||||
expect(compareStrictlyExceptForNullAndUndefined(1, 2)).toBe(false);
|
||||
expect(compareStrictlyExceptForNullAndUndefined(1, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for undefined === undefined', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(undefined, undefined)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for null === null', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(null, null)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for 2 === 2', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(2, 2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for 2 === 3', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(2, 3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined === 2', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(undefined, 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null === 2', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(null, 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for 2 === "2"', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(2, '2')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for "2" === "2"', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined('2', '2')).toBe(true);
|
||||
it('should not treat zero or empty string as nullish', () => {
|
||||
expect(compareStrictlyExceptForNullAndUndefined(0, null)).toBe(false);
|
||||
expect(compareStrictlyExceptForNullAndUndefined('', undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user