Milestone 0: import zappier billing, Verae middleware, and Zapier research

Compose-ready workspace: packages/zappier (rate card, portal, Stripe),
packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier
(CLI app), vendor/zapier-platform, and research/zapier vendor corpus.

Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View file

@ -0,0 +1,140 @@
'use strict';
require('should');
const schema = require('../../schema');
const operation = {
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number', required: true },
{ key: 'location', type: 'string' },
],
};
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation,
},
},
};
describe('bufferedCreateConstraints', () => {
it('should error on creates with both perform and buffer defined without performBuffer', () => {
operation.perform = '$func$2$f$';
operation.buffer = {
groupedBy: ['orderId'],
limit: 10,
};
delete operation.performBuffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation must contain property "performBuffer" because property "buffer" is present.',
);
results.errors[1].stack.should.eql(
'instance.creates.foo.operation must not contain property "perform" because it is mutually exclusive with property "buffer".',
);
});
it('should error on creates with both perform and performBuffer defined without buffer', () => {
operation.perform = '$func$2$f$';
operation.performBuffer = '$func$2$f$';
delete operation.buffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation must contain property "buffer" because property "performBuffer" is present.',
);
results.errors[1].stack.should.eql(
'instance.creates.foo.operation must not contain property "perform" because it is mutually exclusive with property "performBuffer".',
);
});
it('should error on creates with buffer defined without perform and performBuffer', () => {
operation.buffer = {
groupedBy: ['orderId'],
limit: 10,
};
delete operation.perform;
delete operation.performBuffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation must contain property "performBuffer" because property "buffer" is present.',
);
});
it('should error on creates with buffer defined without perform and performBuffer, and with groupedBy using optional inputFields', () => {
operation.buffer = {
groupedBy: ['location'],
limit: 10,
};
delete operation.perform;
delete operation.performBuffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation must contain property "performBuffer" because property "buffer" is present.',
);
results.errors[1].stack.should.eql(
'instance.creates.foo.operation.buffer.groupedBy[0] cannot use optional or non-existent inputField "location".',
);
});
it('should error on creates with performBuffer defined without buffer and perform', () => {
operation.performBuffer = '$func$2$f$';
delete operation.perform;
delete operation.buffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation must contain property "buffer" because property "performBuffer" is present.',
);
});
it('should error on creates with none of perform, buffer, and performBuffer defined', () => {
delete operation.buffer;
delete operation.perform;
delete operation.performBuffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.creates.foo.operation requires property "perform".',
);
});
it('should not error on creates with both buffer and performBuffer defined without perform', () => {
operation.performBuffer = '$func$2$f$';
operation.buffer = {
groupedBy: ['orderId'],
limit: 10,
};
delete operation.perform;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should not error on creates with perform defined without buffer and performBuffer', () => {
operation.perform = '$func$2$f$';
delete operation.buffer;
delete operation.performBuffer;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});

View file

@ -0,0 +1,82 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('deepNestedFields', () => {
it('should not error on fields nested one level deep', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{
key: 'product',
type: 'string',
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error on fields nested more than one level deep', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{ key: 'some do not have children' },
{
key: 'product',
children: [{ key: 'name', type: 'string' }],
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.creates.foo.inputFields[1] must not contain deeply nested child fields. One level max.',
);
});
});

View file

@ -0,0 +1,446 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('inputFieldGroupsConstraints', () => {
const baseDefinition = {
version: '1.0.0',
platformVersion: '17.2.0',
};
describe('Action Types Validation', () => {
it('should pass when group fields reference valid inputFieldGroups in triggers', () => {
const definition = {
...baseDefinition,
triggers: {
test_trigger: {
key: 'test_trigger',
noun: 'Test',
display: {
label: 'Test Trigger',
description: 'A test trigger',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{
key: 'contact',
label: 'Contact Information',
emphasize: false,
},
{ key: 'address', label: 'Address Information' },
],
inputFields: [
{ key: 'name', group: 'contact' },
{ key: 'email', group: 'contact' },
{ key: 'street', group: 'address' },
{ key: 'city', group: 'address' },
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error when group reference does not exist in inputFieldGroups', () => {
const definition = {
...baseDefinition,
searches: {
test_search: {
key: 'test_search',
noun: 'Test',
display: {
label: 'Test Search',
description: 'A test search',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'contact', label: 'Contact Information' },
],
inputFields: [
{ key: 'name', group: 'contact' },
{ key: 'email', group: 'nonexistent' }, // This group doesn't exist
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('invalidGroupReference');
results.errors[0].message.should.match(
/Group "nonexistent" is not defined in inputFieldGroups/,
);
});
it('should error when groups are used in children fields', () => {
const definition = {
...baseDefinition,
creates: {
test_create: {
key: 'test_create',
noun: 'Test',
display: {
label: 'Test Create',
description: 'A test create',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'items', label: 'Item Details' }],
inputFields: [
{
key: 'line_items',
children: [
{
key: 'product',
group: 'items', // Groups not allowed in children
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('groupInChildren');
results.errors[0].message.should.match(
/Group fields are not allowed in children fields/,
);
});
it('should error when inputFieldGroups has duplicate keys', () => {
const definition = {
...baseDefinition,
triggers: {
test_trigger: {
key: 'test_trigger',
noun: 'Test',
display: {
label: 'Test Trigger',
description: 'A test trigger',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'contact', label: 'Contact Information' },
{ key: 'address', label: 'Address Information' },
{ key: 'contact', label: 'Duplicate Contact' }, // Duplicate key
],
inputFields: [{ key: 'name', group: 'contact' }],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('duplicateGroupKey');
results.errors[0].message.should.match(
/Duplicate group key "contact" found in inputFieldGroups/,
);
});
});
describe('Resources Validation', () => {
it('should pass when resource methods have valid group references', () => {
const definition = {
...baseDefinition,
resources: {
contact: {
key: 'contact',
noun: 'Contact',
list: {
display: {
label: 'List Contacts',
description: 'List all contacts',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'filter', label: 'Filter Options' }],
inputFields: [
{ key: 'status', group: 'filter' },
{ key: 'type', group: 'filter' },
],
},
},
create: {
display: {
label: 'Create Contact',
description: 'Create a new contact',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'personal', label: 'Personal Info' },
{ key: 'business', label: 'Business Info' },
],
inputFields: [
{ key: 'firstName', group: 'personal' },
{ key: 'lastName', group: 'personal' },
{ key: 'company', group: 'business' },
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error when resource method has invalid group reference', () => {
const definition = {
...baseDefinition,
resources: {
contact: {
key: 'contact',
noun: 'Contact',
search: {
display: {
label: 'Search Contacts',
description: 'Search for contacts',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'criteria', label: 'Search Criteria' },
],
inputFields: [
{ key: 'name', group: 'criteria' },
{ key: 'email', group: 'invalid_group' }, // Invalid group reference
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('invalidGroupReference');
results.errors[0].message.should.match(
/Group "invalid_group" is not defined in inputFieldGroups/,
);
results.errors[0].property.should.equal(
'instance.resources.contact.search.operation.inputFields[1].group',
);
});
it('should error when resource method has duplicate group keys', () => {
const definition = {
...baseDefinition,
resources: {
project: {
key: 'project',
noun: 'Project',
get: {
display: {
label: 'Get Project',
description: 'Get a project by ID',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'options', label: 'Options' },
{ key: 'settings', label: 'Settings' },
{ key: 'options', label: 'Duplicate Options' }, // Duplicate key
],
inputFields: [{ key: 'includeArchived', group: 'options' }],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('duplicateGroupKey');
results.errors[0].message.should.match(
/Duplicate group key "options" found in inputFieldGroups/,
);
results.errors[0].property.should.equal(
'instance.resources.project.get.operation.inputFieldGroups[2].key',
);
});
it('should error when resource method has groups in children fields', () => {
const definition = {
...baseDefinition,
resources: {
order: {
key: 'order',
noun: 'Order',
create: {
display: {
label: 'Create Order',
description: 'Create a new order',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'items', label: 'Order Items' }],
inputFields: [
{
key: 'lineItems',
children: [
{
key: 'product',
group: 'items', // Groups not allowed in children
},
],
},
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('groupInChildren');
results.errors[0].message.should.match(
/Group fields are not allowed in children fields/,
);
results.errors[0].property.should.equal(
'instance.resources.order.create.operation.inputFields[0].children[0].group',
);
});
it('should validate multiple resource methods independently', () => {
const definition = {
...baseDefinition,
resources: {
user: {
key: 'user',
noun: 'User',
list: {
display: {
label: 'List Users',
description: 'List all users',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'filter', label: 'Filter Options' }],
inputFields: [{ key: 'active', group: 'filter' }],
},
},
search: {
display: {
label: 'Search Users',
description: 'Search for users',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [
{ key: 'criteria', label: 'Search Criteria' },
],
inputFields: [
{ key: 'email', group: 'nonexistent' }, // Invalid reference
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].name.should.equal('invalidGroupReference');
results.errors[0].property.should.equal(
'instance.resources.user.search.operation.inputFields[0].group',
);
});
});
describe('Mixed Validation', () => {
it('should validate both action types and resources together', () => {
const definition = {
...baseDefinition,
triggers: {
user_trigger: {
key: 'user_trigger',
noun: 'User',
display: {
label: 'User Trigger',
description: 'Trigger on user events',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'filter', label: 'Filter' }],
inputFields: [{ key: 'status', group: 'filter' }],
},
},
},
resources: {
contact: {
key: 'contact',
noun: 'Contact',
create: {
display: {
label: 'Create Contact',
description: 'Create a new contact',
},
operation: {
perform: '$func$2$f$',
inputFieldGroups: [{ key: 'personal', label: 'Personal Info' }],
inputFields: [{ key: 'name', group: 'personal' }],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should handle empty inputFieldGroups correctly', () => {
const definition = {
...baseDefinition,
triggers: {
test_trigger: {
key: 'test_trigger',
noun: 'Test',
display: {
label: 'Test Trigger',
description: 'A test trigger',
},
operation: {
perform: '$func$2$f$',
inputFields: [{ key: 'name' }, { key: 'email' }],
},
},
},
resources: {
item: {
key: 'item',
noun: 'Item',
list: {
display: {
label: 'List Items',
description: 'List all items',
},
operation: {
perform: '$func$2$f$',
inputFields: [{ key: 'category' }],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});
});

View file

@ -0,0 +1,73 @@
const should = require('should');
const schema = require('../../schema');
describe('labelWhenVisible', () => {
it('should not error when label is gone and action is hidden', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
hidden: true,
},
operation: {
perform: '$func$2$f$',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error when label is missing and action is visible ', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
hidden: false,
},
operation: {
perform: '$func$2$f$',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
const err = results.errors[0];
should(err.codeLinks[0].includes('BasicDisplaySchema')).be.true();
err.property.should.eql('instance.creates.foo.display');
});
it('should error when label is alone when create is visible ', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'nea',
},
operation: {
perform: '$func$2$f$',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
});
});

View file

@ -0,0 +1,43 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('matchingKeys', () => {
it("should error if the keys don't match", () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
triggers: {}, // this should be harmlessly skipped
creates: {
foo: {
key: 'bar', // this is different than above, which shouldn't validate
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{
key: 'product',
type: 'string',
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
});
});

View file

@ -0,0 +1,269 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('mutuallyExclusiveFields', () => {
it('should not error on fields not mutually exclusive', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{
key: 'product',
type: 'string',
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error on fields that have children and list', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{
key: 'product',
},
],
list: true,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
"instance.creates.foo.inputFields[1] must not contain children and list, as they're mutually exclusive.",
);
});
it('should not error on fields that have children and list when list is false', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
children: [
{
key: 'product',
},
],
list: false,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error on fields that have list and dict', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number' },
{
key: 'line_items',
dict: true,
list: true,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
"instance.creates.foo.inputFields[1] must not contain dict and list, as they're mutually exclusive.",
);
});
it('should error on fields that have dynamic and dict', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'orderId',
type: 'number',
dynamic: 'foo.id.number',
dict: true,
},
{
key: 'line_items',
list: true,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
"instance.creates.foo.inputFields[0] must not contain dynamic and dict, as they're mutually exclusive.",
);
});
it('should not error on fields that have dynamic and dict when dict is false', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'orderId',
type: 'number',
dynamic: 'foo.id.number',
dict: false,
},
{
key: 'line_items',
list: true,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error on fields that have dynamic and choices', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'orderId',
type: 'number',
dynamic: 'foo.id.number',
choices: {
uno: 1,
dos: 2,
},
},
{
key: 'line_items',
list: true,
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
"instance.creates.foo.inputFields[0] must not contain dynamic and choices, as they're mutually exclusive.",
);
});
});

View file

@ -0,0 +1,63 @@
'use strict';
require('should');
const schema = require('../../schema');
const operation = {
type: 'polling',
perform: '$func$2$f$',
throttle: {
window: 60,
limit: 1,
scope: ['account'],
},
sample: { id: 1 },
inputFields: [
{ key: 'orderId', type: 'number', required: true },
{ key: 'location', type: 'string' },
],
};
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
triggers: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'New Foo',
description: 'Triggers when a new foo is added.',
},
operation,
},
},
};
describe('pollingThrottle', () => {
it('should error on a polling trigger with the "retry" field set in its throttle configuration', () => {
operation.throttle.retry = false;
let results;
// for polling trigger with operation.type set
results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.triggers.foo.operation.throttle must not use the "retry" field for a polling trigger.',
);
// for polling trigger with operation.type unset
delete operation.type;
results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.triggers.foo.operation.throttle must not use the "retry" field for a polling trigger.',
);
});
it('should not error on a polling trigger without the "retry" field set in its throttle configuration', () => {
delete operation.throttle.retry;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});

View file

@ -0,0 +1,257 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('searchAndCreatesAlias', () => {
it('should not error if schema contains both searchOrCreates and searchAndCreates keys', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
add_product: {
key: 'add_product',
noun: 'Product',
display: {
label: 'Create Product',
description: 'Creates a new Product',
},
operation: {
perform: '$func$0$f$',
sample: { id: 1, title: 'Air Jordan' },
},
},
update_product: {
key: 'update_product',
noun: 'Product',
display: {
label: 'Update Product',
description: 'Updates an existing Product',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1, title: 'Nike Dunk High Retro' },
inputFields: [{ key: 'product_id', type: 'string' }],
},
},
},
searches: {
find_product: {
key: 'find_product',
noun: 'Product',
display: {
label: 'Search Product',
description: 'Searches for an existing Product',
},
operation: {
perform: '$func$1$f$',
inputFields: [{ key: 'product_title' }],
outputFields: [{ key: 'product_title', type: 'string' }],
sample: { id: 1, title: 'Nike Air' },
},
},
},
// Can have both searchOrCreates AND searchAndCreates defined
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
},
},
searchAndCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should not error if schema contains only searchOrCreates key', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
add_product: {
key: 'add_product',
noun: 'Product',
display: {
label: 'Create Product',
description: 'Creates a new Product',
},
operation: {
perform: '$func$0$f$',
sample: { id: 1, title: 'Air Jordan' },
},
},
update_product: {
key: 'update_product',
noun: 'Product',
display: {
label: 'Update Product',
description: 'Updates an existing Product',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1, title: 'Nike Dunk High Retro' },
inputFields: [{ key: 'product_id', type: 'string' }],
},
},
},
searches: {
find_product: {
key: 'find_product',
noun: 'Product',
display: {
label: 'Search Product',
description: 'Searches for an existing Product',
},
operation: {
perform: '$func$1$f$',
inputFields: [{ key: 'product_title' }],
outputFields: [{ key: 'product_title', type: 'string' }],
sample: { id: 1, title: 'Nike Air' },
},
},
},
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should not error if schema contains only searchAndCreates key', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
add_product: {
key: 'add_product',
noun: 'Product',
display: {
label: 'Create Product',
description: 'Creates a new Product',
},
operation: {
perform: '$func$0$f$',
sample: { id: 1, title: 'Air Jordan' },
},
},
update_product: {
key: 'update_product',
noun: 'Product',
display: {
label: 'Update Product',
description: 'Updates an existing Product',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1, title: 'Nike Dunk High Retro' },
inputFields: [{ key: 'product_id', type: 'string' }],
},
},
},
searches: {
find_product: {
key: 'find_product',
noun: 'Product',
display: {
label: 'Search Product',
description: 'Searches for an existing Product',
},
operation: {
perform: '$func$1$f$',
inputFields: [{ key: 'product_title' }],
outputFields: [{ key: 'product_title', type: 'string' }],
sample: { id: 1, title: 'Nike Air' },
},
},
},
searchAndCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});

View file

@ -0,0 +1,553 @@
'use strict';
require('should');
const _ = require('lodash');
const schema = require('../../schema');
describe('searchOrCreateKeys', () => {
const baseDefinition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
add_product: {
key: 'add_product',
noun: 'Product',
display: {
label: 'Create Product',
description: 'Creates a new Product',
},
operation: {
perform: '$func$0$f$',
sample: { id: 1, title: 'Air Jordan' },
},
},
update_product: {
key: 'update_product',
noun: 'Product',
display: {
label: 'Update Product',
description: 'Updates an existing Product',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1, title: 'Nike Dunk High Retro' },
inputFields: [{ key: 'product_id', type: 'string' }],
},
},
},
searches: {
find_product: {
key: 'find_product',
noun: 'Product',
display: {
label: 'Search Product',
description: 'Searches for an existing Product',
},
operation: {
perform: '$func$1$f$',
inputFields: [{ key: 'product_title', type: 'string' }],
sample: { id: 1, title: 'Nike Air' },
},
},
},
};
it('should not error on properly defined searchesOrCreates', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should not error on properly defined searchesOrCreates with update key', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error if searchesAndCreates contains an unknown key', () => {
const definition = {
...baseDefinition,
searchAndCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
test: 'wrong', // Unknown key
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchAndCreates.findOrCreateProduct is not allowed to have the additional property "test"',
);
});
it('should error if searchOrCreate.key does not match a search key', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'search_product', // Different key from any searches.key
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.key must match a "key" from a search (options: find_product)',
);
});
it('should error if searchAndCreate.key does not match a search key', () => {
const definition = {
...baseDefinition,
searchAndCreates: {
findOrCreateProduct: {
key: 'search_product', // Different key from any searches.key
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchAndCreates.findOrCreateProduct.key must match a "key" from a search (options: find_product)',
);
});
it('should error if searchesOrCreate.create key not matching an existing create key', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'create_product', // No such key in creates
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.create must match a "key" from a create (options: add_product,update_product)',
);
});
it('should error if searchesAndCreate.create key not matching an existing create key', () => {
const definition = {
...baseDefinition,
searchAndCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'create_product', // No such key in creates
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchAndCreates.findOrCreateProduct.create must match a "key" from a create (options: add_product,update_product)',
);
});
it('should error if searchesOrCreate.update is defined and does not match a create key', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'create_product', // No such key in creates
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.update must match a "key" from a create (options: add_product,update_product)',
);
});
it('should error if searchesOrCreate.updateInputFromSearchOutput is defined and but searchesOrCreate.update is not defined', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.updateInputFromSearchOutput requires searchOrCreates.findOrCreateProduct.update to be defined',
);
});
it('should error if searchesOrCreate.searchUniqueInputToOutputConstraint is defined and but searchesOrCreate.update is not defined', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.searchUniqueInputToOutputConstraint requires searchOrCreates.findOrCreateProduct.update to be defined',
);
});
it('should error if the searchOrCreate.updateInputFromSearchOutput object has a key not in creates.operations.inputFields', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
productId: 'id',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.updateInputFromSearchOutput object key must match a "key" from a creates.update_product.operation.inputFields (options: product_id)',
);
});
it('should error if the searchOrCreate.updateInputFromSearchOutput object has a value not in searches.operation.outputFields or searches.operation.sample, when these are defined', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'product_id',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.updateInputFromSearchOutput object value must match a "key" from searches.find_product.operation.(outputFields|sample) (options: id,title)',
);
});
it('should not error if the searchOrCreate.updateInputFromSearchOutput object has a value but searches.operation.outputFields and searches.operation.sample are not defined', () => {
const definition = _.cloneDeep(baseDefinition);
definition.searchOrCreates = {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: {
product_id: 'id',
},
},
};
delete definition.searches.find_product.operation.sample;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error if the searchOrCreate.searchUniqueInputToOutputConstraint object has a key but searches.operations.inputFields is not defined', () => {
const definition = _.cloneDeep(baseDefinition);
definition.searchOrCreates = {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
searchUniqueInputToOutputConstraint: {
product_title: 'title',
},
},
};
definition.searches.find_product.operation.inputFields = [];
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.searchUniqueInputToOutputConstraint object key must match a "key" from a searches.find_product.operation.inputFields (no "key" found in inputFields)',
);
});
it('should error if the searchOrCreate.searchUniqueInputToOutputConstraint object has a key not in searches.operations.inputFields', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
searchUniqueInputToOutputConstraint: {
// title_of_product does not exist in searches.operation.(outputFields.key|sample keys)
title_of_product: 'title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.searchUniqueInputToOutputConstraint object key must match a "key" from a searches.find_product.operation.inputFields (options: product_title)',
);
});
it('should error if the searchOrCreate.searchUniqueInputToOutputConstraint object has a value not in searches.operation.outputFields or searches.operation.sample, when these are defined', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
searchUniqueInputToOutputConstraint: {
product_title: 'product_title',
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.findOrCreateProduct.searchUniqueInputToOutputConstraint object value must match a "key" from searches.find_product.operation.(outputFields|sample) (options: id,title)',
);
});
it('should not error if the searchOrCreate.searchUniqueInputToOutputConstraint object has a value but searches.operation.outputFields and searches.operation.sample are not defined', () => {
const definition = _.cloneDeep(baseDefinition);
definition.searchOrCreates = {
findOrCreateProduct: {
key: 'find_product',
display: {
label: 'Search or Create',
description:
'Tries to fetch a product, creates one if it does not find at least one.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
searchUniqueInputToOutputConstraint: {
product_title: 'product_title',
},
},
};
delete definition.searches.find_product.operation.sample;
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should only give type errors if searchOrCreate.updateInputFromSearchOutput and searchOrCreate.searchUniqueInputToOutputConstraint are not objects', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
find_product: {
key: 'find_product',
display: {
label: 'Find, Create, or Update Product',
description: 'Finds, creates, or updates a product.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
updateInputFromSearchOutput: 'a_string',
searchUniqueInputToOutputConstraint: ['title'],
},
},
};
const results = schema.validateAppDefinition(definition);
// If updateInputFromSearchOutput or searchUniqueInputToOutputConstraint has
// the wrong type, we don't want to validate its content.
results.errors.should.have.length(2);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.find_product.updateInputFromSearchOutput is not of a type(s) object',
);
results.errors[1].stack.should.eql(
'instance.searchOrCreates.find_product.searchUniqueInputToOutputConstraint is not of a type(s) object',
);
});
it('should skip searchUniqueInputToOutputConstraint value check if it is nested', () => {
const definition = {
...baseDefinition,
searchOrCreates: {
find_product: {
key: 'find_product',
display: {
label: 'Find, Create, or Update Product',
description: 'Finds, creates, or updates a product.',
},
search: 'find_product',
create: 'add_product',
update: 'update_product',
searchUniqueInputToOutputConstraint: {
product_title: {
value_from_need: 'lookup_value',
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});

View file

@ -0,0 +1,151 @@
'use strict';
require('should');
const schema = require('../../schema');
describe('uniqueInputFieldKeys', () => {
it('should error when fields are repeated', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [{ key: 'name' }, { key: 'name' }, { key: 'name' }],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
// all errors reference the first instance of `name`
results.errors
.every(
(err) =>
err.message ===
'inputField keys must be unique for each action. The key "name" is already in use at creates.foo.operation.inputFields[0].key',
)
.should.be.true();
});
it('should error when fields nested fields are repeated', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'kids',
children: [
{ key: 'whatever' },
{ key: 'name', type: 'number' },
{ key: 'name', type: 'number' },
],
},
{ key: 'name', type: 'number' },
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
results.errors[0].message.should.eql(
`inputField keys must be unique for each action, even if they're children. The key "name" is already in use at creates.foo.operation.inputFields[0].children[1].key`,
);
results.errors[1].message.should.eql(
`inputField keys must be unique for each action. The key "name" is already in use at creates.foo.operation.inputFields[0].children[1].key`,
);
});
it('should error when cousin fields are repeated', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{ key: 'x', children: [{ key: 'name' }] },
{ key: 'y', children: [{ key: 'name' }] },
{ key: 'z', children: [{ key: 'name' }] },
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(2);
// all errors reference the first instance of `name`
results.errors
.every(
(err) =>
err.message ===
`inputField keys must be unique for each action, even if they're children. The key "name" is already in use at creates.foo.operation.inputFields[0].children[0].key`,
)
.should.be.true();
});
it('should handle non-inputField objects', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
// each of these is valid, but none declares a .key property
'$func$2$f$', // dynamic fields
'$func$2$f$',
{ source: 'return []' },
{ require: './some/js/file.js' },
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});

View file

@ -0,0 +1,614 @@
'use strict';
require('should');
const schema = require('../../schema');
// Helper to create a minimal app definition with a single json input field
const makeDefinition = (fieldSchema) => ({
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'payload',
type: 'json',
schema: fieldSchema,
},
],
},
},
},
});
describe('validateJsonFieldSchema', () => {
describe('standard action validation', () => {
it('should not error when type is json and schema is provided', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
properties: {
name: { type: 'string' },
},
}),
);
results.errors.should.have.length(0);
});
it('should not error when type is json without schema', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'payload',
type: 'json',
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error when schema is provided without type json', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'payload',
type: 'string',
schema: { type: 'object' },
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'must have `type` set to `json` when `schema` is provided',
);
});
});
describe('JSON Schema validation', () => {
it('should not error for a valid JSON Schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
tags: {
type: 'array',
items: { type: 'string' },
},
},
required: ['name'],
additionalProperties: false,
}),
);
results.errors.should.have.length(0);
});
it('should not error for an empty schema object', () => {
const results = schema.validateAppDefinition(makeDefinition({}));
results.errors.should.have.length(0);
});
it('should not error for a schema with type as an array of object and array', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: ['object', 'array'],
}),
);
results.errors.should.have.length(0);
});
it('should not error for a schema using allOf/anyOf/oneOf/not', () => {
const results = schema.validateAppDefinition(
makeDefinition({
allOf: [
{ type: 'object' },
{
properties: {
name: { type: 'string' },
},
},
],
not: { type: 'array' },
}),
);
results.errors.should.have.length(0);
});
it('should error for an invalid type in a nested property', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
properties: {
name: { type: 'not-a-type' },
},
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql('invalid type "not-a-type"');
});
it('should error when required is not an array', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
required: 'name',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql('required: must be an array');
});
it('should error when properties contains a non-object value', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
properties: {
name: 'should be an object',
},
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'must be a valid JSON Schema object',
);
});
it('should report multiple errors for multiple invalid fields', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
required: 'not-array',
properties: {
name: { type: 'not-a-type' },
},
}),
);
results.errors.should.have.length(2);
});
it('should error for invalid items schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'array',
items: 'not-a-schema',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'must be a valid JSON Schema object',
);
});
it('should error when root schema type is a primitive string', () => {
const results = schema.validateAppDefinition(
makeDefinition({ type: 'string' }),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'root `type` must be "object" or "array"',
);
});
it('should error when root schema type is an array of primitives', () => {
const results = schema.validateAppDefinition(
makeDefinition({ type: ['string', 'null'] }),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'root `type` must be "object" or "array"',
);
});
it('should error for invalid enum value', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
enum: 'not-an-array',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql('enum: must be an array');
});
it('should validate schemas in resources', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
resources: {
contact: {
key: 'contact',
noun: 'Contact',
create: {
display: {
label: 'Create Contact',
description: 'Create a new contact',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{
key: 'payload',
type: 'json',
schema: { type: 'badtype' },
},
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'root `type` must be "object" or "array"',
);
});
});
describe('children field validation', () => {
it('should not error for valid schema in a child field', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'parent',
children: [
{
key: 'nested_json',
type: 'json',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error for invalid schema in a child field', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'parent',
children: [
{
key: 'nested_json',
type: 'json',
schema: { type: 'badtype' },
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'root `type` must be "object" or "array"',
);
});
it('should error for schema without type json in a child field', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'parent',
children: [
{
key: 'nested_json',
type: 'string',
schema: { type: 'object' },
},
],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'must have `type` set to `json` when `schema` is provided',
);
});
});
describe('resource validation', () => {
it('should not error when type is json and schema is provided', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
resources: {
contact: {
key: 'contact',
noun: 'Contact',
create: {
display: {
label: 'Create Contact',
description: 'Create a new contact',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{
key: 'payload',
type: 'json',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
},
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should error when schema is provided without type json', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
resources: {
contact: {
key: 'contact',
noun: 'Contact',
create: {
display: {
label: 'Create Contact',
description: 'Create a new contact',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{
key: 'payload',
type: 'string',
schema: { type: 'object' },
},
],
},
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'must have `type` set to `json` when `schema` is provided',
);
});
});
describe('$schema draft version support', () => {
it('should not error for a schema with no $schema (defaults to Draft 7)', () => {
const results = schema.validateAppDefinition(
makeDefinition({
type: 'object',
properties: { name: { type: 'string' } },
}),
);
results.errors.should.have.length(0);
});
it('should not error for a schema with Draft 4 $schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: { name: { type: 'string' } },
}),
);
results.errors.should.have.length(0);
});
it('should not error for a schema with Draft 6 $schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'http://json-schema.org/draft-06/schema#',
type: 'object',
properties: { name: { type: 'string' } },
}),
);
results.errors.should.have.length(0);
});
it('should not error for a schema with Draft 7 $schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: { name: { type: 'string' } },
}),
);
results.errors.should.have.length(0);
});
it('should not error for a Draft 7 $schema without trailing #', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'http://json-schema.org/draft-07/schema',
type: 'object',
properties: { name: { type: 'string' } },
}),
);
results.errors.should.have.length(0);
});
it('should not error for a Draft 7 schema using exclusiveMinimum as a number', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'https://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
age: { type: 'integer', exclusiveMinimum: 5 },
},
}),
);
results.errors.should.have.length(0);
});
it('should error for a Draft 4 schema using exclusiveMinimum as a number', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'https://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
age: { type: 'integer', exclusiveMinimum: 5 },
},
}),
);
results.errors.should.have.length(2);
results.errors[0].stack.should.containEql('exclusiveMinimum');
results.errors[0].stack.should.containEql('is not of a type(s) boolean');
results.errors[1].stack.should.containEql('minimum not found');
});
it('should error for unsupported Draft 2019-09 $schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'https://json-schema.org/draft/2019-09/schema',
type: 'object',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'unsupported JSON Schema version',
);
});
it('should error for unsupported Draft 2020-12 $schema', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'unsupported JSON Schema version',
);
});
it('should error for an unrecognized $schema URI', () => {
const results = schema.validateAppDefinition(
makeDefinition({
$schema: 'https://example.com/my-custom-schema',
type: 'object',
}),
);
results.errors.should.have.length(1);
results.errors[0].stack.should.containEql(
'unsupported JSON Schema version',
);
results.errors[0].stack.should.containEql('Supported versions');
});
});
});

View file

@ -0,0 +1,412 @@
'use strict';
const should = require('should');
const schema = require('../schema');
const testUtils = require('./utils');
const appDefinition = require('../examples/definition.json');
const copy = (o) => JSON.parse(JSON.stringify(o));
const NUM_SCHEMAS = 65; // changes regularly as we expand
describe('app', () => {
describe('validation', () => {
it('should be a valid app', () => {
const results = schema.validateAppDefinition(appDefinition);
results.errors.should.eql([]);
});
it('should invalidate deep errors', () => {
const appCopy = copy(appDefinition);
delete appCopy.version;
delete appCopy.triggers.contact_by_tag.noun;
delete appCopy.triggers.contact_by_tag.display.label;
appCopy.triggers.contact_by_tag.operation.inputFields[0].type = 'loltype';
const results = schema.validateAppDefinition(appCopy);
results.errors.length.should.eql(4);
});
it('should invalidate a badly named trigger', () => {
const appCopy = copy(appDefinition);
appCopy.triggers['3contact_by_tag'] = appCopy.triggers.contact_by_tag;
delete appCopy.triggers.contact_by_tag;
const results = schema.validateAppDefinition(appCopy);
results.errors[0].stack.should.eql(
'instance.triggers is not allowed to have the additional property "3contact_by_tag"',
);
results.errors.length.should.eql(2); // additional property error + top-level key doesn't match trigger key
});
it('should invalidate a badly named create', () => {
const appCopy = copy(appDefinition);
appCopy.creates['3contact_by_tag'] = appCopy.creates.tag_create;
delete appCopy.creates.tag_create;
const results = schema.validateAppDefinition(appCopy);
results.errors[0].stack.should.eql(
'instance.creates is not allowed to have the additional property "3contact_by_tag"',
);
results.errors.length.should.eql(2); // additional property error + top-level key doesn't match create key
});
it('should invalidate a create with a bad key', () => {
const appCopy = copy(appDefinition);
appCopy.creates.tag_create.key = 'tag:create';
const results = schema.validateAppDefinition(appCopy);
results.errors[0].stack.should.eql(
'instance.creates.tag_create.key does not match pattern "^[a-zA-Z]+[a-zA-Z0-9_]*$"',
);
results.errors.length.should.eql(2); // invalid name and top-level key doesn't match create key
});
it('should run and pass functional constraints', function () {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
searches: {
fooSearch: {
key: 'fooSearch',
noun: 'Foo',
display: {
label: 'Find Foo',
description: 'Find a foo...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
},
creates: {
fooCreate: {
key: 'fooCreate',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
},
searchOrCreates: {
fooSearchOrCreate: {
key: 'fooSearch',
display: {
label: 'Find or Create a...',
description: 'Something Something',
},
search: 'fooSearch',
create: 'fooCreate',
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should run functional constraints with errors', function () {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
searches: {
fooSearch: {
key: 'fooSearch',
noun: 'Foo',
display: {
label: 'Find Foo',
description: 'Find a foo...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
},
creates: {
fooCreate: {
key: 'fooCreate',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
},
searchOrCreates: {
fooSearchOrCreate: {
key: 'fooSearchOrCreate',
display: {
label: 'Find or Create a...',
description: 'Something Something',
},
search: 'fooBad',
create: 'fooBad',
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(3);
results.errors[0].stack.should.eql(
'instance.searchOrCreates.fooSearchOrCreate.key must match a "key" from a search (options: fooSearch)',
);
results.errors[1].stack.should.eql(
'instance.searchOrCreates.fooSearchOrCreate.search must match a "key" from a search (options: fooSearch)',
);
results.errors[2].stack.should.eql(
'instance.searchOrCreates.fooSearchOrCreate.create must match a "key" from a create (options: fooCreate)',
);
});
it('should validate inputFormat', () => {
const appCopy = copy(appDefinition);
appCopy.authentication = {
type: 'custom',
test: {
url: 'https://example.com',
},
fields: [
{
key: 'subdomain',
type: 'string',
required: true,
inputFormat: 'https://{{input}}.example.com',
},
],
};
const results = schema.validateAppDefinition(appCopy);
// this line ensures we're getting a ValidatorResult class, not just an object that looks like one
should(results.valid).eql(true);
results.errors.should.eql([]);
});
it('should invalidate illegal inputFormat', () => {
const appCopy = copy(appDefinition);
appCopy.authentication = {
type: 'custom',
test: {
url: 'https://example.com',
},
fields: [
{
key: 'subdomain',
type: 'string',
required: true,
inputFormat: 'https://{{input}.example.com',
},
],
};
const results = schema.validateAppDefinition(appCopy);
results.errors.length.should.eql(1);
should(results.valid).eql(false);
const error = results.errors[0];
error.name.should.eql('pattern');
error.instance.should.eql('https://{{input}.example.com');
});
it('should validate legacy properties', () => {
const appCopy = copy(appDefinition);
appCopy.legacy = {
subscribeUrl: 'https://example.com',
triggers: {
contact: {
url: 'https://example.com',
},
},
};
const results = schema.validateAppDefinition(appCopy);
results.errors.should.eql([]);
});
it('should validate safe/unsafe auth fields', () => {
const appCopy = copy(appDefinition);
// Set up a "custom" auth that has two fields:
// - "username" which is safe
// - "password" which is not safe
appCopy.authentication = {
type: 'custom',
test: {
url: 'https://example.com',
},
fields: [
{
key: 'username',
type: 'string',
isNoSecret: true, // allowed
required: true,
},
{
key: 'password',
type: 'password',
isNoSecret: false, // password is not safe
required: true,
},
],
};
const results = schema.validateAppDefinition(appCopy);
// Expect zero errors
results.errors.should.have.length(0);
should(results.valid).eql(true);
});
it('should invalidate a "safe" password field', () => {
const appCopy = copy(appDefinition);
// Same "custom" auth but now marking "password" as isNoSecret=true
appCopy.authentication = {
type: 'custom',
test: {
url: 'https://example.com',
},
fields: [
{
key: 'password',
type: 'string',
isNoSecret: true, // invalid: password cannot be safe
required: true,
},
],
};
const results = schema.validateAppDefinition(appCopy);
// Expect at least one error because "password" can't have isNoSecret = true
results.errors.should.have.length(1);
should(results.valid).eql(false);
const [error] = results.errors;
error.name.should.equal('sensitive');
error.stack.should.eql(
'instance.authentication.fields[0] cannot set isNoSecret as true for the sensitive key "password".',
);
});
});
describe('export', () => {
it('should export the full schema', () => {
const exportedSchema = schema.exportSchema();
Object.keys(exportedSchema.schemas).length.should.eql(NUM_SCHEMAS);
});
});
});
describe('dynamic choices disambiguation', () => {
const FieldChoicesSchema = require('../lib/schemas/FieldChoicesSchema');
const FieldDynamicChoicesSchema = require('../lib/schemas/FieldDynamicChoicesSchema');
it('FieldChoicesSchema should reject objects with a perform key', () => {
const { errors } = FieldChoicesSchema.validate({ perform: '$func$0$f$' });
errors.should.not.have.length(0);
});
it('FieldDynamicChoicesSchema should accept objects with a perform key', () => {
const { errors } = FieldDynamicChoicesSchema.validate({
perform: '$func$0$f$',
});
errors.should.have.length(0);
});
it('FieldChoicesSchema should still accept static object choices', () => {
const { errors } = FieldChoicesSchema.validate({
high: 'High',
medium: 'Medium',
low: 'Low',
});
errors.should.have.length(0);
});
it('FieldChoicesSchema should still accept static array choices', () => {
const { errors } = FieldChoicesSchema.validate(['open', 'closed']);
errors.should.have.length(0);
});
it('should validate an app with dynamic choices on an input field', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
triggers: {
newThing: {
key: 'newThing',
noun: 'Thing',
display: {
label: 'New Thing',
description: 'Triggers on new things.',
},
operation: {
type: 'polling',
perform: '$func$0$f$',
sample: { id: 1 },
inputFields: [
{
key: 'org',
label: 'Organization',
helpText: 'Pick one.',
type: 'string',
required: true,
choices: { perform: '$func$1$f$' },
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
it('should validate an app with both static and dynamic choices', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
triggers: {
newThing: {
key: 'newThing',
noun: 'Thing',
display: {
label: 'New Thing',
description: 'Triggers on new things.',
},
operation: {
type: 'polling',
perform: '$func$0$f$',
sample: { id: 1 },
inputFields: [
{
key: 'org',
choices: { perform: '$func$1$f$' },
},
{
key: 'priority',
choices: { high: 'High', low: 'Low' },
},
{
key: 'status',
choices: ['open', 'closed'],
},
],
},
},
},
};
const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});
});
describe('auto test', () => {
const _exportedSchema = schema.exportSchema();
Object.keys(_exportedSchema.schemas).map((id) =>
testUtils.testInlineSchemaExamples(id),
);
});

View file

@ -0,0 +1,154 @@
'use strict';
const should = require('should');
const AuthenticationSchema = require('../lib/schemas/AuthenticationSchema');
const CreateSchema = require('../lib/schemas/CreateSchema');
const TriggerSchema = require('../lib/schemas/TriggerSchema');
describe('readability', () => {
it('should have decent messages for anyOf mismatches', () => {
const results = AuthenticationSchema.validate({
type: 'oauth2',
test: 'whateverfake!',
});
results.errors.should.have.length(1);
results.errors[0].stack.should.eql('instance is not of a type(s) object');
should(results.errors[0].property.endsWith('instance')).be.false();
});
it('should have decent messages for minimum length not met', () => {
const results = TriggerSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: '',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
});
results.errors.should.have.length(1);
should(results.errors[0].property.endsWith('instance')).be.false();
results.errors[0].stack.should.eql(
'instance.display.label does not meet minimum length of 2',
);
});
it('should have decent messages for value type mismatch', () => {
const results = CreateSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [123],
},
});
results.errors.should.have.length(1);
should(results.errors[0].property.endsWith('instance')).be.false();
results.errors[0].stack.should.eql('instance is not of a type(s) object');
});
it('should handle falsy values for objects', () => {
const results = CreateSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [0],
},
});
results.errors.should.have.length(1);
should(results.errors[0].property.endsWith('instance')).be.false();
results.errors[0].stack.should.eql('instance is not of a type(s) object');
});
it('should surface deep issues', () => {
const results = CreateSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [{ key: 'field', type: 'string', default: '' }],
},
});
results.errors.should.have.length(1);
should(results.errors[0].property.endsWith('instance')).be.false();
results.errors[0].property.should.eql(
'instance.operation.inputFields[0].default',
);
results.errors[0].message.should.eql('does not meet minimum length of 1');
});
it('should correctly surface subschema types', () => {
const results = CreateSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: {
url: 'https://example.com',
body: 123,
},
sample: { id: 1 },
},
});
results.errors.should.have.length(1);
results.errors[0].property.should.eql('instance.operation.perform.body');
should(
results.errors[0].message.includes('null,string,object,array'),
).be.true();
should(results.errors[0].property.endsWith('instance')).be.false();
results.errors[0].docLinks.length.should.eql(0);
});
it('should be helpful for fieldChoices', () => {
const results = CreateSchema.validate({
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
inputFields: [
{
key: 'adsf',
// schema says these should be strings
choices: [1, 2, 3],
},
],
},
});
results.errors.should.have.length(1);
results.errors[0].property.should.eql(
'instance.operation.inputFields[0].choices',
);
should(
results.errors[0].docLinks[0].includes('schema.md#fieldchoicesschema'),
).be.true();
should(results.errors[0].property.endsWith('instance')).be.false();
});
});

View file

@ -0,0 +1,46 @@
'use strict';
require('should');
const { SKIP_KEY } = require('../lib/constants');
const testInlineSchemaExamples = (name) => {
const Schema = require('../lib/schemas/' + name);
const goods = Schema.schema.examples || [];
const bads = Schema.schema.antiExamples || [];
describe(name, () => {
it('valid example schemas should pass validation', function () {
if (!goods.length) {
this.skip();
} else {
goods
.filter((t) => !t[SKIP_KEY])
.forEach((good) => {
const { errors } = Schema.validate(good);
errors.should.have.length(0);
});
}
});
it('invalid example schemas should fail validation', function () {
if (!bads.length) {
this.skip();
} else {
bads
.filter((t) => !t[SKIP_KEY])
.map((t) => t.example)
.forEach((bad) => {
const { errors } = Schema.validate(bad);
if (errors.length === 0) {
console.log(bad);
}
errors.should.not.have.length(0);
});
}
});
});
};
module.exports = {
testInlineSchemaExamples,
};