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.
310 lines
9.6 KiB
JavaScript
310 lines
9.6 KiB
JavaScript
const _ = require('lodash');
|
|
|
|
const { isFileField, hasFileFields, LazyFile } = require('./file');
|
|
|
|
// Max parts a key can have for unflattening
|
|
const MAX_KEY_PARTS = 6;
|
|
|
|
// Replace '{{bundle.inputData.abc}}' with '{{abc}}'
|
|
const undoCurlyReplacement = (str) =>
|
|
str ? str.replace(/{{\s*bundle\.[^.]+\.([^}\s]+)\s*}}/g, '{{$1}}') : str;
|
|
|
|
// Unflatten from {key__child: value} to {key: {child: value}}
|
|
const unflattenObject = (data, separator = '__') => {
|
|
if (Object(data) !== data || _.isArray(data)) {
|
|
return data;
|
|
}
|
|
|
|
const keys = Object.keys(data);
|
|
|
|
_.each(keys, (key) => {
|
|
if (
|
|
key.includes(separator) &&
|
|
!key.startsWith(separator) &&
|
|
!key.endsWith(separator)
|
|
) {
|
|
const value = data[key];
|
|
const keyParts = key.split(separator, MAX_KEY_PARTS);
|
|
|
|
let i;
|
|
let previousProp = data;
|
|
let currentProp = data;
|
|
|
|
for (i = 0; i < keyParts.length; i++) {
|
|
currentProp[keyParts[i]] = currentProp[keyParts[i]] || {};
|
|
previousProp = currentProp;
|
|
currentProp = currentProp[keyParts[i]];
|
|
}
|
|
|
|
previousProp[keyParts[i - 1]] = value;
|
|
|
|
delete data[key];
|
|
}
|
|
});
|
|
|
|
return data;
|
|
};
|
|
|
|
const convertToQueryString = (object) => {
|
|
if (!object) {
|
|
return '';
|
|
}
|
|
|
|
const objectKeys = Object.keys(object);
|
|
const queryStringItems = _.map(objectKeys, (key) => `${key}=${object[key]}`);
|
|
|
|
return queryStringItems.join('&');
|
|
};
|
|
|
|
//
|
|
// Methods with logic for bundleConverter
|
|
//
|
|
|
|
const addAuthData = (event, bundle, convertedBundle) => {
|
|
// Get authData only for basic auth here, to include in the request
|
|
const { username, password } = _.get(bundle, 'authData', {});
|
|
|
|
if (username && password) {
|
|
convertedBundle.request.auth = [username, password];
|
|
}
|
|
|
|
// OAuth2 specific
|
|
if (event.name.startsWith('auth.oauth2')) {
|
|
convertedBundle.oauth_data = {
|
|
client_id: process.env.CLIENT_ID,
|
|
client_secret: process.env.CLIENT_SECRET,
|
|
};
|
|
convertedBundle.load = _.get(bundle, 'request.body', {});
|
|
}
|
|
};
|
|
|
|
const addInputData = (event, bundle, convertedBundle) => {
|
|
if (event.name === 'auth.connectionLabel') {
|
|
convertedBundle.test_result = bundle.inputData;
|
|
} else if (event.name.startsWith('trigger.')) {
|
|
convertedBundle.trigger_fields = bundle.inputData;
|
|
convertedBundle.trigger_fields_raw =
|
|
bundle.inputDataRaw || bundle.inputData;
|
|
} else if (event.name.startsWith('create.')) {
|
|
convertedBundle.action_fields = bundle._unflatInputData;
|
|
convertedBundle.action_fields_full = bundle.inputData;
|
|
convertedBundle.action_fields_raw = bundle.inputDataRaw || bundle.inputData;
|
|
} else if (event.name.startsWith('search.')) {
|
|
convertedBundle.search_fields = bundle.inputData;
|
|
|
|
if (event.name.startsWith('search.resource')) {
|
|
convertedBundle.read_fields = event.results || bundle.inputData;
|
|
convertedBundle.read_context = bundle.inputData;
|
|
}
|
|
} else if (event.name === 'hydrate.method') {
|
|
Object.assign(convertedBundle, bundle.inputData.bundle);
|
|
} else if (event.name.startsWith('auth.oauth2.token')) {
|
|
// Overwrite convertedBundle.auth_fields with bundle.inputData
|
|
convertedBundle.auth_fields = Object.keys(
|
|
convertedBundle.auth_fields,
|
|
).reduce((result, k) => {
|
|
result[k] = bundle.inputData[k] || convertedBundle.auth_fields[k];
|
|
return result;
|
|
}, {});
|
|
}
|
|
};
|
|
|
|
const addHookData = (event, bundle, convertedBundle) => {
|
|
if (event.name === 'trigger.hook') {
|
|
convertedBundle.request = bundle.rawRequest || convertedBundle.request;
|
|
convertedBundle.cleaned_request = bundle.cleanedRequest;
|
|
|
|
if (!convertedBundle.request.querystring) {
|
|
convertedBundle.request.querystring = convertToQueryString(
|
|
bundle.inputData,
|
|
);
|
|
}
|
|
if (!convertedBundle.request.content) {
|
|
convertedBundle.request.content = convertedBundle.request.data || '';
|
|
}
|
|
} else if (event.name.startsWith('trigger.hook.subscribe')) {
|
|
convertedBundle.target_url = bundle.targetUrl;
|
|
convertedBundle.subscription_url = bundle.targetUrl;
|
|
convertedBundle.event = bundle._legacyEvent;
|
|
convertedBundle.trigger_data = bundle.inputData;
|
|
} else if (event.name.startsWith('trigger.hook.unsubscribe')) {
|
|
convertedBundle.target_url = bundle.targetUrl;
|
|
convertedBundle.subscription_url = bundle.targetUrl;
|
|
convertedBundle.subscribe_data = bundle.subscribeData;
|
|
convertedBundle.event = bundle._legacyEvent;
|
|
convertedBundle.trigger_data = bundle.inputData;
|
|
}
|
|
};
|
|
|
|
const addRequestData = async (event, z, bundle, convertedBundle) => {
|
|
Object.assign(
|
|
convertedBundle.request.headers,
|
|
_.get(bundle, 'request.headers'),
|
|
);
|
|
|
|
Object.assign(
|
|
convertedBundle.request.params,
|
|
_.get(bundle, 'request.params'),
|
|
);
|
|
|
|
const body = _.get(bundle, 'request.body');
|
|
if (!_.isEmpty(body)) {
|
|
let data = body;
|
|
let files;
|
|
|
|
if (typeof data !== 'string' && !event.name.startsWith('auth.oauth2')) {
|
|
if (hasFileFields(bundle)) {
|
|
// Exclude file fields from request.data
|
|
data = Object.keys(body)
|
|
.filter((k) => !isFileField(k, bundle))
|
|
.reduce((result, k) => {
|
|
result[k] = body[k];
|
|
return result;
|
|
}, {});
|
|
|
|
const fileFieldKeys = Object.keys(body).filter((k) =>
|
|
isFileField(k, bundle),
|
|
);
|
|
const fileMetas = await Promise.all(
|
|
fileFieldKeys.map((k) => LazyFile(body[k]).meta()),
|
|
);
|
|
|
|
files = _.zip(fileFieldKeys, fileMetas)
|
|
.map(([k, meta]) => {
|
|
const urlOrContent = body[k];
|
|
return [k, [meta.filename, urlOrContent, meta.contentType]];
|
|
})
|
|
.reduce((result, [k, file]) => {
|
|
result[k] = file;
|
|
return result;
|
|
}, {});
|
|
}
|
|
|
|
data = JSON.stringify(data);
|
|
}
|
|
|
|
convertedBundle.request.data = data;
|
|
|
|
if (!_.isEmpty(files)) {
|
|
convertedBundle.request.files = files;
|
|
delete convertedBundle.request.headers['Content-Type'];
|
|
}
|
|
} else if (event.name.startsWith('auth.oauth2.refresh.pre')) {
|
|
// Make sure bundle.request.data is an object
|
|
convertedBundle.request.data = convertedBundle.request.data || {};
|
|
} else if (event.name.startsWith('create')) {
|
|
if (
|
|
!_.isEmpty(bundle._unflatInputData) ||
|
|
(!event.name.includes('.input') && !event.name.includes('.output'))
|
|
) {
|
|
// Only stringify an empty object when we aren't fetching custom fields
|
|
convertedBundle.request.data = JSON.stringify(bundle._unflatInputData);
|
|
}
|
|
}
|
|
};
|
|
|
|
const addResponse = (event, bundle, convertedBundle) => {
|
|
if (event.name.endsWith('.post')) {
|
|
convertedBundle.response = { ...event.response };
|
|
convertedBundle.response.status_code = convertedBundle.response.status;
|
|
if (convertedBundle.response.request) {
|
|
convertedBundle.response.request = {
|
|
...convertedBundle.response.request,
|
|
};
|
|
// `request.input` contains the entire app definition, which is big and
|
|
// unnecessary for legacy scripting
|
|
delete convertedBundle.response.request.input;
|
|
}
|
|
}
|
|
};
|
|
|
|
const convertBundleMeta = (meta) => {
|
|
if (_.isEmpty(meta)) {
|
|
return {};
|
|
}
|
|
|
|
const newMeta = {
|
|
auth_test:
|
|
meta.auth_test === undefined ? meta.isTestingAuth : meta.auth_test,
|
|
frontend:
|
|
meta.frontend === undefined ? meta.isLoadingSample : meta.frontend,
|
|
prefill:
|
|
meta.prefill === undefined ? meta.isFillingDynamicDropdown : meta.prefill,
|
|
hydrate: true,
|
|
test_poll:
|
|
meta.test_poll === undefined ? meta.isTestingAuth : meta.test_poll,
|
|
first_poll:
|
|
meta.first_poll === undefined ? meta.isPopulatingDedupe : meta.first_poll,
|
|
limit: meta.limit,
|
|
page: meta.page,
|
|
isBulkRead: meta.isBulkRead || false,
|
|
};
|
|
newMeta.standard_poll = !newMeta.test_poll;
|
|
return newMeta;
|
|
};
|
|
|
|
// Convert bundle from CLI to WB based on which event to run
|
|
const bundleConverter = async (bundle, event, z) => {
|
|
let requestMethod = _.get(bundle, 'request.method');
|
|
if (!requestMethod) {
|
|
if (
|
|
event.name.startsWith('create') ||
|
|
event.name.startsWith('auth.oauth2') ||
|
|
event.name.startsWith('trigger.hook.subscribe')
|
|
) {
|
|
requestMethod = 'POST';
|
|
} else if (event.name === 'trigger.hook.unsubscribe.pre') {
|
|
requestMethod = 'DELETE';
|
|
} else {
|
|
requestMethod = 'GET';
|
|
}
|
|
}
|
|
|
|
// From inputData, remove keys that are in fieldsExcludedFromBody
|
|
const excludedFieldKeys = bundle._fieldsExcludedFromBody || [];
|
|
const filteredInputData = Object.keys(bundle.inputData || {})
|
|
.filter((key) => !excludedFieldKeys.includes(key))
|
|
.reduce((fields, key) => {
|
|
return { [key]: bundle.inputData[key], ...fields };
|
|
}, {});
|
|
|
|
// Attach to bundle so we can reuse it
|
|
bundle._unflatInputData = unflattenObject(filteredInputData);
|
|
|
|
const meta = convertBundleMeta(bundle.meta);
|
|
const zap = _.get(bundle, 'meta.zap') || { id: 0 };
|
|
|
|
const convertedBundle = {
|
|
request: {
|
|
method: requestMethod,
|
|
url: undoCurlyReplacement(_.get(bundle, 'request.url', '')),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
params: {},
|
|
data: !requestMethod || requestMethod === 'GET' ? null : '',
|
|
},
|
|
auth_fields: _.get(bundle, 'authData', {}),
|
|
meta,
|
|
zap,
|
|
};
|
|
|
|
if (bundle._legacyUrl) {
|
|
convertedBundle.raw_url = convertedBundle.url_raw = undoCurlyReplacement(
|
|
bundle._legacyUrl,
|
|
);
|
|
}
|
|
|
|
addAuthData(event, bundle, convertedBundle);
|
|
addInputData(event, bundle, convertedBundle);
|
|
await addRequestData(event, z, bundle, convertedBundle);
|
|
addHookData(event, bundle, convertedBundle);
|
|
addResponse(event, bundle, convertedBundle);
|
|
|
|
return convertedBundle;
|
|
};
|
|
|
|
module.exports = {
|
|
bundleConverter,
|
|
unflattenObject,
|
|
};
|