@grouparoo/core
Version:
The Grouparoo Core
266 lines (265 loc) • 11.2 kB
JavaScript
;
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`);
}