// @ts-check
/**
* @typedef {Object} DatabaseObject
* @property {Buffer} UID - The unique identifier as a Buffer
* @property {Buffer} UIDBelongsTo - The parent object identifier as a Buffer
* @property {string} Type - The object type
* @property {string} [Title] - Optional title for the object
*/
/**
* @typedef {Object} FilterObject
* @property {Buffer} UID - The filter unique identifier
* @property {string} Type - Filter type ('include', 'exclude', 'intersect')
* @property {Object} Data - Filter configuration data
*/
import {query, HEX2uuid, UUID2hex} from '@commtool/sql-query'
import {filterObjects} from '../../utils/objectfilter/filters/index.js'
import { publishEvent } from '../../utils/events.js'
import { addUpdateList } from '../../server.ws.js'
import { errorLoggerUpdate } from '../../utils/requestLogger.js'
/*
This module handles the update of lists, when a data object is changed
The function machcObjectsLists is called by treeAdd or treeMigrate
*/
/**
* Reduces duplicate objects to unique entries based on UIDBelongsTo comparison
* @param {any[]} objects - Array of objects to reduce
* @returns {any[]|undefined} - Array with duplicate UIDBelongsTo entries removed
*/
export const reduceObjects=(objects)=>
{
try
{
// should reduce the duplicate persons coming from avatars to one
return objects.reduce(/** @param {any} result */ /** @param {any} current */ (result,current)=>(result.find(/** @param {any} el */ el=>el.UIDBelongsTo.equals(current.UIDBelongsTo))?
result: [...result,current]),[])
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Removes objects based on exclude filter criteria
* @param {any[]} myDEObjects - Array of objects to potentially remove
* @param {any} exclude - Exclude filter object
* @param {any} listUID - List identifier
* @returns {Promise<any[]|undefined>} - Array of removed objects
*/
const removeObjects=async ( myDEObjects,exclude,listUID )=>
{
try {
let removed=[]
let added=[]
// link the entries via dynamik link to the filter
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES (?,'dynamic',?)`,
myDEObjects.map(/** @param {any} o */ o=>[o.UID,exclude.UID]),{batch:true})
// generate the missing membership link
const existingMember0= await query(`SELECT Links.UID FROM Links
WHERE Links.UID IN (?) AND Links.Type='member0' AND Links.UIDTarget=?`,
[myDEObjects.map(/** @param {any} o */ o=>o.UID), listUID])
removed=myDEObjects.filter(/** @param {any} el */ el=>!existingMember0.some(/** @param {any} e */ e=>e.UID.equals(el.UID)))
if(removed.length>0)
{
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget)
VALUES (?,'member0',?)`,
removed.map(/** @param {any} o */ o=>([o.UID,listUID])),{batch:true})
}
return removed
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Matches objects against excluder lists and handles membership updates
* @param {any[]} Objects - Array of objects to process
* @param {string|Buffer} listUID - List identifier
* @param {string} UIDOrga - Organization identifier
* @returns {Promise<void>}
*/
export const matchExcludersList=async (Objects,listUID, UIDOrga)=>
{
try
{
listUID=UUID2hex(listUID)
// deduct the entries, which have to be excluded by excluders
let addResult=reduceObjects(Objects.filter(/** @param {any} ad */ ad=>ad.new))
const excluders=await query(`
SELECT ObjectBase.Type,ObjectBase.UID,ObjectBase.Data
FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type = 'list')
WHERE Links.UIDTarget=? AND ObjectBase.Type IN ('exclude','intersect')`,
[listUID],
)
// get the includers/intersects for this list
/** @type {any[]} */
let toBeRemoved=[]
if(excluders.length>0 && Objects.length>0)
{
// filter for newly created entries
for(const exclude of excluders)
{
let filter
try{
filter=JSON.parse(exclude.Data)
}
catch(e)
{
console.log(e, filter.Data)
}
const myDEObjects=filterObjects(Objects,filter)
if(exclude.Type==='exclude' && myDEObjects.length>0)
{
const removeResult = await removeObjects( myDEObjects,exclude,listUID );
if (removeResult) {
toBeRemoved=[...toBeRemoved, ...removeResult];
}
if (addResult) {
addResult= addResult.filter(/** @param {any} ob */ ob=>(!myDEObjects.some(/** @param {any} fo */ fo=>ob.UIDBelongsTo.equals(fo.UIDBelongsTo))));
}
}
if(exclude.Type==='intersect')
{
const invertedFiltered=Objects.filter(/** @param {any} no */ (no)=>(myDEObjects.find(/** @param {any} dObj */ dObj=>(dObj.UID.compare(no.UID)===0))===undefined))
if(invertedFiltered.length>0)
{
const removeResult = await removeObjects( invertedFiltered,exclude,listUID );
if (removeResult) {
toBeRemoved=[...toBeRemoved,...removeResult];
}
if (addResult) {
addResult= addResult.filter(/** @param {any} ob */ ob=>(!invertedFiltered.some(/** @param {any} fo */ fo=>ob.UIDBelongsTo.equals(fo.UIDBelongsTo))));
}
}
}
}
}
// send the events for everything which is now added after all filters
if(addResult && addResult.length>0)
{
publishEvent(`/add/dlist/person/${HEX2uuid(listUID)}`, { organization: UIDOrga, data: addResult.map(/** @param {any} added */ added=>HEX2uuid(added.UIDBelongsTo)) })
publishEvent(`/add/dlist/entry/${HEX2uuid(listUID)}`, { organization: UIDOrga, data: addResult.map(/** @param {any} added */ added=>HEX2uuid(added.UID)) })
}
const reducedToBeRemoved = reduceObjects(toBeRemoved);
toBeRemoved = reducedToBeRemoved || [];
if(toBeRemoved.length>0)
{
publishEvent(`/remove/dlist/person/${HEX2uuid(listUID)}`, { organization: UIDOrga, data: toBeRemoved.map(/** @param {any} removed */ removed=>HEX2uuid(removed.UIDBelongsTo)) })
publishEvent(`/remove/dlist/entry/${HEX2uuid(listUID)}`, { organization: UIDOrga, data: toBeRemoved.map(/** @param {any} removed */ removed=>HEX2uuid(removed.UID)) })
}
if((addResult && addResult.length>0) || toBeRemoved.length >0 )
addUpdateList(listUID)
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Retrieves all filters for the supplied sources
* @param {Buffer[]} sources - Array of source identifiers
* @returns {Promise<any[]|undefined>} - Array of filter objects
*/
export const getFilters=async (sources)=>
{
//reads all filters for the supplied sources
try {
const filters=await query(
`SELECT DISTINCT ObjectBase.Type,ObjectBase.UID,ObjectBase.Data, Links.UIDTarget AS listUID, Source.Type
AS SourceType FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type = 'list')
INNER JOIN ObjectBase AS Source ON (Source.UID = ObjectBase.UIDBelongsTo AND Source.Type IN ('group','list','event'))
WHERE ObjectBase.UIDBelongsTo IN (?) AND ObjectBase.Type IN ('include','exclude','intersect')
`,
[sources],
{
log:true,
cast:['json'],
group: /** @type {function(any, any): any} */ (result, current)=>
{
const index=result.findIndex(/** @param {any} el */ el=>el.UID.equals(current.UID))
if(index>=0)
{
return result
}
// the row will be addded to the result set
return [...result,current]
}
}
)
return filters
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Retrieves objects based on the provided ObjectUID(s).
*
* This function queries the database to fetch objects and their associated data.
* It supports both single ObjectUID and an array of ObjectUIDs. The returned data
* includes details such as UID, type, associated member data, extra data, main base data,
* title, and validity timestamps.
*
* @async
* @param {Buffer|Buffer[]} ObjectUID - A single UID or an array of UIDs representing the objects to retrieve.
* @returns {Promise<Object[]|undefined>} A promise that resolves to an array of objects containing:
* - `Main.UID` (string): The UID of the main object ('person','guest','extern','job').
* - `ObjectBase.UID` (string): The UID of the base object, this will be the UID of entry, if a list is the data source.
* - `Main.Type` (string): The type of the main object (e.g., 'person', 'job', 'guest', 'extern').
* - `Member.Data` (Object): Data associated with the member.
* - `Main.Data` (Object): Extra data associated with the main object.
* - `MainBase.Data` (Object): Data from the main base object.
* - `ObjectBase.UIDBelongsTo` (string): UID of the Member entry, this Object belongs to.
* - `Main.dindex` (number): Index value associated with the main object.
* - `ObjectBase.Title` (string): Title of the base object.
* - `ObjectBase.validFrom` (number): UNIX timestamp indicating the validity start date of the base object.
*
* @throws {Error} Logs and throws an error if the query fails.
*/
export const getObjects=async(ObjectUID)=>
{
try {
// const asOf= timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
let myObjects
if(Array.isArray(ObjectUID))
{
myObjects=await query( `SELECT DISTINCT Main.UID ,ObjectBase.UID AS UIDbase, Main.Type, Member.Data, Main.Data AS ExtraData, MainBase.Data AS MainBaseData,
ObjectBase.UIDBelongsTo, Main.dindex,ObjectBase.Title, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','job','guest','extern'))
INNER JOIN Member ON (Member.UID=Main.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (MainBase.UID=Member.UID)
WHERE ObjectBase.UID IN (?) AND ObjectBase.Type IN ('person','guest','extern','job','entry')`,
[ObjectUID],{cast:['json']} )
}
else
{
myObjects=await query( `SELECT Main.UID, ObjectBase.UID AS UIDbase, Main.Type, Member.Data, Main.Data AS ExtraData,
MainBase.Data AS MainBaseData,
ObjectBase.UIDBelongsTo, Main.dindex,ObjectBase.Title, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','job','guest','extern'))
INNER JOIN Member ON (Member.UID=Main.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (MainBase.UID=Member.UID)
WHERE ObjectBase.UID =? AND ObjectBase.Type IN ('person','guest','extern','job','entry')`,
[ObjectUID],{cast:['json']} )
}
return myObjects
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Processes the inclusion filter and updates the database with the filtered objects.
* This function handles the creation of new entries, linking entries to filters, and
* processing exclude and intersect filters for newly created entries.
*
* @async
* @function processIncludeFilter
* @param {Object} include - The inclusion filter object containing filter data and source type.
* @param {Object[]} myObjects - Array of objects to be filtered and processed.
* @returns {Promise<void>} Resolves when the processing is complete.
* @throws {Error} Logs and throws an error if any issue occurs during processing.
*
* @example
* const include = {
* Data: { filter criteria here },
* SourceType: 'group',
* listUID: 'someUID',
* UID: 'filterUID'
* };
* const myObjects = [/ array of objects as retrieved by getObjects/];
* await processIncludeFilter(include, myObjects);
*/
/**
* Processes include filter and creates new entries based on filter criteria
* @param {any} include - Include filter object containing Data, SourceType, listUID, UID properties
* @param {any[]} myObjects - Array of objects to filter
* @param {string} UIDOrga - Organization identifier
* @returns {Promise<void>}
*/
export const processIncludeFilter= async (include, myObjects, UIDOrga) =>
{
try
{
const filter=include.Data
const myDObjects=filterObjects(myObjects,filter).map(/** @param {any} o */ o=>({...o}))
const reducedObjects= reduceObjects(myDObjects)
if(reducedObjects && reducedObjects.length>0)
{
{
const toBeCreated=[]
if(include.SourceType==='group')
{
const already=await query(`SELECT ObjectBase.UID,ObjectBase.UIDBelongsTo,ObjectBase.Data,
UNIX_TIMESTAMP(ObjectBase.validUntil) AS validUntil
FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type IN ('member','memberA'))
WHERE ObjectBase.Type='entry' AND Links.UIDTarget=? AND ObjectBase.UIDBelongsTo IN (?) `,
[include.listUID,reducedObjects.map(o=>o.UIDBelongsTo)],
)
for(const fo of reducedObjects)
{
const entry=already.find(at=>(fo.UIDBelongsTo.equals(at.UIDBelongsTo)))
if(entry)
{
fo.UID= entry.UID
}
else
{
//fo.UIDBelongsTo=fo.UID
const [UID]=await query(`SELECT UIDV1() AS UID`, [])
fo.UID=UID.UID
toBeCreated.push(fo)
myDObjects.filter(/** @param {any} el */ el=>el.UIDBelongsTo.equals(fo.UIDBelongsTo)).forEach(/** @param {any} el */ el=>el.new=true)
}
}
}
else if(include.SourceType==='list')
{
// we have to use the original base UID of the entry object as we want to share entries with the original list
//this allows to have the same entry parameters visible/consolidated from difefrent lists in one dlist
reducedObjects.forEach(/** @param {any} fo */ fo=>{
fo.UID=fo.UIDbase
myDObjects.filter(/** @param {any} el */ el=>el.UIDBelongsTo.equals(fo.UIDBelongsTo)).forEach(/** @param {any} el */ el=>el.new=true)
}
)
}
if(toBeCreated.length)
{
// generate List entries for objects which have not yet an entry (empty Display, Data, SortName, as they will come from the real entry, when listed )
for(const tbc of toBeCreated)
{
await query(`INSERT IGNORE INTO ObjectBase (UID,Type,UIDBelongsTo,Title,Data) VALUES (?,'entry',?,?,?)`,
[tbc.UID,tbc.UIDBelongsTo,tbc.Title,JSON.stringify({})]
)
}
}
// link the entries via dynamik link to the filter
for(const tbl of reducedObjects)
{
await query(`
INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES (?,'dynamic',?)`,
[tbl.UID,include.UID]
)
// generate the membership link
const memberType= include.SourceType==='group' ? 'memberA' : 'member'
await query(`
INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES (?,?,?)`,
[tbl.UID,memberType,include.listUID]
)
}
}
// publish the add event in matchExcludersList!!!
}
// now process the exclude and intersect filters for the newly created entries
await matchExcludersList(myDObjects,include.listUID, UIDOrga)
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Matches objects against list filters and updates dynamic lists accordingly
* @param {Buffer|Buffer[]} ObjectUID - Single object UID or array of object UIDs to process
* @param {Buffer|Buffer[]} sources - Source list/group UIDs to check filters for
* @param {string} UIDOrga - Organization UUID for multi-tenant scoping
* @returns {Promise<void>}
*/
export const matchObjectsLists=async (ObjectUID,sources, UIDOrga)=>
{
try
{
// Type validation
if (typeof UIDOrga !== 'string') {
throw new TypeError(`UIDOrga must be a string, got ${typeof UIDOrga}`);
}
if (!Buffer.isBuffer(ObjectUID) && !Array.isArray(ObjectUID)) {
throw new TypeError(`ObjectUID must be a Buffer or array, got ${typeof ObjectUID}`);
}
if (!Buffer.isBuffer(sources) && !Array.isArray(sources)) {
throw new TypeError(`sources must be a Buffer or array, got ${typeof sources}`);
}
// check the supplied object(single object ObjectUID)/objects (ObjectUID=Array of object UIDs) against all
// include/exclude/intersect filters of the supplied sources
// updates the relevant dlist's where these filters are pointing to
// match object or an array of objects against all filters of an array of sources and
// get all filters (include, exclude, intersect). They are seletced by the UIDBelongsTo of the sources
if(!sources || sources.length===0)
return
// Ensure sources is an array
const sourceArray = Array.isArray(sources) ? sources : [sources];
const filters=await getFilters(sourceArray);
// return if we do not have filters
if(!filters || !filters.length)
return
// get Object Data
const myObjects= await getObjects(ObjectUID)
// Add null safety for myObjects
if (!myObjects) {
return;
}
// go through all filters, which are includers and add the filtered objects, Filters with 0 results will be eliminated
const includes=[]
for( const include of filters.filter(/** @param {any} f */ f=>(f.Type==='include')))
{
await processIncludeFilter(include, myObjects, UIDOrga)
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}