// @ts-check
/**
* @typedef {Object} EntryObject
* @property {Buffer} UID - Entry unique identifier
* @property {Buffer} UIDBelongsTo - Parent object identifier
* @property {Buffer} UIDList - List identifier
* @property {Buffer} UIDEntry - Entry identifier
* @property {string} Type - Object type
* @property {Object} Data - Entry data
*/
import {query,transaction, HEX2uuid} from '@commtool/sql-query'
import {filterObjects, matchObject} from '../../utils/objectfilter/filters/index.js'
import { publishEvent } from '../../utils/events.js'
import { addUpdateList } from '../../server.ws.js'
import { errorLoggerUpdate } from '../../utils/requestLogger.js'
/**
* Helper function to ensure UUIDs are converted to strings
* @param {string|Buffer} uuid - UUID that might be Buffer or string
* @returns {string} - UUID as string
*/
function toStringUuid(uuid) {
if (Buffer.isBuffer(uuid)) {
const result = HEX2uuid(uuid);
return typeof result === 'string' ? result : '';
}
return uuid || '';
}
// checkEntries is checking all entries of this object, if they are still matching the criteria for there filters
// if not it updates the dlis accordingly and sends the appropriate add maessages
// ObjectUID can be a single object UID or an array of object UIDs
/**
* Retrieves all filters and related data for objects that need to be checked for dynamic list membership.
* This function queries the database to get filter information for objects and their associated lists.
*
* @async
* @function getObjectFilters
* @param {Buffer|Buffer[]} ObjectUID - Single object UID or array of object UIDs to get filters for
* @returns {Promise<Array<any>|undefined>} Array of filter objects with complete filter and object information
*/
export const getObjectFilters=async (ObjectUID)=>
{
try {
let myObjectFilters
if(Array.isArray(ObjectUID))
{
myObjectFilters=await query( `SELECT DISTINCT ObjectBase.UID,ObjectBase.UIDBelongsTo, Main.Type, Member.Data, Main.Data AS ExtraData,MainBase.Data AS MainBaseData,
Main.dindex,Filter.UID AS UIDFilter,Filter.Data AS FilterData,Filter.Type AS FilterType, UNIX_TIMESTAMP(Filter.validFrom) AS FilterFrom,
ListLinks.UIDTarget AS UIDList, Entries.UID AS UIDEntry
FROM ObjectBase INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','guest','job','extern'))
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (MainBase.UID=Member.UID)
INNER JOIN ObjectBase AS Entries ON (Entries.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Entries.Type='entry')
INNER JOIN Links AS ListLinks ON (ListLinks.UID=Entries.UID AND ListLinks.Type IN ('member','memberA','member0'))
INNER JOIN Links AS FilterLinks ON (FilterLinks.UIDTarget=ListLinks.UIDTarget AND FilterLinks.Type='list')
INNER JOIN ObjectBase AS Filter ON (Filter.UID=FilterLinks.UID AND Filter.Type IN ('include','exclude','intersect'))
WHERE ObjectBase.UID IN (?) AND ObjectBase.Type IN ('person','guest','extern','job','entry')
ORDER BY UIDList,Entries.UID
`,[ObjectUID],{cast:['json']} )
}
else
{
myObjectFilters=await query( `SELECT DISTINCT ObjectBase.UID,ObjectBase.UIDBelongsTo, Main.Type, Member.Data, Main.Data AS ExtraData,MainBase.Data AS MainBaseData,
Main.dindex,Filter.UID AS UIDFilter,Filter.Data AS FilterData,Filter.Type AS FilterType, UNIX_TIMESTAMP(Filter.validFrom) AS FilterFrom,
ListLinks.UIDTarget AS UIDList, Entries.UID AS UIDEntry
FROM ObjectBase INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','guest','job','extern'))
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (MainBase.UID=Member.UID)
INNER JOIN ObjectBase AS Entries ON (Entries.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Entries.Type='entry')
INNER JOIN Links AS ListLinks ON (ListLinks.UID=Entries.UID AND ListLinks.Type IN ('member','memberA','member0'))
INNER JOIN Links AS FilterLinks ON (FilterLinks.UIDTarget=ListLinks.UIDTarget AND FilterLinks.Type='list')
INNER JOIN ObjectBase AS Filter ON (Filter.UID=FilterLinks.UID AND Filter.Type IN ('include','exclude','intersect'))
WHERE ObjectBase.UID =? AND ObjectBase.Type IN ('person','guest','extern','job','entry')
ORDER BY UIDList,Entries.UID
`,[ObjectUID],{
cast:['json'],
log:false
} )
}
return myObjectFilters
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Processes a single filter entry and groups it under the appropriate entry key in the ObjectFilter.
* This function organizes filter data by entry UID, creating or updating filter collections
* for include, exclude, and intersect filters, along with membership information.
*
* @async
* @function processGroupEntry
* @param {Record<string, any>} ObjectFilter - The object to store grouped filter data, keyed by entry UID
* @param {any} of - Filter object containing entry data, filter information, and list details
* @throws Will log an error if any operation fails during execution.
*/
export const processGroupEntry=async(ObjectFilter,of)=>
{
try {
// UID key for this entry (we convert it into base64 from buffer for easier comparison)
const key=of.UIDEntry.toString('base64')
// store the relevant data for the later objetc filtereing over all filters
const filtering={
Data: {
Data: of.Data,
ExtraData :of.ExtraData,
MainBaseData: of.MainBaseData,
Type: of.Type
},
Filter: of.FilterData,
UIDFilter: of.UIDFilter,
timestamp: of.FilterFrom
}
// if not yet have an UID entry in the ObjectFilter, we are creating a new one
if(!ObjectFilter[key])
{
// retrieve the membership links for this entry
const mLinks= await query(`SELECT Links.Type FROM Links WHERE UID=? AND Links.UIDTarget=? AND Links.Type IN ('member','memberA','member0')`,
[of.UIDEntry,of.UIDList])
// form the object to be added to ObjectFilter
/** @type {any} */
const newUID = {
Filter: {include:[], exclude:[], intersect:[]},
UIDList: of.UIDList,
MemberType: mLinks.some(/** @param {any} l */ l=>l.Type==='member0') ? 'member0': mLinks.find(/** @param {any} l */ l=>l.Type==='member' || l.Type==='memberA').Type ,
UIDEntry: of.UIDEntry,
UIDBelongsTo: of.UIDBelongsTo,
StillMember: mLinks.some(/** @param {any} l */ l=>l.Type==='member' || l.Type==='memberA')
}
// save this filter in the right filter category
newUID.Filter[of.FilterType].push(filtering)
// add the newly created oblect under the entry key in the ObjectFilter object
ObjectFilter[key]=newUID
}
else
{
// just push the filter in the right category array
/** @type {any} */ (ObjectFilter[key]).Filter[of.FilterType].push(filtering)
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Groups filter data by entry UID and organizes filters into include/exclude/intersect categories.
* This function processes raw filter data from the database and structures it for efficient
* filtering operations, creating an object keyed by entry UIDs with complete filter information.
*
* @async
* @function groupEntriesFilter
* @param {Array<any>} myObjectFilters - Array of raw filter objects from the database query
* @returns {Promise<Record<string, any>|undefined>} Object keyed by entry UIDs containing organized filter data
* @throws Will log an error if any operation fails during execution.
*/
export const groupEntriesFilter=async (myObjectFilters)=>
{
// forms from the retrieved lines an object with the entry UID as primary key
// and stores in it the relevant data to be filtered and all filters with there list for this entry
// this can be later easy processed
try {
const ObjectFilter={}
// group by entries.
for (const of of myObjectFilters)
await processGroupEntry(ObjectFilter,of)
return ObjectFilter
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Removes the exclusion link for an entry and publishes add events to notify about the re-inclusion.
* This function is called when an entry that was previously excluded from a list should now be included.
*
* @async
* @function unExclude
* @param {any} entry - Entry object containing UIDEntry, UIDList, UIDBelongsTo
* @param {string} UIDOrga - The unique identifier of the organization (required)
* @throws Will log an error if any operation fails during execution.
*/
export const unExclude=async (entry, UIDOrga)=>
{
try {
// we have to 'un-exclude'
await query(`DELETE FROM Links WHERE UID=? AND UIDTarget=? AND Type ='member0'`,[entry.UIDEntry,entry.UIDList])
// we have to publish, that we have it again here (only, if it was not there before)
// Ensure UIDOrga is string for publishEvent
const listId = HEX2uuid(entry.UIDList);
const belongsToId = HEX2uuid(entry.UIDBelongsTo);
const entryId = HEX2uuid(entry.UIDEntry);
/** @type {any} */ (publishEvent)(`/add/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
/** @type {any} */ (publishEvent)(`/add/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryId] })
addUpdateList(entry.UIDList)
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Creates an exclusion link for an entry and publishes remove events to notify about the exclusion.
* This function is called when an entry should be excluded from a list due to filter matching.
*
* @async
* @function exclude
* @param {any} entry - Entry object containing UIDEntry, UIDList, UIDBelongsTo
* @param {string} UIDOrga - The unique identifier of the organization (required)
* @throws Will log an error if any operation fails during execution.
*/
export const exclude=async(entry, UIDOrga)=>
{
try {
// we have to 'exclude'
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'member0',?)`,
[entry.UIDEntry,entry.UIDList],
)
// we have to publish, that we have removed it
// Ensure UIDOrga is string for publishEvent
const listId = HEX2uuid(entry.UIDList);
const belongsToId = HEX2uuid(entry.UIDBelongsTo);
const entryId = HEX2uuid(entry.UIDEntry);
/** @type {any} */ (publishEvent)(`/remove/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
/** @type {any} */ (publishEvent)(`/remove/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryId] })
addUpdateList(entry.UIDList)
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Checks a single entry against its filters and performs necessary add/remove operations.
* This function evaluates whether an entry should be included or excluded from a list based on
* the current filter state and publishes appropriate events for membership changes.
*
* @async
* @function checkEntry
* @param {any} entry - Entry object with filter information and membership data
* @param {string} UIDOrga - The unique identifier of the organization (required)
* @throws Will log an error if any operation fails during execution.
*/
export const checkEntry= async (entry, UIDOrga)=>
{
try {
let remains=false
// we are first checking, if the object ist still matching at least one of the include filters of the dlist
if(entry.StillMember!==null)
{
for(const Filter of entry.Filter.include)
{
if(matchObject(Filter.Data,Filter.Filter))
{
remains=true
break
}
}
}
if(!remains)
{
// if it is no more includes, we remove the entry and the relevant links
await transaction(/** @param {any} connection */ (connection)=>
{
connection.query(`DELETE ObjectBase,Links FROM ObjectBase INNER JOIN Links ON (Links.UID=ObjectBase.UID)
WHERE ObjectBase.UID=?`,
[entry.UIDEntry])
},
{
beforeTransaction: /** @type {function(any): Promise<any>} */ async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
})
// we are publishing the removal as well if it was member (did not have a member0 link)
if(entry.MemberType!=='member0')
{
// Ensure UIDOrga is string for publishEvent
const listId = HEX2uuid(entry.UIDList);
const belongsToId = HEX2uuid(entry.UIDBelongsTo);
const entryUid = HEX2uuid(entry.UID);
/** @type {any} */ (publishEvent)(`/remove/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
/** @type {any} */ (publishEvent)(`/remove/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryUid] })
// and update the list in the UID
addUpdateList(entry.UIDList)
}
}
if(remains)
{
// if the object is still included, we have to check the includers and the excluders
let excludes=false
// check if the excluders are still valid
for(const Filter of entry.Filter.exclude)
{
if(Filter.Data && matchObject(Filter.Data,Filter.Filter))
{
excludes=true
// make sure, that we have the dynamic link for this entry to the exclude filter
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'dynamic',?)`,[Filter.UIDFilter,entry.UIDEntry])
break
}
}
for(const Filter of entry.Filter.intersect)
{
if(Filter.Data && !matchObject(Filter.Data,Filter.Filter))
{
excludes=true
// make sure, that we have the dynamic link for this ebtry to the exclude filter
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'dynamic',?)`,[Filter.UIDFilter,entry.UIDEntry])
break
}
}
// if it is no more excluded and still masked by a member0 link, we have to unexclude
if(excludes===false && entry.MemberType==='member0')
{
await unExclude(entry, UIDOrga)
}
else if((entry.Filter.exclude.length>0 || entry.Filter.intersect.length>0) && entry.MemberType!=='member0' && excludes)
{
await exclude(entry, UIDOrga)
}
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Checks all entries for the given objects against their filters and performs necessary cleanup.
* This function processes multiple objects, retrieves their filters, and checks each entry
* to ensure they still match the current filter criteria, removing entries that no longer qualify.
*
* @async
* @function checkEntries
* @param {Buffer|Buffer[]} ObjectUID - Single object UID or array of object UIDs to check
* @param {string} UIDOrga - The organization UUID for multi-tenant scoping (must be string)
* @returns {Promise<void>}
* @throws Will log an error if any operation fails during execution.
*/
export const checkEntries=async (ObjectUID, UIDOrga)=>
{
try {
// Type validation
if (typeof UIDOrga !== 'string') {
throw new TypeError(`UIDOrga must be a string, got ${typeof UIDOrga}`);
}
// Check if ObjectUID is a Buffer or an array of Buffers
if (!Buffer.isBuffer(ObjectUID) && !Array.isArray(ObjectUID)) {
throw new TypeError(`ObjectUID must be a Buffer or an array of Buffers, got ${typeof ObjectUID}`);
}
// check all filters for the entries of the supplied objects.
// if they are not longer valid, remove there entries from the list
// get Object Data and Filters
const myObjectFilters=await getObjectFilters(ObjectUID)
// Add null safety check
if (!myObjectFilters) {
return;
}
const ObjectFilter= await groupEntriesFilter(myObjectFilters)
// Add null safety check for ObjectFilter
if (!ObjectFilter) {
return;
}
// now we go through all entries and check, if the filters are keeping the objects in the list
for (const [uid,entry] of Object.entries(ObjectFilter))
{
await checkEntry(entry, UIDOrga)
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}