Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,753 @@
import { ApFile, LATEST_CONTEXT_VERSION, PieceAuth, Property } from '@activepieces/pieces-framework'
import { FlowActionType, FlowTriggerType, GenericStepOutput, PropertyExecutionType, PropertySettings, StepOutputStatus } from '@activepieces/shared'
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
import { StepExecutionPath } from '../../src/lib/handler/context/step-execution-path'
import { propsProcessor } from '../../src/lib/variables/props-processor'
import { createPropsResolver } from '../../src/lib/variables/props-resolver'
const propsResolverService = createPropsResolver({
projectId: 'PROJECT_ID',
engineToken: 'WORKER_TOKEN',
apiUrl: 'http://127.0.0.1:3000',
contextVersion: LATEST_CONTEXT_VERSION,
})
const executionState = FlowExecutorContext.empty()
.upsertStep(
'trigger',
GenericStepOutput.create({
type: FlowTriggerType.PIECE,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: {
items: [5, 'a'],
name: 'John',
price: 6.4,
users: [
{
name: 'Alice',
},
{
name: 'Bob',
},
],
lastNames: [
'Smith',
'Doe',
],
},
}),
)
.upsertStep('step_1',
GenericStepOutput.create({
type: FlowActionType.PIECE,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: {
success: true,
},
}))
.upsertStep('step_2', GenericStepOutput.create({
type: FlowActionType.PIECE,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
}))
describe('Props resolver', () => {
test('Test resolve inside nested loops', async () => {
const modifiedExecutionState = executionState.upsertStep('step_3', GenericStepOutput.create({
type: FlowActionType.LOOP_ON_ITEMS,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: {
iterations: [
{
'step_8': GenericStepOutput.create({
type: FlowActionType.PIECE,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: {
delayForInMs: 20000,
success: true,
},
}),
'step_4': GenericStepOutput.create({
type: FlowActionType.LOOP_ON_ITEMS,
status: StepOutputStatus.SUCCEEDED,
input: {},
output: {
iterations: [
{
'step_7': GenericStepOutput.create({
'type': FlowActionType.PIECE,
'status': StepOutputStatus.SUCCEEDED,
'input': {
'unit': 'seconds',
'delayFor': '20',
},
'output': {
'delayForInMs': 20000,
'success': true,
},
}),
},
],
item: 1,
index: 0,
},
}),
},
],
item: 1,
index: 0,
},
})).setCurrentPath(StepExecutionPath.empty()
.loopIteration({
loopName: 'step_3',
iteration: 0,
})
.loopIteration({
loopName: 'step_4',
iteration: 0,
}),
)
const { resolvedInput: secondLevelResolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_7.delayForInMs}}', executionState: modifiedExecutionState })
expect(secondLevelResolvedInput).toEqual(20000)
const { resolvedInput: firstLevelResolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_8.delayForInMs}}', executionState: modifiedExecutionState })
expect(firstLevelResolvedInput).toEqual(20000)
})
test('Test resolve text with no variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'Hello world!', executionState })
expect(resolvedInput).toEqual(
'Hello world!',
)
})
test('Test resolve text with double variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'Price is {{ trigger.price }}', executionState })
expect(resolvedInput,
).toEqual('Price is 6.4')
})
test('Test resolve object steps variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{ {"where": "a"} }}', executionState })
expect(resolvedInput).toEqual(
{
where: 'a',
},
)
})
test('Test resolve object steps variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger}}', executionState })
expect(resolvedInput).toEqual(
{
items: [5, 'a'],
name: 'John',
price: 6.4,
users: [
{
name: 'Alice',
},
{
name: 'Bob',
},
],
lastNames: [
'Smith',
'Doe',
],
},
)
})
test('flatten array path', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{flattenNestedKeys(trigger, [\'users\',\'name\'])}}', executionState })
expect(resolvedInput).toEqual(['Alice', 'Bob'])
})
test('merge multiple flatten array paths', async ()=>{
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{flattenNestedKeys(trigger, [\'users\',\'name\'])}} {{trigger.lastNames}}', executionState })
expect(resolvedInput).toEqual(['Alice Smith', 'Bob Doe'])
})
test('Test resolve steps variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.name}}', executionState })
expect(resolvedInput).toEqual(
'John',
)
})
test('Test resolve multiple variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.name}} {{trigger.name}}', executionState })
expect(
resolvedInput,
).toEqual('John John')
})
test('Test resolve variable array items', async () => {
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput:
'{{trigger.items[0]}} {{trigger.items[1]}}',
executionState,
})
expect(
resolvedInput,
).toEqual('5 a')
})
test('Test resolve array variable', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.items}}', executionState })
expect(resolvedInput).toEqual(
[5, 'a'],
)
})
test('Test resolve integer from variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.items[0]}}', executionState })
expect(
resolvedInput,
).toEqual(5)
})
test('Test resolve text with undefined variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput:
'test {{configs.bar}} {{trigger.items[4]}}',
executionState,
})
expect(
resolvedInput,
).toEqual('test ')
})
test('Test resolve empty text', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '', executionState })
expect(resolvedInput).toEqual('')
})
test('Test resolve empty variable operator', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{}}', executionState })
expect(resolvedInput).toEqual('')
})
test('Test resolve object', async () => {
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput:
{
input: {
foo: 'bar',
nums: [1, 2, '{{trigger.items[0]}}'],
var: '{{trigger.price}}',
},
},
executionState,
})
expect(
resolvedInput,
).toEqual({ input: { foo: 'bar', nums: [1, 2, 5], var: 6.4 } })
})
test('Test resolve boolean from variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_1.success}}', executionState })
expect(resolvedInput).toEqual(
true,
)
})
test('Test resolve addition from variables', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.price + 2 - 3}}', executionState })
expect(resolvedInput).toEqual(
6.4 + 2 - 3,
)
})
test('Test resolve text with array variable', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'items are {{trigger.items}}', executionState })
expect(
resolvedInput,
).toEqual('items are [5,"a"]')
})
test('Test resolve text with object variable', async () => {
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput:
'values from trigger step: {{trigger}}',
executionState,
})
expect(
resolvedInput,
).toEqual('values from trigger step: {"items":[5,"a"],"name":"John","price":6.4,"users":[{"name":"Alice"},{"name":"Bob"}],"lastNames":["Smith","Doe"]}')
})
test('Test use built-in Math Min function', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{Math.min(trigger.price + 2 - 3, 2)}}', executionState })
expect(resolvedInput).toEqual(
2,
)
})
test('Test use built-in Math Max function', async () => {
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{Math.max(trigger.price + 2, 2)}}', executionState })
expect(resolvedInput).toEqual(
8.4,
)
})
it('should not compress memory file in native value in non-logs mode', async () => {
const input = {
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
}
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput: input,
executionState,
})
expect(resolvedInput).toEqual({
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
})
})
it('should not compress memory file in referenced value in non-logs mode', async () => {
const input = {
base64: '{{step_2}}',
}
const { resolvedInput } = await propsResolverService.resolve({
unresolvedInput: input,
executionState,
})
expect(resolvedInput).toEqual({
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
})
})
it('should return base64 from base64 with mime only', async () => {
const input = {
base64WithMime: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
base64: 'iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
}
const props = {
base64WithMime: Property.File({
displayName: 'Base64',
required: true,
}),
base64: Property.File({
displayName: 'Base64',
required: true,
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput).toEqual({
base64: null,
base64WithMime: new ApFile('unknown.png', Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z', 'base64'), 'png'),
})
expect(errors).toEqual({
'base64': [
'Expected file url or base64 with mimeType, received: iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
],
})
})
it('should resolve files inside the array properties', async () => {
const input = {
documents: [
{
file: 'https://cdn.activepieces.com/brand/logo.svg?token=123',
},
],
}
const props = {
documents: Property.Array({
displayName: 'Documents',
required: true,
properties: {
file: Property.File({
displayName: 'File',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.documents[0].file).toBeDefined()
expect(processedInput.documents[0].file.extension).toBe('svg')
expect(processedInput.documents[0].file.filename).toBe('logo.svg')
expect(errors).toEqual({})
})
it('should return error for invalid file inside the array properties', async () => {
const input = {
documents: [
{
file: 'invalid-url',
},
],
}
const props = {
documents: Property.Array({
displayName: 'Documents',
required: true,
properties: {
file: Property.File({
displayName: 'File',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.documents[0].file).toBeNull()
expect(errors).toEqual({
'documents': {
properties: [{
file: [
'Expected file url or base64 with mimeType, received: invalid-url',
],
}],
},
})
})
it('should return images for image url', async () => {
const input = {
file: 'https://cdn.activepieces.com/brand/logo.svg?token=123',
}
const props = {
file: Property.File({
displayName: 'File',
required: true,
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.file).toBeDefined()
expect(processedInput.file.extension).toBe('svg')
expect(processedInput.file.filename).toBe('logo.svg')
expect(errors).toEqual({})
})
// Test with invalid url
it('should return error for invalid data', async () => {
const input = {
file: 'https://google.com',
nullFile: null,
nullOptionalFile: null,
}
const props = {
file: Property.File({
displayName: 'File',
required: true,
}),
nullFile: Property.File({
displayName: 'File',
required: true,
}),
nullOptionalFile: Property.File({
displayName: 'File',
required: false,
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.file).toBeDefined()
expect(processedInput.file.extension).toBe('html')
expect(processedInput.file.filename).toBe('unknown.html')
expect(processedInput.nullFile).toBeNull()
expect(processedInput.nullOptionalFile).toBeNull()
expect(errors).toEqual({
'nullFile': [
'Expected file url or base64 with mimeType, received: null',
],
})
})
it('should return casted number for text', async () => {
const input = {
price: '0',
auth: {
age: '12',
},
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, {
price: Property.Number({
displayName: 'Price',
required: true,
}),
}, PieceAuth.CustomAuth({
required: true,
props: {
age: Property.Number({
displayName: 'age',
required: true,
}),
},
}), true, {})
expect(processedInput).toEqual({
auth: {
age: 12,
},
price: 0,
})
expect(errors).toEqual({})
})
it('should not error if auth configured, but no auth provided in input', async () => {
const input = {
price: '0',
}
const props = {
price: Property.Number({
displayName: 'Price',
required: true,
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.CustomAuth({
required: true,
props: {},
}), false, {})
expect(processedInput).toEqual({
price: 0,
})
expect(errors).toEqual({})
})
it('should flatten arrays inside DYNAMIC properties', async () => {
const input = {
dynamicProp: {
items: {
id: [1, 2],
name: ['Item 1', 'Item 2'],
},
},
}
const propertySettings: Record<string, PropertySettings> = {
dynamicProp: {
type: PropertyExecutionType.MANUAL,
schema: {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.Number({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: true,
}),
},
}),
},
},
}
const props = {
dynamicProp: Property.DynamicProperties({
auth: undefined,
displayName: 'Dynamic Property',
required: true,
props: async () => {
return {}
},
refreshers: [],
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, propertySettings)
expect(processedInput.dynamicProp.items).toEqual([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
])
expect(errors).toEqual({})
})
})
describe('Array Flatter Processor', () => {
it('should flatten array of objects', async () => {
const input = {
items: {
id: [1, 2],
name: ['Item 1', 'Item 2'],
},
}
const props = {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.Number({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.items).toEqual([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
])
expect(errors).toEqual({})
})
it('should handle non-array properties gracefully', async () => {
const input = {
items: {
id: [1, 2],
name: 'Single Item', // Non-array property
},
}
const props = {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.Number({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.items).toEqual([
{ id: 1, name: 'Single Item' },
{ id: 2, name: 'Single Item' },
])
expect(errors).toEqual({})
})
it('should handle arrays of unequal length', async () => {
const input = {
items: {
id: [1, 2, 3], // Longer array
name: ['Item 1', 'Item 2'], // Shorter array
},
}
const props = {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.Number({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: false,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.items).toEqual([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: undefined }, // Handle missing name
])
expect(errors).toEqual({})
})
it('should convert number to string for ShortText properties', async () => {
const input = {
items: {
id: 123,
name: 'Item Name',
},
}
const props = {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.ShortText({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.items).toEqual([
{ id: '123', name: 'Item Name' },
])
expect(errors).toEqual({})
})
it('should handle arrays with string values', async () => {
const input = {
items: {
id: '1',
name: 'item1',
},
}
const props = {
items: Property.Array({
displayName: 'Items',
required: true,
properties: {
id: Property.ShortText({
displayName: 'ID',
required: true,
}),
name: Property.LongText({
displayName: 'Name',
required: true,
}),
},
}),
}
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
expect(processedInput.items).toEqual([
{ id: '1', name: 'item1' },
])
expect(errors).toEqual({})
})
})

View File

@@ -0,0 +1,342 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework'
import { propsProcessor } from '../../src/lib/variables/props-processor'
describe('Property Validation', () => {
describe('required properties', () => {
it('should validate required string property', async () => {
const props = {
text: Property.ShortText({
displayName: 'Text',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ text: 'valid text' },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
{ text: null },
props,
PieceAuth.None(),
false,
{},
)
expect(nullErrors).toEqual({
text: ['Expected string, received: null'],
})
const { errors: undefinedErrors } = await propsProcessor.applyProcessorsAndValidators(
{ text: undefined },
props,
PieceAuth.None(),
false,
{},
)
expect(undefinedErrors).toEqual({
text: ['Expected string, received: undefined'],
})
})
it('should validate required number property', async () => {
const props = {
number: Property.Number({
displayName: 'Number',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ number: 42 },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
{ number: null },
props,
PieceAuth.None(),
false,
{},
)
expect(nullErrors).toEqual({
number: ['Expected number, received: null'],
})
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
{ number: 'not a number' },
props,
PieceAuth.None(),
false,
{},
)
expect(typeErrors).toEqual({
number: ['Expected number, received: not a number'],
})
})
it('should validate required datetime property', async () => {
const props = {
date: Property.DateTime({
displayName: 'DateTime',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ date: '2024-03-14T12:00:00.000Z' },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: invalidErrors } = await propsProcessor.applyProcessorsAndValidators(
{ date: 'not a date' },
props,
PieceAuth.None(),
false,
{},
)
expect(invalidErrors).toEqual({
date: ['Invalid datetime format. Expected ISO format (e.g. 2024-03-14T12:00:00.000Z), received: not a date'],
})
})
it('should validate required array property', async () => {
const props = {
array: Property.Array({
displayName: 'Array',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ array: [1, 2, 3] },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
{ array: 'not an array' },
props,
PieceAuth.None(),
false,
{},
)
expect(typeErrors).toEqual({
array: ['Expected array, received: not an array'],
})
})
it('should validate required json property', async () => {
const props = {
json: Property.Json({
displayName: 'JSON',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: { key: 'value' } },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: validJsonStringErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: '{"key": "value"}' },
props,
PieceAuth.None(),
false,
{},
)
expect(validJsonStringErrors).toEqual({})
const { errors: validArrayErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: [1, 2, 3] },
props,
PieceAuth.None(),
false,
{},
)
expect(validArrayErrors).toEqual({})
const { errors: validArrayStringErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: '[1, 2, 3]' },
props,
PieceAuth.None(),
false,
{},
)
expect(validArrayStringErrors).toEqual({})
const { errors: invalidJsonErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: 'not a json object' },
props,
PieceAuth.None(),
false,
{},
)
expect(invalidJsonErrors).toEqual({
json: ['Expected JSON, received: not a json object'],
})
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
{ json: null },
props,
PieceAuth.None(),
false,
{},
)
expect(nullErrors).toEqual({
json: ['Expected JSON, received: null'],
})
})
it('should validate required object property', async () => {
const props = {
object: Property.Object({
displayName: 'Object',
required: true,
}),
}
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
{ object: { key: 'value' } },
props,
PieceAuth.None(),
false,
{},
)
expect(validErrors).toEqual({})
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
{ object: null },
props,
PieceAuth.None(),
false,
{},
)
expect(nullErrors).toEqual({
object: ['Expected object, received: null'],
})
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
{ object: 'not an object' },
props,
PieceAuth.None(),
false,
{},
)
expect(typeErrors).toEqual({
object: ['Expected object, received: not an object'],
})
const { errors: jsonStringErrors } = await propsProcessor.applyProcessorsAndValidators(
{ object: JSON.stringify({ key: 'value' }) },
props,
PieceAuth.None(),
false,
{},
)
expect(jsonStringErrors).toEqual({})
const { errors: undefinedErrors } = await propsProcessor.applyProcessorsAndValidators(
{ object: { key: 'value' } },
props,
PieceAuth.None(),
false,
{},
)
expect(undefinedErrors).toEqual({})
})
})
describe('optional properties', () => {
it('should validate optional properties', async () => {
const props = {
text: Property.ShortText({
displayName: 'Text',
required: false,
}),
number: Property.Number({
displayName: 'Number',
required: false,
}),
}
const { errors } = await propsProcessor.applyProcessorsAndValidators(
{
text: null,
number: undefined,
},
props,
PieceAuth.None(),
false,
{},
)
expect(errors).toEqual({})
})
})
describe('type validation', () => {
it('should validate property types', async () => {
const props = {
string: Property.ShortText({
displayName: 'Text',
required: true,
}),
number: Property.Number({
displayName: 'Number',
required: true,
}),
boolean: Property.Checkbox({
displayName: 'Checkbox',
required: true,
}),
array: Property.Array({
displayName: 'Array',
required: true,
}),
object: Property.Object({
displayName: 'Object',
required: true,
}),
}
const { errors } = await propsProcessor.applyProcessorsAndValidators(
{
string: 42,
number: 'not a number',
boolean: 'not a boolean',
array: 'not an array',
object: 'not an object',
},
props,
PieceAuth.None(),
false,
{},
)
expect(errors).toEqual({
number: ['Expected number, received: not a number'],
boolean: ['Expected boolean, received: not a boolean'],
array: ['Expected array, received: not an array'],
object: ['Expected object, received: not an object'],
})
})
})
})