@grouparoo/core
Version:
The Grouparoo Core
358 lines (357 loc) • 14.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GroupOps = void 0;
const actionhero_1 = require("actionhero");
const sequelize_1 = __importStar(require("sequelize"));
const Group_1 = require("../../models/Group");
const GroupMember_1 = require("../../models/GroupMember");
const RecordMultipleAssociationShim_1 = require("../../models/RecordMultipleAssociationShim");
const Import_1 = require("../../models/Import");
const record_1 = require("./record");
var GroupOps;
(function (GroupOps) {
/**
* Given a GrouparooRecord, create an import to recalculate its Group Membership.
*/
async function updateRecords(recordIds, creatorType, creatorId, destinationId) {
const bulkData = [];
for (const recordId of recordIds) {
bulkData.push({
state: "importing",
data: destinationId ? { _meta: { destinationId } } : {},
creatorType,
creatorId,
recordId,
recordAssociatedAt: new Date(),
});
}
let _imports = [];
while (bulkData.length > 0) {
_imports = _imports.concat(await Import_1.Import.bulkCreate(bulkData.splice(0, actionhero_1.config.batchSize.internalWrite)));
}
await record_1.RecordOps.markPendingByIds(recordIds, false);
return _imports;
}
GroupOps.updateRecords = updateRecords;
/**
* Check if the record should be in this Group
*/
async function updateRecordsMembership(group, records) {
const response = {};
records.forEach((p) => (response[p.id] = false));
const existingMemberships = await GroupMember_1.GroupMember.findAll({
where: {
groupId: group.id,
recordId: { [sequelize_1.Op.in]: records.map((p) => p.id) },
},
});
const rules = await group.getRules();
if (Object.keys(rules).length == 0) {
await GroupMember_1.GroupMember.destroy({
where: { id: { [sequelize_1.Op.in]: existingMemberships.map((gm) => gm.id) } },
});
return response;
}
else {
const { where, include } = await group._buildGroupMemberQueryParts(rules, group.matchType);
// and includes this record
//@ts-ignore
if (!where[sequelize_1.Op.and])
where[sequelize_1.Op.and] = [];
//@ts-ignore
where[sequelize_1.Op.and].push({ id: { [sequelize_1.Op.in]: records.map((p) => p.id) } });
const matchedRecords = await RecordMultipleAssociationShim_1.RecordMultipleAssociationShim.findAll({
attributes: ["id"],
where,
include,
});
const bulkCreateRecordIds = [];
const bulkDestroyRecordIds = [];
for (const record of records) {
const belongs = matchedRecords.find((p) => p.id === record.id)
? true
: false;
response[record.id] = belongs;
const existingMembership = existingMemberships.find((gm) => gm.recordId === record.id);
if (belongs && !existingMembership) {
bulkCreateRecordIds.push(record.id);
}
if (!belongs && existingMembership) {
bulkDestroyRecordIds.push(record.id);
}
}
while (bulkCreateRecordIds.length > 0) {
await GroupMember_1.GroupMember.bulkCreate(bulkCreateRecordIds
.splice(0, actionhero_1.config.batchSize.internalWrite)
.map((recordId) => {
return { recordId, groupId: group.id };
}));
}
while (bulkDestroyRecordIds.length > 0) {
await GroupMember_1.GroupMember.destroy({
where: {
recordId: {
[sequelize_1.Op.in]: bulkDestroyRecordIds.splice(0, actionhero_1.config.batchSize.internalWrite),
},
groupId: group.id,
},
});
}
return response;
}
}
GroupOps.updateRecordsMembership = updateRecordsMembership;
/**
* Count the members that would be in this group
*/
async function countPotentialMembers(group, rules, matchType = group.matchType) {
if (!rules)
rules = await group.getRules();
const { where, include } = await group._buildGroupMemberQueryParts(rules, matchType);
// we use RecordMultipleAssociationShim as a shim model which has extra associations
return RecordMultipleAssociationShim_1.RecordMultipleAssociationShim.count({
distinct: true,
where,
include,
});
}
GroupOps.countPotentialMembers = countPotentialMembers;
/**
* Take the rules for the group and check how many group members there would have been both individually for each single rule, and then by building up the query step-by-step
*/
async function countComponentMembersFromRules(group, rules) {
const componentCounts = [];
const funnelCounts = [];
let funnelStep = 0;
if (!rules)
rules = await group.getRules();
for (const i in rules) {
const localRule = rules[i];
componentCounts[i] = await group.countPotentialMembers([localRule]);
const funnelRules = [];
let j = 0;
while (j <= funnelStep) {
funnelRules.push(rules[j]);
j++;
}
funnelCounts[funnelStep] = await group.countPotentialMembers(funnelRules);
funnelStep++;
}
return { componentCounts, funnelCounts };
}
GroupOps.countComponentMembersFromRules = countComponentMembersFromRules;
/**
* Part 1 of the Group Run
* Add New Members
*/
async function runAddGroupMembers(group, run, limit = 1000, offset = 0, highWaterMark = null, destinationId) {
let records;
const rules = await group.getRules();
// if there are no group rules, there's nothing to do
if (Object.keys(rules).length === 0) {
return { groupMembersCount: 0, nextHighWaterMark: 0, nextOffset: 0 };
}
const { where, include } = await group._buildGroupMemberQueryParts(rules, group.matchType, "ready");
where.createdAt = { [sequelize_1.Op.and]: [{ [sequelize_1.Op.lt]: run.createdAt }] };
if (highWaterMark) {
//@ts-ignore
where.createdAt[sequelize_1.Op.and].push({ [sequelize_1.Op.gte]: highWaterMark });
}
records = await RecordMultipleAssociationShim_1.RecordMultipleAssociationShim.findAll({
attributes: ["id", "createdAt"],
where,
include,
limit,
offset,
order: [["createdAt", "asc"]],
subQuery: false,
});
let nextHighWaterMark = 0;
if (records.length > 0) {
nextHighWaterMark = records.reverse()[0].createdAt.getTime() + 1;
}
let nextOffset = 0;
if (records.length > 1 &&
records[0].createdAt.getTime() ===
records.reverse()[0].createdAt.getTime()) {
nextOffset = offset + records.length;
nextHighWaterMark--;
}
const groupMembers = await GroupMember_1.GroupMember.findAll({
where: {
recordId: { [sequelize_1.Op.in]: records.map((p) => p.id) },
groupId: group.id,
},
});
const existingGroupMemberRecordIds = groupMembers.map((member) => member.recordId);
const recordsNeedingGroupMembership = destinationId
? records
: records.filter((p) => !existingGroupMemberRecordIds.includes(p.id));
await updateRecords(recordsNeedingGroupMembership.map((p) => p.id), "run", run.id, destinationId);
if (records.length > 0) {
await GroupMember_1.GroupMember.update({ updatedAt: new Date(), removedAt: null }, {
where: {
recordId: { [sequelize_1.Op.in]: records.map((p) => p.id) },
groupId: group.id,
},
});
}
await group.update({ calculatedAt: new Date() });
return {
groupMembersCount: records.length,
nextHighWaterMark,
nextOffset,
};
}
GroupOps.runAddGroupMembers = runAddGroupMembers;
/**
* Part 2 of the Group Run
* Remove Members
*/
async function runRemoveGroupMembers(group, run, limit = 1000, destinationId) {
const groupMembersToRemove = await GroupMember_1.GroupMember.findAll({
where: {
groupId: group.id,
updatedAt: { [sequelize_1.Op.lt]: run.createdAt },
createdAt: { [sequelize_1.Op.lt]: run.createdAt },
removedAt: null,
},
limit,
});
// The offset and order can be ignored as we will keep running this query until the set of results becomes 0.
await updateRecords(groupMembersToRemove.map((member) => member.recordId), "run", run.id, destinationId);
const now = new Date();
await GroupMember_1.GroupMember.update({ removedAt: now }, {
where: {
id: {
[sequelize_1.Op.in]: groupMembersToRemove.map((member) => member.id),
},
},
});
await group.update({ calculatedAt: new Date() });
return groupMembersToRemove.length;
}
GroupOps.runRemoveGroupMembers = runRemoveGroupMembers;
/**
* Part 3 for the Group Run
* Check if there was anyone we should have removed from a previous Run that was stopped for this Run
*/
async function removePreviousRunGroupMembers(group, run, limit = 100) {
const groupMembersToRemove = await GroupMember_1.GroupMember.findAll({
where: {
groupId: group.id,
removedAt: { [sequelize_1.Op.lte]: run.createdAt },
},
limit,
});
await updateRecords(groupMembersToRemove.map((member) => member.recordId), "run", run.id);
const now = new Date();
await GroupMember_1.GroupMember.update({ removedAt: now }, {
where: {
id: {
[sequelize_1.Op.in]: groupMembersToRemove.map((member) => member.id),
},
},
});
return groupMembersToRemove.length;
}
GroupOps.removePreviousRunGroupMembers = removePreviousRunGroupMembers;
/**
* Determine if these Group Rules are equal
*/
function rulesAreEqual(oldRules, newRules) {
var _a, _b;
if (oldRules.length !== newRules.length)
return false;
function nullish(value) {
if (value === null)
return null;
if (value === undefined)
return null;
if (value === "null")
return null;
return value.toString();
}
for (const i in oldRules) {
const A = oldRules[i];
const B = newRules[i];
if (A.key !== B.key)
return false;
if (((_a = A.operation) === null || _a === void 0 ? void 0 : _a.op) !== ((_b = B.operation) === null || _b === void 0 ? void 0 : _b.op))
return false;
if (nullish(A.match) !== nullish(B.match))
return false;
if (nullish(A.relativeMatchNumber) !== nullish(B.relativeMatchNumber)) {
return false;
}
if (nullish(A.relativeMatchUnit) !== nullish(B.relativeMatchUnit)) {
return false;
}
if (nullish(A.relativeMatchDirection) !== nullish(B.relativeMatchDirection)) {
return false;
}
}
return true;
}
GroupOps.rulesAreEqual = rulesAreEqual;
/**
* Get a list of all groups and when the newest member was added
*/
async function newestGroupMembers(limit = 5) {
const newGroupMembers = await GroupMember_1.GroupMember.findAll({
attributes: [
"groupId",
[sequelize_1.default.fn("max", sequelize_1.default.col("createdAt")), "newestMemberAdded"],
],
group: ["groupId"],
order: [[sequelize_1.default.col("newestMemberAdded"), "desc"]],
limit: limit,
});
const groupIds = newGroupMembers.map((mem) => mem.groupId);
let groups = await Group_1.Group.findAll();
groups = groups
.sort((a, b) => {
if (groupIds.indexOf(a.id) < 0)
return 1;
if (groupIds.indexOf(b.id) < 0)
return -1;
return groupIds.indexOf(a.id) - groupIds.indexOf(b.id);
})
.slice(0, limit);
const newestMembersAdded = {};
newGroupMembers.forEach((g) => {
// @ts-ignore
const value = g.getDataValue("newestMemberAdded"); // this may be a string if SQLite is used
newestMembersAdded[g.groupId] =
value instanceof Date ? value.getTime() : new Date(value).getTime();
});
return {
groups,
newestMembersAdded: newestMembersAdded,
};
}
GroupOps.newestGroupMembers = newestGroupMembers;
})(GroupOps = exports.GroupOps || (exports.GroupOps = {}));