Environment
@@ -3763,7 +4015,7 @@
Adding Throttle Configuration
},
operation: {
perform:
() => {},
-
inputFields: [{
key:
'name',
required:
true,
type:
'string'}],
+
inputFields: [{
key:
'name',
required:
true,
type:
'string' }],
throttle: {
window:
600,
diff --git a/packages/cli/snippets/buffered-create.js b/packages/cli/snippets/buffered-create.js
new file mode 100644
index 000000000..750f4b5fc
--- /dev/null
+++ b/packages/cli/snippets/buffered-create.js
@@ -0,0 +1,101 @@
+const performBuffer = async (z, bufferedBundle) => {
+ // Grab the line items, preserving the order
+ const rows = bufferedBundle.buffer.map(({ inputData }) => {
+ return { title: inputData.title, year: inputData.year };
+ });
+
+ // Make the bulk-create API request
+ const response = await z.request({
+ method: 'POST',
+ url: 'https://api.example.com/add_rows',
+ body: {
+ spreadsheet: bufferedBundle.groupedBy.spreadsheet,
+ worksheet: bufferedBundle.groupedBy.worksheet,
+ rows,
+ },
+ });
+
+ // Create a matching result using the idempotency ID for each buffered invocation run.
+ // The returned IDs will tell Zapier backend which items were successfully written.
+ const result = {};
+ bufferedBundle.buffer.forEach(({ inputData, meta }, index) => {
+ let error = '';
+ let outputData = {};
+
+ // assuming request order matches response and
+ // response.data = {
+ // "rows": [
+ // {"id": "12910"},
+ // {"id": "92830"},
+ // {"error": "Not Created"},
+ // ...
+ // ]
+ // }
+ if (response.data.rows.length > index) {
+ // assuming an error is returned with an "error" key in the response data
+ if (response.data.rows[index].error) {
+ error = response.data.rows[index].error;
+ } else {
+ outputData = response.data.rows[index];
+ }
+ }
+
+ // the performBuffer method must return a data just like this
+ // {
+ // "idempotency ID 1": {
+ // "outputData": {"id": "12910"},
+ // "error": ""
+ // },
+ // "idempotency ID 2": {
+ // "outputData": {"id": "92830"},
+ // "error": ""
+ // },
+ // "idempotency ID 3": {
+ // "outputData": {},
+ // "error": "Not Created"
+ // },
+ // ...
+ // }
+ result[meta.id] = { outputData, error };
+ });
+
+ return result;
+};
+
+module.exports = {
+ key: 'add_rows',
+ noun: 'Rows',
+ display: {
+ label: 'Add Rows',
+ description: 'Add rows to a worksheet.',
+ },
+ operation: {
+ buffer: {
+ groupedBy: ['spreadsheet', 'worksheet'],
+ limit: 3,
+ },
+ performBuffer,
+ inputFields: [
+ {
+ key: 'spreadsheet',
+ type: 'string',
+ required: true,
+ },
+ {
+ key: 'worksheet',
+ type: 'string',
+ required: true,
+ },
+ {
+ key: 'title',
+ type: 'string',
+ },
+ {
+ key: 'year',
+ type: 'string',
+ },
+ ],
+ outputFields: [{ key: 'id', type: 'string' }],
+ sample: { id: '12345' },
+ },
+};
diff --git a/packages/cli/snippets/throttle-configuration.js b/packages/cli/snippets/throttle-configuration.js
index e4b5a89e8..00e2a2b69 100644
--- a/packages/cli/snippets/throttle-configuration.js
+++ b/packages/cli/snippets/throttle-configuration.js
@@ -18,7 +18,7 @@ const App = {
},
operation: {
perform: () => {},
- inputFields: [{key: 'name', required: true, type: 'string'}],
+ inputFields: [{ key: 'name', required: true, type: 'string' }],
// overwrites the default, for this action
throttle: {
window: 600,
diff --git a/packages/core/integration-test/integration-test.js b/packages/core/integration-test/integration-test.js
index 7627de76d..a534314ab 100644
--- a/packages/core/integration-test/integration-test.js
+++ b/packages/core/integration-test/integration-test.js
@@ -684,7 +684,7 @@ const doTest = (runner) => {
it('should log requests', () => {
const event = {
command: 'execute',
- method: 'resources.requestfunc.list.operation.perform',
+ method: 'resources.requestsugar.list.operation.perform',
logExtra: {
app_cli_id: 666,
},
diff --git a/packages/core/src/app-middlewares/after/checks.js b/packages/core/src/app-middlewares/after/checks.js
index eb1236500..33b3a2fd3 100644
--- a/packages/core/src/app-middlewares/after/checks.js
+++ b/packages/core/src/app-middlewares/after/checks.js
@@ -32,7 +32,7 @@ const checkOutput = (output) => {
})
.map((check) => {
return check
- .run(event.method, output.results, compiledApp)
+ .run(event.method, output.results, compiledApp, event.bundle)
.map((err) => ({ name: check.name, error: err }));
});
const checkResults = _.flatten(rawResults);
diff --git a/packages/core/src/checks/index.js b/packages/core/src/checks/index.js
index 981090bac..5a1087dc5 100644
--- a/packages/core/src/checks/index.js
+++ b/packages/core/src/checks/index.js
@@ -9,4 +9,5 @@ module.exports = {
triggerHasId: require('./trigger-has-id'),
firehoseSubscriptionIsArray: require('./firehose_is_array'),
firehoseSubscriptionKeyIsString: require('./firehose_is_string'),
+ performBufferReturnType: require('./perform-buffer-return-type'),
};
diff --git a/packages/core/src/checks/is-create.js b/packages/core/src/checks/is-create.js
index c14f7596e..69c889a0f 100644
--- a/packages/core/src/checks/is-create.js
+++ b/packages/core/src/checks/is-create.js
@@ -1,6 +1,10 @@
module.exports = (method) => {
+ // `method` will never start with "resources." in production.
+ // Seems only for testing.
return (
- (method.startsWith('creates.') && method.endsWith('.operation.perform')) ||
+ (method.startsWith('creates.') &&
+ (method.endsWith('.operation.perform') ||
+ method.endsWith('.operation.performBuffer'))) ||
(method.startsWith('resources.') &&
method.endsWith('.create.operation.perform'))
);
diff --git a/packages/core/src/checks/is-trigger.js b/packages/core/src/checks/is-trigger.js
index 955a99e68..8f78dbcff 100644
--- a/packages/core/src/checks/is-trigger.js
+++ b/packages/core/src/checks/is-trigger.js
@@ -1,6 +1,7 @@
module.exports = (method) => {
return (
- // `method` will never start with "resources.". Seems like legacy code.
+ // `method` will never start with "resources." in production.
+ // Seems only for testing.
(method.startsWith('triggers.') && method.endsWith('.operation.perform')) ||
(method.startsWith('resources.') &&
method.endsWith('.list.operation.perform'))
diff --git a/packages/core/src/checks/perform-buffer-return-type.js b/packages/core/src/checks/perform-buffer-return-type.js
new file mode 100644
index 000000000..d5334f858
--- /dev/null
+++ b/packages/core/src/checks/perform-buffer-return-type.js
@@ -0,0 +1,65 @@
+const _ = require('lodash');
+
+const performBufferEchoesIds = {
+ name: 'performBufferReturnType',
+
+ shouldRun: (method, bundle) => {
+ return (
+ Array.isArray(bundle.buffer) &&
+ method.endsWith('.operation.performBuffer') &&
+ method.startsWith('creates.')
+ );
+ },
+
+ run: (method, results, compiledApp, bundle) => {
+ if (!_.isPlainObject(results)) {
+ // create-is-object should have caught this
+ return [];
+ }
+
+ const inputIds = bundle.buffer
+ .map((b) => {
+ return b && b.meta ? b.meta.id : null;
+ })
+ .filter((id) => id);
+
+ const outputIds = Object.keys(results);
+ const missingIds = inputIds.filter((id) => !outputIds.includes(id));
+
+ if (missingIds.length > 0) {
+ const LIMIT = 3;
+ let missingIdsStr = missingIds.slice(0, LIMIT).join(', ');
+ const remainingCount = missingIds.length - LIMIT;
+ if (remainingCount > 0) {
+ // Don't want to flood the user with too many IDs
+ missingIdsStr += `, and ${remainingCount} more`;
+ }
+ return [`Result object is missing these IDs as keys: ${missingIdsStr}`];
+ }
+
+ const errors = [];
+ for (const id of inputIds) {
+ const item = results[id];
+
+ if (!_.isPlainObject(item)) {
+ errors.push(`Result object member with ID '${id}' must be an object`);
+ } else if (
+ !_.isPlainObject(item.outputData) &&
+ typeof item.error !== 'string'
+ ) {
+ errors.push(
+ `Result object member with ID '${id}' must have 'outputData' object or 'error' string`
+ );
+ }
+
+ if (errors.length >= 4) {
+ // No need to flood the user with too many errors
+ break;
+ }
+ }
+
+ return errors;
+ },
+};
+
+module.exports = performBufferEchoesIds;
diff --git a/packages/core/test/checks.js b/packages/core/test/checks.js
index ca0ee5e1a..9ade66d2c 100644
--- a/packages/core/test/checks.js
+++ b/packages/core/test/checks.js
@@ -247,6 +247,78 @@ describe('checks', () => {
isFirehoseWebhook(firehoseMethod).should.be.true();
isFirehoseWebhook('triggers.blah.operation.perform').should.be.false(); // the firehose webhook check is at app level, not trigger
});
+
+ it('should check performBuffer missing IDs', () => {
+ const results = { one: {}, two: {} };
+ const bundle = {
+ buffer: [
+ { meta: { id: 'one' } },
+ { meta: { id: 'two' } },
+ { meta: { id: 'three' } },
+ { meta: { id: 'four' } },
+ { meta: { id: 'five' } },
+ { meta: { id: 'six' } },
+ { meta: { id: 'seven' } },
+ { meta: { id: 'eight' } },
+ { meta: { id: 'nine' } },
+ ],
+ };
+ const errors = checks.performBufferReturnType.run(
+ 'creates.blah.operation.performBuffer',
+ results,
+ {},
+ bundle
+ );
+ errors.length.should.eql(1);
+ errors[0].should.match(
+ /missing these IDs as keys: three, four, five, and 4 more/
+ );
+ });
+
+ it('should check performBuffer object shape', () => {
+ const results = {
+ one: 'not an object',
+ two: { error: 123 },
+ three: { outputData: {} },
+ four: { error: 'test' },
+ five: {
+ outputData: 'this one should pass because it is not in the input',
+ },
+ };
+ const bundle = {
+ buffer: [
+ { meta: { id: 'one' } },
+ { meta: { id: 'two' } },
+ { meta: { id: 'three' } },
+ { meta: { id: 'four' } },
+ ],
+ };
+ const errors = checks.performBufferReturnType.run(
+ 'creates.blah.operation.performBuffer',
+ results,
+ {},
+ bundle
+ );
+ errors.length.should.eql(2);
+ errors[0].should.match(/member with ID 'one' must be an object/);
+ errors[1].should.match(
+ /member with ID 'two' must have 'outputData' object or 'error' string/
+ );
+ });
+
+ it('should pass performBuffer check', () => {
+ const results = { one: { outputData: {} }, two: { outputData: {} } };
+ const bundle = {
+ buffer: [{ meta: { id: 'one' } }, { meta: { id: 'two' } }],
+ };
+ const errors = checks.performBufferReturnType.run(
+ 'creates.blah.operation.performBuffer',
+ results,
+ {},
+ bundle
+ );
+ errors.should.be.empty();
+ });
});
describe('checkOutput', () => {
diff --git a/packages/core/test/create-app.js b/packages/core/test/create-app.js
index 80e772a7d..acedd6178 100644
--- a/packages/core/test/create-app.js
+++ b/packages/core/test/create-app.js
@@ -17,9 +17,10 @@ describe('create-app', () => {
const app = createApp(appDefinition);
- const createTestInput = (method) => {
+ const createTestInput = (method, bundle) => {
const event = {
- bundle: {},
+ command: 'execute',
+ bundle: { ...bundle },
method,
callback_url: 'calback_url',
};
@@ -108,13 +109,14 @@ describe('create-app', () => {
const input = createTestInput('resources.contact.list.operation.perform');
const output = await app(input);
- should.exist(output.results.headers);
+ const result = output.results[0];
+ should.exist(result.headers);
// verify that custom http before middleware was applied
- output.results.headers['X-Hashy'].should.deepEqual([
+ result.headers['X-Hashy'].should.deepEqual([
'1a3ba5251cb33ee7ade01af6a7b960b8',
]);
- output.results.headers['X-Author'].should.deepEqual(['One Cool Dev']);
+ result.headers['X-Author'].should.deepEqual(['One Cool Dev']);
});
it('should fail on a live request call', async () => {
@@ -155,28 +157,18 @@ describe('create-app', () => {
throw new Error('expected an error');
});
- it('should make call via z.request', async () => {
- const input = createTestInput('triggers.requestfuncList.operation.perform');
-
- const output = await app(input);
-
- output.results[0].headers['X-Hashy'].should.deepEqual([
- '1a3ba5251cb33ee7ade01af6a7b960b8',
- ]);
- output.results[0].headers['X-Author'].should.deepEqual(['One Cool Dev']);
- });
-
it('should make call via z.request with sugar url param', async () => {
const input = createTestInput(
'triggers.requestsugarList.operation.perform'
);
const output = await app(input);
+ const result = output.results[0];
- output.results.headers['X-Hashy'].should.deepEqual([
+ result.headers['X-Hashy'].should.deepEqual([
'1a3ba5251cb33ee7ade01af6a7b960b8',
]);
- output.results.headers['X-Author'].should.deepEqual(['One Cool Dev']);
+ result.headers['X-Author'].should.deepEqual(['One Cool Dev']);
});
it('should fail on a sync function', (done) => {
@@ -256,11 +248,13 @@ describe('create-app', () => {
const input = createTestInput('triggers.contactList.operation.perform');
const output = await app(input);
- output.results.url.should.eql(`${HTTPBIN_URL}/get`);
- output.results.headers['X-Hashy'].should.deepEqual([
+ const result = output.results[0];
+
+ result.url.should.eql(`${HTTPBIN_URL}/get`);
+ result.headers['X-Hashy'].should.deepEqual([
'1a3ba5251cb33ee7ade01af6a7b960b8',
]);
- output.results.headers['X-Author'].should.deepEqual(['One Cool Dev']);
+ result.headers['X-Author'].should.deepEqual(['One Cool Dev']);
});
it('should return a rendered URL for OAuth2 authorizeURL', (done) => {
@@ -319,6 +313,46 @@ describe('create-app', () => {
.catch(done);
});
+ describe('output checks', () => {
+ it('should check performBuffer output', async () => {
+ const definition = dataTools.jsonCopy(appDefinition);
+ definition.resources.row = {
+ key: 'row',
+ noun: 'Row',
+ create: {
+ display: {
+ label: 'Insert Row',
+ },
+ operation: {
+ performBuffer: (z, bundle) => {
+ const firstId = bundle.buffer[0].meta.id;
+ return { [firstId]: {} };
+ },
+ },
+ },
+ };
+ const app = createApp(definition);
+ const bundle = {
+ buffer: [
+ { meta: { id: 'ffee-0000' } },
+ { meta: { id: 'ffee-0001' } },
+ { meta: { id: 'ffee-0002' } },
+ { meta: { id: 'ffee-0003' } },
+ { meta: { id: 'ffee-0004' } },
+ { meta: { id: 'ffee-0005' } },
+ ],
+ };
+ const input = createTestInput(
+ 'creates.rowCreate.operation.performBuffer',
+ bundle
+ );
+ const err = await app(input).should.be.rejected();
+ err.name.should.eql('CheckError');
+ err.message.should.match(/missing these IDs as keys/);
+ err.message.should.match(/ffee-0001, ffee-0002, ffee-0003, and 2 more/);
+ });
+ });
+
describe('HTTP after middleware for auth refresh', () => {
// general purpose response tester
const testResponse = async (appDef, useShorthand, errorVerifier) => {
@@ -499,29 +533,37 @@ describe('create-app', () => {
});
describe('hydration', () => {
- it('should hydrate method', (done) => {
+ it('should hydrate method', async () => {
const input = createTestInput(
'resources.honkerdonker.list.operation.perform'
);
-
- app(input)
- .then((output) => {
- output.results.should.eql([
+ const output = await app(input);
+ output.results.should.eql([
+ {
+ id: 1,
+ $HOIST$:
'hydrate|||{"type":"method","method":"resources.honkerdonker.get.operation.perform","bundle":{"honkerId":1}}|||hydrate',
+ },
+ {
+ id: 2,
+ $HOIST$:
'hydrate|||{"type":"method","method":"resources.honkerdonker.get.operation.perform","bundle":{"honkerId":2}}|||hydrate',
+ },
+ {
+ id: 3,
+ $HOIST$:
'hydrate|||{"type":"method","method":"resources.honkerdonker.get.operation.perform","bundle":{"honkerId":3}}|||hydrate',
- ]);
- done();
- })
- .catch(done);
+ },
+ ]);
});
});
+
describe('calling a callback method', () => {
let results;
before(() =>
app(
createTestInput(
- 'resources.executeCallbackRequest.list.operation.perform'
+ 'resources.executeCallbackRequest.create.operation.perform'
)
).then((output) => {
results = output;
@@ -530,8 +572,12 @@ describe('create-app', () => {
it('returns a CALLBACK envelope', () =>
results.status.should.eql('CALLBACK'));
+
it('returns the methods values', () =>
- results.results.should.eql({ callbackUrl: 'calback_url' }));
+ results.results.should.eql({
+ id: 'dontcare',
+ callbackUrl: 'calback_url',
+ }));
});
describe('using require', () => {
@@ -579,13 +625,13 @@ describe('create-app', () => {
it('should import and use the crypto module from node', async () => {
const definition = createDefinition(`
const crypto = z.require('crypto');
- return crypto.createHash('md5').update('abc').digest('hex');
+ return [{id: crypto.createHash('md5').update('abc').digest('hex')}];
`);
const input = createTestInput('triggers.testRequire.operation.perform');
const appPass = createApp(definition);
const { results } = await appPass(input);
- results.should.eql('900150983cd24fb0d6963f7d28e17f72');
+ results[0].id.should.eql('900150983cd24fb0d6963f7d28e17f72');
});
it('should handle require errors', async () => {
@@ -610,7 +656,8 @@ describe('create-app', () => {
const appPass = createApp(appDefinition);
const { results } = await appPass(input);
- results.should.eql('www.base-url.com');
+ results.length.should.eql(1);
+ results[0].url.should.eql('www.base-url.com');
});
});
diff --git a/packages/core/test/tools/resolve-method-path.js b/packages/core/test/tools/resolve-method-path.js
index 4bd5a4e47..8ee200291 100644
--- a/packages/core/test/tools/resolve-method-path.js
+++ b/packages/core/test/tools/resolve-method-path.js
@@ -25,8 +25,8 @@ describe('resolve-method-path', () => {
it('should resolve a request method object with a url', () => {
resolveMethodPath(
app,
- app.resources.contact.list.operation.perform
- ).should.eql('resources.contact.list.operation.perform');
+ app.resources.contacterror.list.operation.perform
+ ).should.eql('resources.contacterror.list.operation.perform');
});
it('should resolve a method in a module', () => {
diff --git a/packages/core/test/userapp/index.js b/packages/core/test/userapp/index.js
index 063e5b151..b5fb21dbe 100644
--- a/packages/core/test/userapp/index.js
+++ b/packages/core/test/userapp/index.js
@@ -38,7 +38,7 @@ const ListRequire = {
perform: (z, bundle) => {
// in prod, process.cwd will return the app root directory
const { BASE_URL } = z.require('./test/userapp/constants.js');
- return BASE_URL;
+ return [{ id: 1, url: BASE_URL }];
},
},
},
@@ -53,8 +53,11 @@ const Contact = {
description: 'Trigger on new contacts.',
},
operation: {
- perform: {
- url: '{{process.env.BASE_URL}}/get',
+ perform: async (z, bundle) => {
+ const response = await z.request({
+ url: `${process.env.BASE_URL}/get`,
+ });
+ return [{ id: 'dontcare', ...response.data }];
},
inputFields: [
{
@@ -139,28 +142,6 @@ const LoggingFunc = {
},
};
-const RequestFunc = {
- key: 'requestfunc',
- noun: 'requestfunc',
- list: {
- display: {
- label: 'New Request Func',
- description: 'Makes an http request via z.request.',
- },
- operation: {
- perform: (z /* , bundle */) => {
- return z
- .request({ url: '{{process.env.BASE_URL}}/get' })
- .then((resp) => {
- const result = resp.data;
- result.id = 123;
- return [result];
- });
- },
- },
- },
-};
-
const RequestSugar = {
key: 'requestsugar',
noun: 'requestsugar',
@@ -172,7 +153,7 @@ const RequestSugar = {
operation: {
perform: (z /* , bundle */) => {
return z.request(`${HTTPBIN_URL}/get`).then((resp) => {
- return resp.data;
+ return [{ id: 'dontcare', ...resp.data }];
});
},
},
@@ -419,7 +400,11 @@ const HonkerDonker = {
description: 'This will be dehydrated by list',
},
operation: {
- perform: (z, bundle) => `honker donker number ${bundle.honkerId}`,
+ perform: (z, bundle) => {
+ return {
+ message: `honker donker number ${bundle.honkerId}`,
+ };
+ },
},
},
list: {
@@ -431,9 +416,12 @@ const HonkerDonker = {
perform: (z, bundle) => {
const honkerIds = [1, 2, 3];
return honkerIds.map((id) => {
- return z.dehydrate(HonkerDonker.get.operation.perform, {
- honkerId: id,
- });
+ return {
+ id,
+ $HOIST$: z.dehydrate(HonkerDonker.get.operation.perform, {
+ honkerId: id,
+ }),
+ };
});
},
},
@@ -525,7 +513,7 @@ const EnvironmentVariable = {
const ExecuteCallbackRequest = {
key: 'executeCallbackRequest',
noun: 'Callback',
- list: {
+ create: {
display: {
label: 'Callback Usage in a perform',
description: 'Used for one-offs in the tests.',
@@ -534,7 +522,7 @@ const ExecuteCallbackRequest = {
perform: (z) => {
// we need to access the callback url
const callbackUrl = z.generateCallbackUrl();
- return { callbackUrl };
+ return { id: 'dontcare', callbackUrl };
},
performResume: (z, bundle) => {
return Object.assign({}, bundle.outputData, bundle.cleanedRequest);
@@ -629,7 +617,6 @@ const App = {
[ContactError.key]: ContactError,
[ContactSource.key]: ContactSource,
[LoggingFunc.key]: LoggingFunc,
- [RequestFunc.key]: RequestFunc,
[RequestSugar.key]: RequestSugar,
[WorkingFunc.key]: WorkingFunc,
[WorkingFuncAsync.key]: WorkingFuncAsync,
diff --git a/packages/core/types/zapier.custom.d.ts b/packages/core/types/zapier.custom.d.ts
index cf8a9e565..e0390e7b4 100644
--- a/packages/core/types/zapier.custom.d.ts
+++ b/packages/core/types/zapier.custom.d.ts
@@ -135,13 +135,12 @@ type DehydrateFunc =
(
export interface ZObject {
request: {
// most specific overloads go first
- (
- url: string,
- options: HttpRequestOptions & { raw: true }
- ): Promise;
- (
- options: HttpRequestOptions & { raw: true; url: string }
- ): Promise;
+ (url: string, options: HttpRequestOptions & { raw: true }): Promise<
+ RawHttpResponse
+ >;
+ (options: HttpRequestOptions & { raw: true; url: string }): Promise<
+ RawHttpResponse
+ >;
(url: string, options?: HttpRequestOptions): Promise;
(options: HttpRequestOptions & { url: string }): Promise;
@@ -231,3 +230,40 @@ export type AfterResponseMiddleware = (
z: ZObject,
bundle?: Bundle
) => HttpResponse | Promise;
+
+export interface BufferedItem {
+ inputData: InputData;
+ meta: {
+ id: string;
+ [x: string]: any;
+ };
+}
+
+export interface BufferedBundle {
+ authData: { [x: string]: string };
+ buffer: BufferedItem[];
+ groupedBy: { [x: string]: string };
+}
+
+interface PerformBufferSuccessItem {
+ outputData: { [x: string]: any };
+ error?: string;
+}
+
+interface PerformBufferErrorItem {
+ outputData?: { [x: string]: any };
+ error: string;
+}
+
+export type PerformBufferResultItem =
+ | PerformBufferSuccessItem
+ | PerformBufferErrorItem;
+
+export interface PerformBufferResult {
+ [id: string]: PerformBufferResultItem;
+}
+
+export const performBuffer: (
+ z: ZObject,
+ bundle: BufferedBundle
+) => Promise;
diff --git a/packages/core/types/zapier.generated.d.ts b/packages/core/types/zapier.generated.d.ts
index 9cbe5dfc0..6554ec581 100644
--- a/packages/core/types/zapier.generated.d.ts
+++ b/packages/core/types/zapier.generated.d.ts
@@ -1316,6 +1316,34 @@ export interface Search {
operation: BasicActionOperation;
}
+/**
+ * Currently an **internal-only** feature. Zapier uses this
+ * configuration for creating objects in bulk.
+ *
+ * [Docs: BufferConfigSchema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#BufferConfigSchema)
+ */
+export interface BufferConfig {
+ /**
+ * The list of keys of input fields to group bulk-create with. The
+ * actual user data provided for the fields will be used during
+ * execution. Note that a required input field should be referenced
+ * to get user data always.
+ *
+ * @minItems 1
+ */
+ groupedBy: unknown[];
+
+ /**
+ * The maximum number of items to call `performBuffer` with.
+ * **Note** that it is capped by the platform to prevent exceeding
+ * the [AWS Lambda's request/response payload size quota of 6
+ * MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution).
+ * Also, the execution is time-bound; we recommend reducing it upon
+ * consistent timeout.
+ */
+ limit: number;
+}
+
/**
* Represents the fundamental mechanics of a create.
*
@@ -1332,9 +1360,11 @@ export interface BasicCreateActionOperation {
/**
* How will Zapier get the data? This can be a function like `(z) =>
- * [{id: 123}]` or a request like `{url: 'http...'}`.
+ * [{id: 123}]` or a request like `{url: 'http...'}`. Exactly one of
+ * `perform` or `performBuffer` must be defined. If you choose to
+ * define `buffer` and `performBuffer`, you must omit `perform`.
*/
- perform: Request | Function;
+ perform?: Request | Function;
/**
* Internal pointer to a function from the original source or the
@@ -1385,6 +1415,21 @@ export interface BasicCreateActionOperation {
* concurrency)?
*/
shouldLock?: boolean;
+
+ /**
+ * Currently an **internal-only** feature. Zapier uses this
+ * configuration for creating objects in bulk with `performBuffer`.
+ */
+ buffer?: BufferConfig;
+
+ /**
+ * Internal pointer to a function from the original source or the
+ * source code itself. Encodes arity and if `arguments` is used in
+ * the body. Note - just write normal functions and the system will
+ * encode the pointers for you. Or, provide {source: "return 1 + 2"}
+ * and the system will wrap in a function for you.
+ */
+ performBuffer?: Function;
}
/**
diff --git a/packages/schema/docs/build/schema.md b/packages/schema/docs/build/schema.md
index 180f7a00d..f38f4a855 100644
--- a/packages/schema/docs/build/schema.md
+++ b/packages/schema/docs/build/schema.md
@@ -24,6 +24,7 @@ This is automatically generated by the `npm run docs` command in `zapier-platfor
* [/BasicHookOperationSchema](#basichookoperationschema)
* [/BasicOperationSchema](#basicoperationschema)
* [/BasicPollingOperationSchema](#basicpollingoperationschema)
+* [/BufferConfigSchema](#bufferconfigschema)
* [/BulkReadSchema](#bulkreadschema)
* [/BulkReadsSchema](#bulkreadsschema)
* [/CreateSchema](#createschema)
@@ -419,7 +420,7 @@ Represents the fundamental mechanics of a create.
Key | Required | Type | Description
--- | -------- | ---- | -----------
`resource` | no | [/KeySchema](#keyschema) | Optionally reference and extends a resource. Allows Zapier to automatically tie together samples, lists and hooks, greatly improving the UX. EG: if you had another trigger reusing a resource but filtering the results.
-`perform` | **yes** | oneOf([/RequestSchema](#requestschema), [/FunctionSchema](#functionschema)) | How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.
+`perform` | no (with exceptions, see description) | oneOf([/RequestSchema](#requestschema), [/FunctionSchema](#functionschema)) | How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`. Exactly one of `perform` or `performBuffer` must be defined. If you choose to define `buffer` and `performBuffer`, you must omit `perform`.
`performResume` | no | [/FunctionSchema](#functionschema) | A function that parses data from a perform (which uses z.generateCallbackUrl()) and callback request to resume this action.
`performGet` | no | oneOf([/RequestSchema](#requestschema), [/FunctionSchema](#functionschema)) | How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.
`inputFields` | no | [/DynamicFieldsSchema](#dynamicfieldsschema) | What should the form a user sees and configures look like?
@@ -428,6 +429,8 @@ Key | Required | Type | Description
`lock` | no | [/LockObjectSchema](#lockobjectschema) | **INTERNAL USE ONLY**. Zapier uses this configuration for internal operation locking.
`throttle` | no | [/ThrottleObjectSchema](#throttleobjectschema) | Zapier uses this configuration to apply throttling when the limit for the window is exceeded.
`shouldLock` | no | `boolean` | Should this action be performed one at a time (avoid concurrency)?
+`buffer` | no (with exceptions, see description) | [/BufferConfigSchema](#bufferconfigschema) | Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk with `performBuffer`.
+`performBuffer` | no (with exceptions, see description) | [/FunctionSchema](#functionschema) | Currently an **internal-only** feature. A function to create objects in bulk with. `buffer` and `performBuffer` must either both be defined or neither. Additionally, only one of `perform` or `performBuffer` can be defined. If you choose to define `perform`, you must omit `buffer` and `performBuffer`.
#### Examples
@@ -592,6 +595,34 @@ Key | Required | Type | Description
-----
+## /BufferConfigSchema
+
+Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk.
+
+#### Details
+
+* **Type** - `object`
+* [**Source Code**](https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@15.14.0/packages/schema/lib/schemas/BufferConfigSchema.js)
+
+#### Properties
+
+Key | Required | Type | Description
+--- | -------- | ---- | -----------
+`groupedBy` | **yes** | `array` | The list of keys of input fields to group bulk-create with. The actual user data provided for the fields will be used during execution. Note that a required input field should be referenced to get user data always.
+`limit` | **yes** | `integer` | The maximum number of items to call `performBuffer` with. **Note** that it is capped by the platform to prevent exceeding the [AWS Lambda's request/response payload size quota of 6 MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution). Also, the execution is time-bound; we recommend reducing it upon consistent timeout.
+
+#### Examples
+
+* `{ groupedBy: [ 'workspace', 'sheet' ], limit: 100 }`
+
+#### Anti-Examples
+
+* `{ groupedBy: [], limit: 100 }` - _Empty groupedBy list provided: `[]`._
+* `{ groupedBy: [ 'workspace' ] }` - _Missing required key: `limit`._
+* `{ limit: 1 }` - _Missing required key: `groupedBy`._
+
+-----
+
## /BulkReadSchema
How will Zapier fetch resources from your application?
diff --git a/packages/schema/exported-schema.json b/packages/schema/exported-schema.json
index 6ada9eef2..51f49e6e4 100644
--- a/packages/schema/exported-schema.json
+++ b/packages/schema/exported-schema.json
@@ -1360,18 +1360,35 @@
},
"additionalProperties": false
},
+ "BufferConfigSchema": {
+ "id": "/BufferConfigSchema",
+ "description": "Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk.",
+ "type": "object",
+ "required": ["groupedBy", "limit"],
+ "properties": {
+ "groupedBy": {
+ "description": "The list of keys of input fields to group bulk-create with. The actual user data provided for the fields will be used during execution. Note that a required input field should be referenced to get user data always.",
+ "type": "array",
+ "minItems": 1
+ },
+ "limit": {
+ "description": "The maximum number of items to call `performBuffer` with. **Note** that it is capped by the platform to prevent exceeding the [AWS Lambda's request/response payload size quota of 6 MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution). Also, the execution is time-bound; we recommend reducing it upon consistent timeout.",
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
"BasicCreateActionOperationSchema": {
"id": "/BasicCreateActionOperationSchema",
"description": "Represents the fundamental mechanics of a create.",
"type": "object",
- "required": ["perform"],
"properties": {
"resource": {
"description": "Optionally reference and extends a resource. Allows Zapier to automatically tie together samples, lists and hooks, greatly improving the UX. EG: if you had another trigger reusing a resource but filtering the results.",
"$ref": "/KeySchema"
},
"perform": {
- "description": "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.",
+ "description": "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`. Exactly one of `perform` or `performBuffer` must be defined. If you choose to define `buffer` and `performBuffer`, you must omit `perform`.",
"oneOf": [
{
"$ref": "/RequestSchema"
@@ -1379,7 +1396,13 @@
{
"$ref": "/FunctionSchema"
}
- ]
+ ],
+ "docAnnotation": {
+ "required": {
+ "type": "replace",
+ "value": "no (with exceptions, see description)"
+ }
+ }
},
"performResume": {
"description": "A function that parses data from a perform (which uses z.generateCallbackUrl()) and callback request to resume this action.",
@@ -1426,6 +1449,26 @@
"shouldLock": {
"description": "Should this action be performed one at a time (avoid concurrency)?",
"type": "boolean"
+ },
+ "buffer": {
+ "description": "Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk with `performBuffer`.",
+ "$ref": "/BufferConfigSchema",
+ "docAnnotation": {
+ "required": {
+ "type": "replace",
+ "value": "no (with exceptions, see description)"
+ }
+ }
+ },
+ "performBuffer": {
+ "description": "Currently an **internal-only** feature. A function to create objects in bulk with. `buffer` and `performBuffer` must either both be defined or neither. Additionally, only one of `perform` or `performBuffer` can be defined. If you choose to define `perform`, you must omit `buffer` and `performBuffer`.",
+ "$ref": "/FunctionSchema",
+ "docAnnotation": {
+ "required": {
+ "type": "replace",
+ "value": "no (with exceptions, see description)"
+ }
+ }
}
},
"additionalProperties": false
diff --git a/packages/schema/lib/functional-constraints/bufferedCreateConstraints.js b/packages/schema/lib/functional-constraints/bufferedCreateConstraints.js
new file mode 100644
index 000000000..158599153
--- /dev/null
+++ b/packages/schema/lib/functional-constraints/bufferedCreateConstraints.js
@@ -0,0 +1,102 @@
+'use strict';
+
+const _ = require('lodash');
+const jsonschema = require('jsonschema');
+
+const bufferedCreateConstraints = (definition) => {
+ const errors = [];
+ const actionType = 'creates';
+
+ if (definition[actionType]) {
+ _.each(definition[actionType], (actionDef) => {
+ if (actionDef.operation && actionDef.operation.buffer) {
+ if (!actionDef.operation.performBuffer) {
+ errors.push(
+ new jsonschema.ValidationError(
+ 'must contain property "performBuffer" because property "buffer" is present.',
+ actionDef.operation,
+ '/BasicCreateActionOperationSchema',
+ `instance.${actionType}.${actionDef.key}.operation`,
+ 'missing',
+ 'performBuffer'
+ )
+ );
+ }
+
+ if (actionDef.operation.perform) {
+ errors.push(
+ new jsonschema.ValidationError(
+ 'must not contain property "perform" because it is mutually exclusive with property "buffer".',
+ actionDef.operation,
+ '/BasicCreateActionOperationSchema',
+ `instance.${actionType}.${actionDef.key}.operation`,
+ 'invalid',
+ 'perform'
+ )
+ );
+ }
+
+ if (actionDef.operation.buffer.groupedBy) {
+ const requiredInputFields = [];
+ const inputFields = _.get(
+ actionDef,
+ ['operation', 'inputFields'],
+ []
+ );
+ inputFields.forEach((inputField) => {
+ if (inputField.required) {
+ requiredInputFields.push(inputField.key);
+ }
+ });
+
+ actionDef.operation.buffer.groupedBy.forEach((field, index) => {
+ if (!requiredInputFields.includes(field)) {
+ errors.push(
+ new jsonschema.ValidationError(
+ `cannot use optional or non-existent inputField "${field}".`,
+ actionDef.operation.buffer,
+ '/BufferConfigSchema',
+ `instance.${actionType}.${actionDef.key}.operation.buffer.groupedBy[${index}]`,
+ 'invalid',
+ 'groupedBy'
+ )
+ );
+ }
+ });
+ }
+ }
+
+ if (actionDef.operation && actionDef.operation.performBuffer) {
+ if (!actionDef.operation.buffer) {
+ errors.push(
+ new jsonschema.ValidationError(
+ 'must contain property "buffer" because property "performBuffer" is present.',
+ actionDef.operation,
+ '/BasicCreateActionOperationSchema',
+ `instance.${actionType}.${actionDef.key}.operation`,
+ 'missing',
+ 'buffer'
+ )
+ );
+ }
+
+ if (actionDef.operation.perform) {
+ errors.push(
+ new jsonschema.ValidationError(
+ 'must not contain property "perform" because it is mutually exclusive with property "performBuffer".',
+ actionDef.operation,
+ '/BasicCreateActionOperationSchema',
+ `instance.${actionType}.${actionDef.key}.operation`,
+ 'invalid',
+ 'perform'
+ )
+ );
+ }
+ }
+ });
+ }
+
+ return errors;
+};
+
+module.exports = bufferedCreateConstraints;
diff --git a/packages/schema/lib/functional-constraints/index.js b/packages/schema/lib/functional-constraints/index.js
index 9b801f976..838d5f3f9 100644
--- a/packages/schema/lib/functional-constraints/index.js
+++ b/packages/schema/lib/functional-constraints/index.js
@@ -18,6 +18,8 @@ const checks = [
require('./matchingKeys'),
require('./labelWhenVisible'),
require('./uniqueInputFieldKeys'),
+ require('./bufferedCreateConstraints'),
+ require('./requirePerformConditionally'),
require('./pollingThrottle'),
];
diff --git a/packages/schema/lib/functional-constraints/requirePerformConditionally.js b/packages/schema/lib/functional-constraints/requirePerformConditionally.js
new file mode 100644
index 000000000..de53594fc
--- /dev/null
+++ b/packages/schema/lib/functional-constraints/requirePerformConditionally.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const _ = require('lodash');
+const jsonschema = require('jsonschema');
+
+const requirePerformConditionally = (definition) => {
+ const errors = [];
+ const actionType = 'creates';
+
+ if (definition[actionType]) {
+ _.each(definition[actionType], (actionDef) => {
+ if (
+ actionDef.operation &&
+ !actionDef.operation.buffer &&
+ !actionDef.operation.performBuffer &&
+ !actionDef.operation.perform
+ ) {
+ errors.push(
+ new jsonschema.ValidationError(
+ 'requires property "perform".',
+ actionDef.operation,
+ '/BasicCreateActionOperationSchema',
+ `instance.${actionType}.${actionDef.key}.operation`,
+ 'required',
+ 'perform'
+ )
+ );
+ }
+ });
+ }
+
+ return errors;
+};
+
+module.exports = requirePerformConditionally;
diff --git a/packages/schema/lib/schemas/BasicCreateActionOperationSchema.js b/packages/schema/lib/schemas/BasicCreateActionOperationSchema.js
index 22e83f9a9..ae0b670c7 100644
--- a/packages/schema/lib/schemas/BasicCreateActionOperationSchema.js
+++ b/packages/schema/lib/schemas/BasicCreateActionOperationSchema.js
@@ -3,6 +3,9 @@
const makeSchema = require('../utils/makeSchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
+const BufferConfigSchema = require('./BufferConfigSchema');
+const FunctionSchema = require('./FunctionSchema');
+const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
@@ -20,7 +23,45 @@ BasicCreateActionOperationSchema.properties.shouldLock = {
type: 'boolean',
};
+BasicCreateActionOperationSchema.properties.perform = {
+ description:
+ "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`. Exactly one of `perform` or `performBuffer` must be defined. If you choose to define `buffer` and `performBuffer`, you must omit `perform`.",
+ oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
+ docAnnotation: {
+ required: {
+ type: 'replace', // replace or append
+ value: 'no (with exceptions, see description)',
+ },
+ },
+};
+
+BasicCreateActionOperationSchema.properties.buffer = {
+ description:
+ 'Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk with `performBuffer`.',
+ $ref: BufferConfigSchema.id,
+ docAnnotation: {
+ required: {
+ type: 'replace', // replace or append
+ value: 'no (with exceptions, see description)',
+ },
+ },
+};
+
+BasicCreateActionOperationSchema.properties.performBuffer = {
+ description:
+ 'Currently an **internal-only** feature. A function to create objects in bulk with. `buffer` and `performBuffer` must either both be defined or neither. Additionally, only one of `perform` or `performBuffer` can be defined. If you choose to define `perform`, you must omit `buffer` and `performBuffer`.',
+ $ref: FunctionSchema.id,
+ docAnnotation: {
+ required: {
+ type: 'replace', // replace or append
+ value: 'no (with exceptions, see description)',
+ },
+ },
+};
+
+delete BasicCreateActionOperationSchema.required;
+
module.exports = makeSchema(
BasicCreateActionOperationSchema,
- BasicActionOperationSchema.dependencies
+ BasicActionOperationSchema.dependencies.concat(BufferConfigSchema)
);
diff --git a/packages/schema/lib/schemas/BufferConfigSchema.js b/packages/schema/lib/schemas/BufferConfigSchema.js
new file mode 100644
index 000000000..a36548877
--- /dev/null
+++ b/packages/schema/lib/schemas/BufferConfigSchema.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const makeSchema = require('../utils/makeSchema');
+
+module.exports = makeSchema({
+ id: '/BufferConfigSchema',
+ description:
+ 'Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk.',
+ type: 'object',
+ required: ['groupedBy', 'limit'],
+ properties: {
+ groupedBy: {
+ description:
+ 'The list of keys of input fields to group bulk-create with. The actual user data provided for the fields will be used during execution. Note that a required input field should be referenced to get user data always.',
+ type: 'array',
+ minItems: 1,
+ },
+ limit: {
+ description:
+ "The maximum number of items to call `performBuffer` with. **Note** that it is capped by the platform to prevent exceeding the [AWS Lambda's request/response payload size quota of 6 MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution). Also, the execution is time-bound; we recommend reducing it upon consistent timeout.",
+ type: 'integer',
+ },
+ },
+ examples: [
+ {
+ groupedBy: ['workspace', 'sheet'],
+ limit: 100,
+ },
+ ],
+ antiExamples: [
+ {
+ example: {
+ groupedBy: [],
+ limit: 100,
+ },
+ reason: 'Empty groupedBy list provided: `[]`.',
+ },
+ {
+ example: { groupedBy: ['workspace'] },
+ reason: 'Missing required key: `limit`.',
+ },
+ {
+ example: { limit: 1 },
+ reason: 'Missing required key: `groupedBy`.',
+ },
+ ],
+ additionalProperties: false,
+});
diff --git a/packages/schema/test/functional-constraints/bufferedCreateConstraints.js b/packages/schema/test/functional-constraints/bufferedCreateConstraints.js
new file mode 100644
index 000000000..24e2936b8
--- /dev/null
+++ b/packages/schema/test/functional-constraints/bufferedCreateConstraints.js
@@ -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);
+ });
+});
diff --git a/packages/schema/test/functional-constraints/pollingThrottle.js b/packages/schema/test/functional-constraints/pollingThrottle.js
index d10528323..2271b2b2d 100644
--- a/packages/schema/test/functional-constraints/pollingThrottle.js
+++ b/packages/schema/test/functional-constraints/pollingThrottle.js
@@ -46,7 +46,7 @@ describe('pollingThrottle', () => {
);
// for polling trigger with operation.type unset
- delete operation.type
+ delete operation.type;
results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql(
diff --git a/packages/schema/test/index.js b/packages/schema/test/index.js
index 211eed549..dbd469ba9 100644
--- a/packages/schema/test/index.js
+++ b/packages/schema/test/index.js
@@ -9,7 +9,7 @@ const appDefinition = require('../examples/definition.json');
const copy = (o) => JSON.parse(JSON.stringify(o));
-const NUM_SCHEMAS = 55; // changes regularly as we expand
+const NUM_SCHEMAS = 56; // changes regularly as we expand
describe('app', () => {
describe('validation', () => {