c008e874e5
Fixes https://github.com/twentyhq/private-issues/issues/410#issuecomment-3781085655 Currently, JSON keys with spaces like { "toto toto": 123 } are rejected with "JSON keys cannot contain spaces" error. This is problematic for HTTP requests and webhook triggers where users cannot control the response structure. We use Handlebars to eval variables, segment-literal bracket notation to escape keys with special characters: Normal: {{step.normalKey}} With spaces: `{{step.[key with space]}}` So we simply need to wrap segments with spaces with brackets. This PR: - Create shared path utilities to wrap the variable segments when needed - Use it in all variable generation places - Remove the restrictions This body is now supported: <img width="609" height="457" alt="Capture d’écran 2026-01-22 à 16 10 13" src="https://github.com/user-attachments/assets/e7653c0a-df1e-49af-9c9a-4b7d59a99726" />
183 lines
4.8 KiB
TypeScript
183 lines
4.8 KiB
TypeScript
import { resolveInput } from '../variable-resolver';
|
|
|
|
describe('resolveInput', () => {
|
|
const context = {
|
|
user: {
|
|
name: 'John Doe',
|
|
age: 30,
|
|
},
|
|
settings: {
|
|
theme: 'dark',
|
|
notifications: true,
|
|
},
|
|
specialValues: {
|
|
nullValue: null,
|
|
undefinedValue: undefined,
|
|
emptyString: '',
|
|
zero: 0,
|
|
booleanFalse: false,
|
|
booleanTrue: true,
|
|
},
|
|
};
|
|
|
|
it('should return null for null input', () => {
|
|
expect(resolveInput(null, context)).toBeNull();
|
|
});
|
|
|
|
it('should support special values', () => {
|
|
expect(resolveInput('{{specialValues.nullValue}}', context)).toBeNull();
|
|
expect(
|
|
resolveInput('{{specialValues.undefinedValue}}', context),
|
|
).toBeUndefined();
|
|
expect(resolveInput('{{specialValues.emptyString}}', context)).toBe('');
|
|
expect(resolveInput('{{specialValues.zero}}', context)).toBe(0);
|
|
expect(resolveInput('{{specialValues.booleanFalse}}', context)).toBe(false);
|
|
expect(resolveInput('{{specialValues.booleanTrue}}', context)).toBe(true);
|
|
});
|
|
|
|
it('should return undefined for undefined input', () => {
|
|
expect(resolveInput(undefined, context)).toBeUndefined();
|
|
});
|
|
|
|
it('should resolve a simple string variable', () => {
|
|
expect(resolveInput('{{user.name}}', context)).toBe('John Doe');
|
|
});
|
|
|
|
it('should resolve multiple variables in a string', () => {
|
|
expect(
|
|
resolveInput('Name: {{user.name}}, Age: {{user.age}}', context),
|
|
).toBe('Name: John Doe, Age: 30');
|
|
});
|
|
|
|
it('should handle non-existent variables', () => {
|
|
expect(resolveInput('{{user.email}}', context)).toBe(undefined);
|
|
});
|
|
|
|
it('should resolve variables in an array', () => {
|
|
const input = ['{{user.name}}', '{{settings.theme}}', 'static'];
|
|
const expected = ['John Doe', 'dark', 'static'];
|
|
|
|
expect(resolveInput(input, context)).toEqual(expected);
|
|
});
|
|
|
|
it('should resolve variables in an object', () => {
|
|
const input = {
|
|
name: '{{user.name}}',
|
|
theme: '{{settings.theme}}',
|
|
static: 'value',
|
|
};
|
|
const expected = {
|
|
name: 'John Doe',
|
|
theme: 'dark',
|
|
static: 'value',
|
|
};
|
|
|
|
expect(resolveInput(input, context)).toEqual(expected);
|
|
});
|
|
|
|
it('should handle nested objects and arrays', () => {
|
|
const input = {
|
|
user: {
|
|
displayName: '{{user.name}}',
|
|
preferences: ['{{settings.theme}}', '{{settings.notifications}}'],
|
|
},
|
|
staticData: [1, 2, 3],
|
|
};
|
|
const expected = {
|
|
user: {
|
|
displayName: 'John Doe',
|
|
preferences: ['dark', true],
|
|
},
|
|
staticData: [1, 2, 3],
|
|
};
|
|
|
|
expect(resolveInput(input, context)).toEqual(expected);
|
|
});
|
|
|
|
it('does not wrap string variables with double quotes', () => {
|
|
expect(
|
|
resolveInput('{ {{test}}: 2 }', {
|
|
test: 'prop',
|
|
}),
|
|
).toBe('{ prop: 2 }');
|
|
});
|
|
|
|
it('does not modify static JSON', () => {
|
|
expect(resolveInput('{ "a": 2 }', {})).toBe('{ "a": 2 }');
|
|
});
|
|
|
|
it('supports variable as JSON object property name', () => {
|
|
expect(
|
|
resolveInput('{ "{{test}}": 2 }', {
|
|
test: 'prop',
|
|
}),
|
|
).toBe('{ "prop": 2 }');
|
|
});
|
|
|
|
it('supports variable as JSON number value', () => {
|
|
expect(
|
|
resolveInput('{ "a": {{test}} }', {
|
|
test: 2,
|
|
}),
|
|
).toBe('{ "a": 2 }');
|
|
});
|
|
|
|
it('supports variable as JSON string value', () => {
|
|
expect(
|
|
resolveInput('{ "a": "{{test}}" }', {
|
|
test: 'str',
|
|
}),
|
|
).toBe('{ "a": "str" }');
|
|
});
|
|
|
|
describe('bracket notation for keys with special characters', () => {
|
|
it('should resolve variables with keys containing spaces', () => {
|
|
const contextWithSpaces = {
|
|
step: {
|
|
'key with space': 'value from space key',
|
|
},
|
|
};
|
|
expect(resolveInput('{{step.[key with space]}}', contextWithSpaces)).toBe(
|
|
'value from space key',
|
|
);
|
|
});
|
|
|
|
it('should resolve nested variables with keys containing spaces', () => {
|
|
const contextWithSpaces = {
|
|
step: {
|
|
'first key': {
|
|
'nested key': 'nested value',
|
|
},
|
|
},
|
|
};
|
|
expect(
|
|
resolveInput('{{step.[first key].[nested key]}}', contextWithSpaces),
|
|
).toBe('nested value');
|
|
});
|
|
|
|
it('should resolve mixed normal and bracket notation paths', () => {
|
|
const contextWithMixed = {
|
|
step: {
|
|
normal: {
|
|
'key with space': 42,
|
|
},
|
|
},
|
|
};
|
|
expect(
|
|
resolveInput('{{step.normal.[key with space]}}', contextWithMixed),
|
|
).toBe(42);
|
|
});
|
|
|
|
it('should resolve variables with keys containing dots', () => {
|
|
const contextWithDots = {
|
|
step: {
|
|
'key.with.dots': 'dotted value',
|
|
},
|
|
};
|
|
expect(resolveInput('{{step.[key.with.dots]}}', contextWithDots)).toBe(
|
|
'dotted value',
|
|
);
|
|
});
|
|
});
|
|
});
|