UNPKG

@grouparoo/core

Version:
266 lines (265 loc) 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RecordPropertyOps = void 0; const max_1 = require("libphonenumber-js/max"); const isEmail_1 = __importDefault(require("../validators/isEmail")); const isURL_1 = __importDefault(require("validator/lib/isURL")); const RecordProperty_1 = require("../../models/RecordProperty"); const cls_1 = require("../cls"); const sequelize_1 = require("sequelize"); const actionhero_1 = require("actionhero"); const propertiesCache_1 = require("../../modules/caches/propertiesCache"); const plugin_1 = require("../plugin"); var RecordPropertyOps; (function (RecordPropertyOps) { const defaultRecordPropertyProcessingDelay = 1000 * 60 * 5; async function buildRawValue(value, type, recordProperty) { let rawValue = null; let invalidValue = null; let invalidReason = null; if (value === null || value === undefined || value === "") { return { rawValue, invalidValue, invalidReason }; } try { switch (type) { case "float": rawValue = await formatFloat(value === null || value === void 0 ? void 0 : value.toString()); break; case "integer": rawValue = await formatInteger(value === null || value === void 0 ? void 0 : value.toString()); break; case "date": rawValue = await formatDate(value); break; case "string": rawValue = await formatString(value === null || value === void 0 ? void 0 : value.toString()); break; case "email": rawValue = await formatEmail(value === null || value === void 0 ? void 0 : value.toString()); break; case "phoneNumber": rawValue = await formatPhoneNumber(value === null || value === void 0 ? void 0 : value.toString()); break; case "url": rawValue = await formatURL(value === null || value === void 0 ? void 0 : value.toString()); break; case "boolean": rawValue = await formatBoolean(value === null || value === void 0 ? void 0 : value.toString()); break; default: throw new Error(`cannot coerce recordProperty ${value} to type ${type}`); } } catch (error) { rawValue = null; invalidValue = value === null || value === void 0 ? void 0 : value.toString(); if (recordProperty && error instanceof Error) { error.message += ` for record ${recordProperty.recordId}`; } invalidReason = `Invalid ${type} value`; (0, actionhero_1.log)(error, "error"); } return { rawValue, invalidValue, invalidReason }; } RecordPropertyOps.buildRawValue = buildRawValue; function getValue(rawValue, type) { if (rawValue === null || rawValue === undefined) return null; switch (type) { case "float": return parseFloat(rawValue); case "integer": return parseInt(rawValue, 10); case "date": return new Date(parseInt(rawValue)); case "string": return rawValue; case "email": return rawValue; case "phoneNumber": return rawValue; case "url": return rawValue; case "boolean": if ([true, 1, "true"].includes(rawValue)) { return true; } else { return false; } default: throw new Error(`cannot coerce recordProperty ${rawValue} into type ${type}`); } } RecordPropertyOps.getValue = getValue; async function processPendingRecordProperties(source, limit = 100, delayMs = defaultRecordPropertyProcessingDelay) { var _a; if (!delayMs || delayMs < defaultRecordPropertyProcessingDelay) { delayMs = defaultRecordPropertyProcessingDelay; } if (source.state !== "ready") return []; const { pluginConnection } = await source.getPlugin(); const properties = (await propertiesCache_1.PropertiesCache.findAllWithCache(undefined, "ready")).filter((p) => p.sourceId === source.id); const pendingRecordPropertyIds = []; const unGroupedProperties = []; const propertyGroups = {}; for (const property of properties) { const options = await property.getOptions(); const filters = await property.getFilters(); const aggregationMethod = options.aggregationMethod; if (((_a = pluginConnection.groupAggregations) === null || _a === void 0 ? void 0 : _a.includes(aggregationMethod)) && filters.length === 0 && property.isArray === false) { if (aggregationMethod && !propertyGroups[aggregationMethod]) { propertyGroups[aggregationMethod] = []; } propertyGroups[aggregationMethod].push(property); } else { unGroupedProperties.push(property); } } // groupedProperties for (const aggregationMethod in propertyGroups) { const pendingRecordProperties = await RecordProperty_1.RecordProperty.findAll({ where: { propertyId: { [sequelize_1.Op.in]: propertyGroups[aggregationMethod].map((p) => p.id), }, state: "pending", startedAt: { [sequelize_1.Op.or]: [null, { [sequelize_1.Op.lt]: new Date().getTime() - delayMs }], }, }, order: [["recordId", "asc"]], limit: limit * propertyGroups[aggregationMethod].length, }); await preparePropertyImports(pluginConnection, pendingRecordProperties, propertyGroups[aggregationMethod], limit); pendingRecordPropertyIds.push(...propertyGroups[aggregationMethod].map((p) => p.id)); } // unGroupedProperties for (const property of unGroupedProperties) { const pendingRecordProperties = await RecordProperty_1.RecordProperty.findAll({ where: { propertyId: property.id, state: "pending", startedAt: { [sequelize_1.Op.or]: [null, { [sequelize_1.Op.lt]: new Date().getTime() - delayMs }], }, }, order: [["recordId", "asc"]], limit, }); await preparePropertyImports(pluginConnection, pendingRecordProperties, [property], limit); pendingRecordPropertyIds.push(property.id); } return pendingRecordPropertyIds; } RecordPropertyOps.processPendingRecordProperties = processPendingRecordProperties; })(RecordPropertyOps = exports.RecordPropertyOps || (exports.RecordPropertyOps = {})); // utilities (private) async function preparePropertyImports(pluginConnection, pendingRecordProperties, properties, limit) { if (pendingRecordProperties.length === 0) return; if (properties.length === 0) return; const uniqueRecordIds = pendingRecordProperties .map((ppp) => ppp.recordId) .filter((val, idx, arr) => arr.indexOf(val) === idx); const method = pluginConnection.methods.recordProperties ? "RecordProperties" : pluginConnection.methods.recordProperty ? "RecordProperty" : null; await RecordProperty_1.RecordProperty.updateAllInBatches(pendingRecordProperties, { startedAt: new Date(), }); if (method === "RecordProperties") { while (uniqueRecordIds.length > 0) { await cls_1.CLS.enqueueTask(`recordProperty:importRecordProperties`, { propertyIds: properties.map((p) => p.id), recordIds: uniqueRecordIds.splice(0, limit), }); } } else if (method === "RecordProperty") { for (const property of properties) { for (const recordId of uniqueRecordIds) { await cls_1.CLS.enqueueTask(`recordProperty:importRecordProperty`, { propertyId: property.id, recordId, }); } } } else { // Schedule sources don't import properties on-demand, keep old value await RecordProperty_1.RecordProperty.updateAllInBatches(pendingRecordProperties, { state: "ready", stateChangedAt: new Date(), confirmedAt: new Date(), }); } } // formatters and validators async function formatFloat(v) { // try to parse const parsed = parseFloat(v); if (isNaN(parsed)) throw new Error(`float "${v}" is not valid`); return parsed.toString(); } async function formatInteger(v) { // try to parse const parsed = parseInt(v, 10); if (isNaN(parsed)) throw new Error(`integer "${v}" is not valid`); return parsed.toString(); } async function formatString(v) { // Any string is valid return v; } async function formatDate(v) { // try to parse with new Date() if (v instanceof Date) return v.getTime().toString(); const dateString = new Date(v).getTime().toString(); if (dateString === "NaN") throw new Error(`date "${v}" is not valid`); return dateString; } function formatURL(v) { // We do strong validation on the URL if (!(0, isURL_1.default)(v)) throw new Error(`url "${v}" is not valid`); return v.toLocaleLowerCase(); } function formatEmail(v) { // We do light validation on the email to ensure that it has an "@" and a "." if (!(0, isEmail_1.default)(v)) throw new Error(`email "${v}" is not valid`); return v; } async function formatPhoneNumber(v) { // Use Google's phone number validator and formatter const defaultCountryCode = (await plugin_1.plugin.readSetting("core", "records-default-country-code")).value; const formattedPhoneNumber = (0, max_1.parsePhoneNumberFromString)(v, defaultCountryCode); if (!formattedPhoneNumber || !formattedPhoneNumber.isValid()) { throw new Error(`phone number "${v}" is not valid`); } return formattedPhoneNumber.formatInternational(); } async function formatBoolean(v) { // 1,0 or true,false are valid const check = v.toLocaleLowerCase(); if (["1", "true"].includes(check)) return "true"; if (["0", "false"].includes(check)) return "false"; throw new Error(`${v} is not a valid boolean value`); }