UNPKG

n8n-nodes-base

Version:

Base nodes of n8n

301 lines 13.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.koBoToolboxApiRequest = koBoToolboxApiRequest; exports.koBoToolboxRawRequest = koBoToolboxRawRequest; exports.parseStringList = parseStringList; exports.formatSubmission = formatSubmission; exports.validateAttachmentUrl = validateAttachmentUrl; exports.downloadAttachments = downloadAttachments; exports.loadForms = loadForms; const clone_1 = __importDefault(require("lodash/clone")); const compact_1 = __importDefault(require("lodash/compact")); const concat_1 = __importDefault(require("lodash/concat")); const escapeRegExp_1 = __importDefault(require("lodash/escapeRegExp")); const every_1 = __importDefault(require("lodash/every")); const first_1 = __importDefault(require("lodash/first")); const isArray_1 = __importDefault(require("lodash/isArray")); const isString_1 = __importDefault(require("lodash/isString")); const last_1 = __importDefault(require("lodash/last")); const set_1 = __importDefault(require("lodash/set")); const some_1 = __importDefault(require("lodash/some")); const split_1 = __importDefault(require("lodash/split")); const toNumber_1 = __importDefault(require("lodash/toNumber")); const toString_1 = __importDefault(require("lodash/toString")); const trim_1 = __importDefault(require("lodash/trim")); const n8n_workflow_1 = require("n8n-workflow"); async function koBoToolboxApiRequest(option = {}) { const credentials = await this.getCredentials('koBoToolboxApi'); // Set up pagination / scrolling const returnAll = !!option.returnAll; if (returnAll) { // Override manual pagination options (0, set_1.default)(option, 'qs.limit', 3000); // Don't pass this custom param to helpers.httpRequest delete option.returnAll; } const options = { url: '', headers: { Accept: 'application/json', }, json: true, }; if (Object.keys(option)) { Object.assign(options, option); } if (options.url && !/^http(s)?:/.test(options.url)) { options.url = credentials.URL + options.url; } let results = null; let keepLooking = true; while (keepLooking) { const response = await this.helpers.httpRequestWithAuthentication.call(this, 'koBoToolboxApi', options); // Append or set results results = response.results ? (0, concat_1.default)(results || [], response.results) : response; if (returnAll && response.next) { options.url = response.next; } else { keepLooking = false; } } return results; } async function koBoToolboxRawRequest(option) { const credentials = await this.getCredentials('koBoToolboxApi'); if (option.url && !/^http(s)?:/.test(option.url)) { option.url = credentials.URL + option.url; } return await this.helpers.httpRequestWithAuthentication.call(this, 'koBoToolboxApi', option); } function parseGeoPoint(geoPoint) { // Check if it looks like a "lat lon z precision" flat string e.g. "-1.931161 30.079811 0 0" (lat, lon, elevation, precision) // NOTE: we are discarding the elevation and precision values since they're not (well) supported in GeoJSON const coordinates = (0, split_1.default)(geoPoint, ' '); if (coordinates.length >= 2 && (0, every_1.default)(coordinates, (coord) => coord && /^-?\d+(?:\.\d+)?$/.test((0, toString_1.default)(coord)))) { // NOTE: GeoJSON uses lon, lat, while most common systems use lat, lon order! return [(0, toNumber_1.default)(coordinates[1]), (0, toNumber_1.default)(coordinates[0])]; } return null; } function parseStringList(value) { return (0, split_1.default)((0, toString_1.default)(value), /[\s,]+/); } const matchWildcard = (value, pattern) => { const regex = new RegExp(`^${(0, escapeRegExp_1.default)(pattern).replace('\\*', '.*')}$`); return regex.test(value); }; const formatValue = (value, format) => { if ((0, isString_1.default)(value)) { // Sanitize value value = (0, toString_1.default)(value); // Parse geoPoints const geoPoint = parseGeoPoint(value); if (geoPoint) { return { type: 'Point', coordinates: geoPoint, }; } // Check if it's a closed polygon geo-shape: -1.954117 30.085159 0 0;-1.955005 30.084622 0 0;-1.956057 30.08506 0 0;-1.956393 30.086229 0 0;-1.955853 30.087143 0 0;-1.954609 30.08725 0 0;-1.953966 30.086735 0 0;-1.953805 30.085897 0 0;-1.954117 30.085159 0 0 const points = value.split(';'); if (points.length >= 2 && /^[-\d\.\s;]+$/.test(value)) { // Using the GeoJSON format as per https://geojson.org/ const coordinates = (0, compact_1.default)(points.map(parseGeoPoint)); // Only return if all values are properly parsed if (coordinates.length === points.length) { // If the shape is closed, declare it as Polygon, otherwise as LineString if ((0, first_1.default)(points) === (0, last_1.default)(points)) { return { type: 'Polygon', coordinates: [coordinates], }; } return { type: 'LineString', coordinates }; } } // Parse numbers if ('number' === format) { return (0, toNumber_1.default)(value); } // Split multi-select if ('multiSelect' === format) { return (0, split_1.default)((0, toString_1.default)(value), ' '); } } return value; }; function formatSubmission(submission, selectMasks = [], numberMasks = []) { // Create a shallow copy of the submission const response = {}; for (const key of Object.keys(submission)) { let value = (0, clone_1.default)(submission[key]); // Sanitize key names: split by group, trim _ const sanitizedKey = key .split('/') .map((k) => (0, trim_1.default)(k, ' _')) .join('.'); const leafKey = sanitizedKey.split('.').pop() || ''; let format = 'string'; if ((0, some_1.default)(numberMasks, (mask) => matchWildcard(leafKey, mask))) { format = 'number'; } if ((0, some_1.default)(selectMasks, (mask) => matchWildcard(leafKey, mask))) { format = 'multiSelect'; } value = formatValue(value, format); (0, set_1.default)(response, sanitizedKey, value); } // Reformat _geolocation if ((0, isArray_1.default)(response.geolocation) && response.geolocation.length === 2 && response.geolocation[0] && response.geolocation[1]) { response.geolocation = { type: 'Point', coordinates: [response.geolocation[1], response.geolocation[0]], }; } return response; } const KOBO_DOMAINS = ['kobotoolbox.org', 'humanitarianresponse.info']; function validateAttachmentUrl(url, allowedDomain) { let attachmentUrl; let allowedUrl; try { attachmentUrl = new URL(url); } catch { throw new Error('Invalid attachment URL format'); } if (!['http:', 'https:'].includes(attachmentUrl.protocol)) { throw new Error('Attachment URL must use HTTP or HTTPS'); } try { if (!allowedDomain.startsWith('http://') && !allowedDomain.startsWith('https://')) { allowedDomain = `https://${allowedDomain}`; } allowedUrl = new URL(allowedDomain); } catch { throw new Error('Invalid allowed domain configuration'); } const attachmentHost = attachmentUrl.hostname.toLowerCase(); const allowedHost = allowedUrl.hostname.toLowerCase(); const matchesDomain = attachmentHost === allowedHost || attachmentHost.endsWith(`.${allowedHost}`); if (matchesDomain) return; const isKoboDomain = KOBO_DOMAINS.some((domain) => allowedHost === domain || allowedHost.endsWith(`.${domain}`)); if (isKoboDomain) { const isFromKoboDomain = KOBO_DOMAINS.some((domain) => attachmentHost === domain || attachmentHost.endsWith(`.${domain}`)); if (isFromKoboDomain) return; } throw new Error(`Attachment URL domain (${attachmentHost}) does not match allowed domain (${allowedHost})`); } async function downloadAttachments(submission, options) { // Initialize return object with the original submission JSON content const binaryItem = { json: { ...submission, }, binary: {}, }; const credentials = await this.getCredentials('koBoToolboxApi'); const koboToolboxUrl = credentials.URL; // Look for attachment links - there can be more than one const attachmentList = (submission._attachments || submission.attachments); if (attachmentList?.length) { for (const [index, attachment] of attachmentList.entries()) { // look for the question name linked to this attachment const fileName = attachment.filename; const sanitizedFileName = (0, toString_1.default)(fileName).replace(/_[^_]+(?=\.\w+)/, ''); // strip suffix let relatedQuestion = null; if ('question' === options.binaryNamingScheme) { for (const question of Object.keys(submission)) { // The logic to map attachments to question is sometimes ambiguous: // - If the attachment is linked to a question, the question's value is the same as the attachment's filename (with spaces replaced by underscores) // - BUT sometimes the attachment's filename has an extra suffix, e.g. "My_Picture_0OdlaKJ.jpg", would map to the question "picture": "My Picture.jpg" const sanitizedQuestionValue = (0, toString_1.default)(submission[question]).replace(/\s/g, '_'); // replace spaces with underscores if (sanitizedFileName === sanitizedQuestionValue) { relatedQuestion = question; // Just use the first match... break; } } } // Download attachment // NOTE: this needs to follow redirects (possibly across domains), while keeping Authorization headers // The Axios client will not propagate the Authorization header on redirects (see https://github.com/axios/axios/issues/3607), so we need to follow ourselves... let response = null; const attachmentUrl = attachment[options.version] || attachment.download_url; try { validateAttachmentUrl(attachmentUrl, koboToolboxUrl); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message); } let final = false, redir = 0; const axiosOptions = { url: attachmentUrl, method: 'GET', headers: { Authorization: `Token ${credentials.token}`, }, ignoreHttpStatusErrors: true, returnFullResponse: true, disableFollowRedirect: true, encoding: 'arraybuffer', }; while (!final && redir < 5) { response = await this.helpers.httpRequest(axiosOptions); if (response?.headers.location) { try { validateAttachmentUrl(response.headers.location, koboToolboxUrl); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message); } // Follow redirect axiosOptions.url = response.headers.location; redir++; } else { final = true; } } if (response?.body) { // Use the provided prefix if any, otherwise try to use the original question name let binaryName; if ('question' === options.binaryNamingScheme && relatedQuestion) { binaryName = relatedQuestion; } else { binaryName = `${options.dataPropertyAttachmentsPrefixName || 'attachment_'}${index}`; } binaryItem.binary[binaryName] = await this.helpers.prepareBinaryData(response.body, fileName); } } } else { delete binaryItem.binary; } // Add item to final output - even if there's no attachment retrieved return binaryItem; } async function loadForms() { const responseData = await koBoToolboxApiRequest.call(this, { url: '/api/v2/assets/', qs: { q: 'asset_type:survey', ordering: 'name', }, scroll: true, }); return responseData?.map((survey) => ({ name: survey.name, value: survey.uid })) || []; } //# sourceMappingURL=GenericFunctions.js.map