UNPKG

@grouparoo/core

Version:
358 lines (357 loc) 14.8 kB
"use strict"; 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 = {}));