master-zapier-plan-draft/vendor/zapier-platform/packages/legacy-scripting-runner/bundle.js
George Lambert b4150c8250 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.
2026-09-09 02:37:36 -04:00

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,
};