// @ts-check
/**
* @import {FilterAction,FilterOptions} from './../../types.js'
*/
import {query, transaction, UUID2hex, HEX2uuid}from "@commtool/sql-query"
import { matchObject } from "../../utils/objectfilter/filters/index.js"
import { publishEvent } from "../../utils/events.js"
import { addUpdateList } from "../../server.ws.js"
import { connection } from "@commtool/sql-query"
import { errorLoggerUpdate } from "../../utils/requestLogger.js"
// processes the addition of include/exclude or intersect filter to a dynamic list (dlist)
/**
* Gets filter data and source information for a filter
* @param {Buffer} UIDFilter - The UID of the filter
* @param {FilterAction} action - The filter action type
* @param {any} [connection=null] - Database connection
* @returns {Promise<any[]|undefined>} Filter data with source information, or undefined if error occurs
*/
export const getFilterData= async (UIDFilter,action,connection=null)=>
{
try {
return await query (`
SELECT ObjectBase.Data AS filter, ObjectBase.UIDBelongsTo AS sourceUID, Source.Type AS sourceType
FROM ObjectBase
INNER JOIN ObjectBase AS Source ON (Source.UID=ObjectBase.UIDBelongsTo AND Source.Type IN ('list','group'))
WHERE ObjectBase.UID=? AND ObjectBase.Type=?`,
[UIDFilter,action],{cast:['json'], connection})
}
catch(e)
{
errorLoggerUpdate(e)
return undefined
}
}
/**
* Determines what types of objects to select based on source type and filter
* @param {string} sourceType - The type of source ('list' or 'group')
* @param {Object} filter - The filter configuration object
* @returns {string[]} Array of object types to select
*/
export const getTypes=(sourceType,filter)=>
(
// which type of data do we have to select?
// for list sources it is 'entry' for other sources it is defined in the filter
sourceType==='list' ? ['entry'] : Object.keys(filter).filter(el=>['person','guest','job','extern'].includes(el))
)
/**
* Gets objects that match include filter criteria
* @param {Object} filter - The filter configuration
* @param {Buffer} sourceUID - Source object UID
* @param {string} sourceType - Source type ('list' or 'group')
* @param {Buffer} target - Target list UID
* @param {string[]} types - Object types to include
* @param {any} connection - Database connection
* @returns {Promise<any[]|undefined>} Filtered objects for inclusion, or undefined if error occurs
*/
export const getIncludeObjects= async(filter,sourceUID,sourceType,target,types, connection)=>
{
try {
// will return the UID of an exsiting entry in this list in UIDentry, will as well provide a newly generated UIDentry for later usage in newEntry
// this function is already filtering the data, while it is streamed from the database (only whne the source is not a list)
return await query(`SELECT ObjectBase.Type, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.UID,
ObjectBase.Data AS ExtraData,Main.Data,MainBase.Data AS MainBaseData,
ObjectBase.Title,Main.Display,Main.SortName,Entry.UID AS UIDEntry, UIDV1() AS newEntry
FROM ObjectBase
INNER JOIN Links
ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA') AND ObjectBase.Type NOT IN ('filter','list','achievement'))
INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (Main.UID=MainBase.UID)
LEFT JOIN (
SELECT Entry.UID, Entry.UIDBelongsTo
FROM ObjectBase AS Entry
INNER JOIN Links AS ELink
ON (Entry.UID=ELink.UID)
AND ELink.UIDTarget=? AND Entry.Type='entry'
) AS Entry ON (Entry.UIDBelongsTo=ObjectBase.UIDBelongsTo)
WHERE Links.UIDTarget=? AND ObjectBase.Type IN (?)
GROUP BY ObjectBase.UID
`,[target,sourceUID, types],
{
cast:['json'],
// filter, when there are filter keys and the source is not a list
filter: (
Object.keys(filter).length && sourceType!=='list'?
/** @param {any} row */ (row)=>(matchObject(row,filter)) :null
),
log: false,
connection
})
}
catch(e)
{
errorLoggerUpdate(e)
return undefined
}
}
/**
* Gets objects that match exclude/intersect filter criteria
* @param {Object} filter - The filter configuration
* @param {FilterAction} action - Filter action ('exclude' or 'intersect')
* @param {Buffer} sourceUID - Source object UID
* @param {string} sourceType - Source type
* @param {Buffer} target - Target list UID
* @param {string[]} types - Object types to process
* @param {any} [connection] - Database connection
* @returns {Promise<any[]|undefined>} Filtered objects for exclusion/intersection, or undefined if error occurs
*/
export const getExcludeObjects= async (filter,action,sourceUID,sourceType,target,types,connection)=>
{
try {
return await query(`SELECT ObjectBase.Type, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.UID,ObjectBase.Data AS ExtraData,Main.Data,MainBase.Data AS MainBaseData,
ObjectBase.Title,Main.Display,Main.SortName,Entry.UID AS UIDEntry, UIDV1() AS newEntry,ObjectBase.Title
FROM ObjectBase
INNER JOIN Links
ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA') AND ObjectBase.Type NOT IN ('filter','list','achievement'))
INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
INNER JOIN ObjectBase AS MainBase ON (Main.UID=MainBase.UID)
INNER JOIN (
SELECT Entry.UID, Entry.UIDBelongsTo
FROM ObjectBase AS Entry
INNER JOIN Links AS ELink
ON (Entry.UID=ELink.UID)
AND ELink.UIDTarget=? AND Entry.Type='entry'
) AS Entry ON (Entry.UIDBelongsTo=ObjectBase.UIDBelongsTo)
WHERE Links.UIDTarget=? AND ObjectBase.Type IN (?)
GROUP BY ObjectBase.UID
`,[target,sourceUID,types],{
cast:['json'],
filter: (
Object.keys(filter).length && sourceType!=='list' ?
action==='exclude' ?
/** @param {any} row */ (row)=>(matchObject(row,filter)) :
/** @param {any} row */ (row)=>(!matchObject(row,filter)) :null
)
,connection
})
}
catch(e)
{
errorLoggerUpdate(e)
return undefined
}
}
/**
* Reduces filtered results to unique entries by UIDBelongsTo
* @param {any[]} filtered - Array of filtered objects
* @returns {any[]} Reduced array with unique entries
*/
export const reduceFiltered=(filtered)=>
(
// make unique UIDBelongsTo
filtered.reduce((result,current)=>(result.find(/** @param {any} el */ el=>el.UIDBelongsTo.equals(current.UIDBelongsTo))
? result: [...result,current]),[])
)
/**
* Creates database links for included objects
* @param {string} sourceType - Source type ('group' or 'list')
* @param {Buffer} UIDFilter - Filter UID
* @param {any[]} filtered - Filtered objects to link
* @param {Buffer} target - Target list UID
* @param {any} execConnection - Database connection
* @returns {Promise<void>}
*/
const linkIncludes=async (sourceType,UIDFilter,filtered,target,execConnection)=>
{
try {
// link the entries via dynamik link to the filter
await query(
`INSERT IGNORE INTO Links (UID,Type,UIDTarget)
VALUES (?,?,?)`,
filtered.map(o=>([o.UID,'dynamic',UIDFilter])),
{batch:true, connection: execConnection})
const memberType= sourceType==='group' ? 'memberA' : 'member'
await query(
`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES (?,?,?)`,
filtered.map(o=>[o.UID,memberType,target]),
{ batch:true,connection:execConnection})
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Processes include filter operations
* @param {Buffer} UIDFilter - Filter UID
* @param {any[]} filtered - Filtered objects to include
* @param {string} sourceType - Source type
* @param {Buffer} target - Target list UID
* @param {string} organization - Organization UUID for events
* @param {FilterOptions} options - Operation options
* @returns {Promise<void>}
*/
export const processInclude= async (UIDFilter,filtered, sourceType,target,organization,{connection,virtual})=>
{
return new Promise(async (fullfill,reject)=>
{
try {
let toBeAdded=[]
// generate entry objetcs, if source is a group, if is a list, reuse the entries
if(sourceType==='group' )
{
// for a group the UIDEntry does not yet exist, if it is not already member
toBeAdded = filtered.filter(o=>o.UIDEntry===null)
// use generted uid's for entry objects, if they are already praesent
filtered.forEach(fo=>{fo.UIDEntry ? fo.UID=fo.UIDEntry : fo.UID=fo.newEntry})
}
else
{
// for a list the
}
// list entries are passed through to the dlist
if(filtered.length)
{
if(!virtual)
{
// generate List entries for objects which have not yet an entry
if(toBeAdded.length>0)
{
await query(`INSERT INTO ObjectBase (UID,Type,UIDBelongsTo,Title) VALUES (?,?,?,?)`,
toBeAdded.map(o=>([o.UID,'entry',o.UIDBelongsTo,o.Title])),
{batch:true,connection}
)
// only publish events, if it is a none virtual add/remove
publishEvent(`/add/dlist/person/${HEX2uuid(target)}`, { organization, data: toBeAdded.map(o=>HEX2uuid(o.UIDBelongsTo)) })
}
publishEvent(`/add/dlist/entry/${HEX2uuid(target)}`, { organization, data: toBeAdded.map(o=>HEX2uuid(o.UID)) })
//matchExcludersList(toBeAdded,target)
await transaction(async (/** @type {any} */ connection)=>
{
await linkIncludes(sourceType,UIDFilter,filtered,target,connection)
})
addUpdateList(target)
}
else
{
// generate List entries for objects which have not yet an entry
if(toBeAdded.length>0)
{
await query(`INSERT INTO ObjectBase (UID,Type,UIDBelongsTo,Title) VALUES (?,?,?,?)`,
toBeAdded.map(o=>([o.UID,'entry',o.UIDBelongsTo,o.Title])),
{batch:true,connection}
)
}
// virtual addFilter within remove/add transaction
await linkIncludes(sourceType,UIDFilter,filtered,target,connection)
}
}
fullfill()
}
catch(e)
{
reject(e)
errorLoggerUpdate(e)
}
})
}
/**
* Creates database links for excluded/intersected objects
* @param {Buffer} UIDFilter - Filter UID
* @param {any[]} filtered - Filtered objects to exclude/intersect
* @param {Buffer} target - Target list UID
* @param {any} connection - Database connection
* @returns {Promise<void>}
*/
const linkExcludeIntersect=async (UIDFilter,filtered,target, connection)=>
{
try {
// link the entries via dynamik link to the filter
await query(
`INSERT INTO Links (UID,Type,UIDTarget)
VALUES (?,'dynamic',?) ON DUPLICATE KEY UPDATE Type='dynamic'`,
filtered.map(o=>[o.UIDEntry,UIDFilter]),
{batch:true,connection})
await query(
`INSERT INTO Links (UID,Type,UIDTarget) VALUES (?,'member0',?)
ON DUPLICATE KEY UPDATE Type=VALUE(Type)`,
filtered.map(o=>[o.UIDEntry,target]),
{batch:true, connection})
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Processes exclude filter operations
* @param {Buffer} UIDFilter - Filter UID
* @param {any[]} filtered - Filtered objects to exclude
* @param {Buffer} target - Target list UID
* @param {string} organization - Organization UUID for events
* @param {FilterOptions} options - Operation options
* @returns {Promise<void>}
*/
export const processExclude=async(UIDFilter,filtered,target,organization,{connection,virtual})=>
{
try {
if(!virtual)
{
await transaction(async(/** @type {any} */ connection)=>
{
linkExcludeIntersect(UIDFilter,filtered,target,connection)
})
publishEvent(`/remove/dlist/person/${HEX2uuid(target)}`, { organization, data: filtered.map(o=>HEX2uuid(o.UIDBelongsTo)) })
publishEvent(`/remove/dlist/entry/${HEX2uuid(target)}`, { organization, data: filtered.map(o=>HEX2uuid(o.UID)) })
addUpdateList(target)
}
else
{
// virtual addFilter within remove/add transaction
await linkExcludeIntersect(UIDFilter,filtered,target,connection)
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Executes a dynamic list filter (include/exclude/intersect) on a target list
* @param {Buffer|string} UIDFilter - The UID of the filter to execute
* @param {Buffer|string} target - The UID of the target dynamic list
* @param {FilterAction} action - The type of filter action
* @param {string} organization - The organization UUID for multi-tenant scoping
* @param {FilterOptions} options - Additional options for the operation
* @returns {Promise<void>}
*/
export const execDListFilter=async (UIDFilter,target,action,organization,{connection,virtual})=>
{
// executes the filter UIDFilter, taking data form its source UIDBelongsTo
// includes/excludes/intersects (defined by action) the filtered data of its source into the target list
// Type validation
UIDFilter = UUID2hex(UIDFilter)
target = UUID2hex(target)
if (typeof organization !== 'string') {
throw new TypeError(`organization must be a string, got ${typeof organization}`);
}
if (!['include', 'exclude', 'intersect'].includes(action)) {
throw new TypeError(`action must be a valid DListFilterAction, got ${action}`);
}
// UIDFilter: the UID of the filter to be acecuted
// target: the UID of the target dlist
// action: the type of the filter (include,exclude,intersect)
// myConnection: a database connection. This has to be set, when you would like to execute the filter in a
// seperate transaction
// get the data of the filter
try {
const filters= await getFilterData(UIDFilter,action)
if(!filters || filters.length===0)
return
const [{filter,sourceUID, sourceType}]= filters
if(filter)
{
// which type of data do we have to select?
// for list sources it is 'entry' for other sources it is defined in the filter
const types= getTypes(sourceType,filter)
let filtered
if(action==='include')
{
// will contain all filtered objects of the source
// get everything belonging to the group to be filtered ( we are storing UIDbelongsTo in the list)
// this means, we are bypassing guest, external or job avatars and generate later an entry avatar, which is directly linked to the person
filtered=await getIncludeObjects(filter,sourceUID,sourceType, target, types, connection)
}
if(action==='exclude' || action==='intersect')
{
// get all entries in the dlist for that source filtered by the filter
filtered=await getExcludeObjects(filter,action,sourceUID,sourceType, target, types, connection)
}
filtered=reduceFiltered(filtered || [])
if(filtered.length)
{
if(action==='include')
{
await processInclude(UIDFilter,filtered, sourceType, target, organization, {connection, virtual})
}
if(action==='exclude' || action==='intersect')
{
await processExclude(UIDFilter, filtered, target, organization, {connection,virtual})
}
}
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}