// @ts-check
/**
* @import {FilterAction, treeAction} from './../../types.js'
*/
import {query, transaction, HEX2uuid, UUID2hex} from '@commtool/sql-query'
import {addUpdateList } from '../../server.ws.js'
import {addFilter} from '../executeFilters/executeFilters.js'
import {personRebuildAccess} from '../rebuildList.js'
import {addVisibility} from '../matchObjects/matchObjects.js';
import { matchObjectsLists } from '../matchObjects/matchObjectsLists.js';
import {match as matchAchievement} from '../../utils/objectfilter/filters/achievement.js'
import {requalify} from '../../Router/job/utilities.js'
import rebuildFees from '../rebuildFees.js'
import { publishEvent } from '../../utils/events.js'
import { addGroupGuest, addGuests } from '../../Router/guest.js'
import { queueAdd, queueAddArray} from './treeQueue.js'
import { listRebuildAccess } from '../rebuildList.js';
import { checkPersonListMember } from '../matchObjects/checkPersonListMember.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';
import { familyAddress } from '../familyAddress.js';
/**
* Handles various actions related to adding objects to a tree structure, such as filters, memberships, visibility,
* and other hierarchical updates. This function processes different types of actions and performs corresponding
* database operations and updates.
*
* @async
* @function addAction
* @param {treeAction} action - The action object containing details about the operation to be performed.
* @throws {Error} Throws an error if any database operation or action processing fails.
*/
export const addAction= async (action)=>
{
try {
const timestamp = action.timestamp ? (typeof action.timestamp === 'string' ? parseFloat(action.timestamp) : action.timestamp) : null
const asOf=timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
const since= timestamp ? `FOR SYSTEM_TIME BETWEEN FROM_UNIXTIME(${timestamp}) AND NOW()` : ''
const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp-3600}) TO NOW()`:''
/* performs the relevant actions, when a filter has been addes to a source (group or list)
* the filter target is supplied as action.UIDnewTarget. It can be a dlist fo include/exclude/intersect filter or a user for visibility/changeability filter
* the filter object is supplied as action.UIDObjectID
* the action.Type is the type of filter, which has been added
* the action.UIDBelongsTo is the source object, which is the group or list, acting as source for the filter
* valid action.Types are:
* - visible: the filter is a visibility filter, which will be used to extract visible objects from the source for a user, or it will define visibility for a list/dlist/email
* - changeable: the filter is a changeability filter, which will be used to extract changeable objects from the source for a user or it will define visibility for a list/dlist/email
* - include: the filter is an include filter, which will be used to extract objects from the source for a dlist, which will be added to the dlist
* - exclude: the filter is an exclude filter, which will be used to extract objects from the source for a dlist, which will be removed from the dlist
* - intersect: the filter is an intersect filter, which will be used to keep only these objects in the dlist, which are also in the filtered source
* */
if (['visible','changeable','include','exclude','intersect'].includes(action.Type))
{
await addFilter(action.UIDObjectID,action.UIDnewTarget,/** @type {FilterAction} */(action.Type), HEX2uuid(action.UIDroot)) // UIDBelongsTo
addUpdateList(action.UIDnewTarget)
}
/* evaluates to which dlists an object should belongs and which useres can see/modify an object, when it is newly created or changed
* action.UIDUIDBelongsTo the base/Member object, which has to be evaluated
* checks all filters, and changes dlists and updates visibility and changeability for the users or for lists,dlists and events
* */
else if(action.Type==='listMember')
{
// match jobs, guest and person and extern
const objects= await query(`SELECT UID FROM Objects ${asOf}
WHERE (UIDBelongsTo=? OR UID=?) AND Type IN ('job','person','guest','extern')`,
[action.UIDObjectID,action.UIDObjectID]
)
if(objects.length)
{
const objectUIDs=objects.map(o=>o.UID)
const groups=await query(`SELECT ObjectBase.UID FROM ObjectBase ${asOf}
INNER JOIN Links ${asOf} ON (Links.UIDTarget=ObjectBase.UID AND Links.Type IN ('member','memberA'))
WHERE Links.UID IN(?) AND ObjectBase.Type = 'group'
GROUP BY ObjectBase.UID
`,[objectUIDs],
)
const groupUIDs=groups.map(o=>o.UID)
await addVisibility(objectUIDs,groupUIDs,{andRemove:true})
await checkPersonListMember(action.UIDBelongsTo, HEX2uuid(action.UIDroot))
}
}
/* * this is executed, when a visibility or changeability filter defining the visibility or changeability of list/dlist/email,evant,eventT has been modified/added
* action.UIDnewTarget is the list/dlist, whichfilters have been modified
* */
else if(action.Type==='listVisibility')
{
await listRebuildAccess(action.UIDnewTarget, HEX2uuid(action.UIDroot))
}
/* * this is executed, when an entry has been added to a list
* we have to match the object against the filters of the list and add it to their dlist
* action.UIDObjectID is the entry object, which has been added to the list
* action.UIDnewTarget is the list, which has received the entry
* */
else if(action.Type==='list')
{
// this is executed if a person was added to a list
// we have to execute the filters for the list
matchObjectsLists(action.UIDObjectID,action.UIDnewTarget, HEX2uuid(action.UIDroot))
}
/* * this has to be executed, when a function has been created or modified according to its qualification requirements
* it (re-)qualifies all jobs, which are based to this function template
* action.UIDObjectID is the function template, which has been created or modified
*/
else if(action.Type==='function')
{
// rebuild jobs for this funntion
requalify(HEX2uuid(action.UIDroot), action.UIDObjectID)
}
/* * this has to be executed, when a function has been created or modified according to its visibility and changeability requirements
* it rebuilds all visibility and changeability filters for the jobs, which are based to this function template
* action.UIDObjectID is the function template, which has been created or modified
* is the group, which has been added to the function
*
* */
else if(action.Type==='functionV')
{
// adjust all visibilities of jobs based on this function
// find all jobs for this function
const jobs=await query(`SELECT Jobs.UID AS UIDJob, Jobs.UIDBelongsTo AS UIDPerson, GLinks.UIDTarget AS UIDGroup
FROM ObjectBase AS Jobs INNER JOIN Links AS FLinks ON(FLinks.UIDTarget=Jobs.UID AND FLinks.Type='function' AND Jobs.Type='job')
INNER JOIN Links AS GLinks ON (GLinks.UID=Jobs.UID AND GLinks.Type='memberA') AND FLinks.UID=?
`,[action.UIDObjectID])
for await (const job of jobs)
{
await transaction(async(connection)=>
{
// delete all visibility and changeability filters for this job
await connection.query(`DELETE ObjectBase,Links
FROM ObjectBase INNER JOIN Links ON(ObjectBase.UID=Links.UID AND ObjectBase.Type IN ('visible','changeable') AND Links.Type ='list')
WHERE Links.UIDTarget=?`,[job.UIDJob])
const functionTemplate= await connection.query(`SELECT ObjectBase.Data FROM ObjectBase WHERE UID=?`,
[action.UIDObjectID],
{cast:['json']})
const functionData=functionTemplate[0].Data
if(functionTemplate.length>0 && Object.keys(functionData.access).length>0)
{
// add the visibility access includers for this job
// get the groups from the hierarchie of this job given by the key
const hGroups=await connection.query(`SELECT ObjectBase.UID,JSON_VALUE(Member.Data,'$.hierarchie') AS Hierarchie,
JSON_VALUE(Member.Data,'$.gender') AS Gender,ObjectBase.Title
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID )
WHERE (Links.UID=? OR ObjectBase.UID=? ) AND ObjectBase.Type='group'
AND JSON_VALUE(Member.Data,'$.hierarchie') IN (?) GROUP BY ObjectBase.UID`,
[job.UIDGroup,job.UIDGroup,Object.keys(functionData.access)])
// add the includer to the groups which will later add the visibilty to the person
if(hGroups.length>0)
{
for(const hGroup of hGroups)
{
// add only filteres to combined groups. if a group with this both hierarchie exists
if(hGroup.Gender==='C' || !hGroups.find(hg=>hg.Hierarchie===hGroup.Hierarchie && hg.Gender==='C'))
{
// add filter to group for extraction of the visible objects and pointing to job
const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
await connection.query(`
INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
VALUES (?,'visible',?,?,?,'visible','',0,?)
`,
[
filterUID, hGroup.UID, functionData.name,hGroup.Title,JSON.stringify(functionData.access[hGroup.Hierarchie])
]
)
// link filter to the person holding the job
await connection.query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'list',?)`,[filterUID,job.UIDJob])
}
}
}
// now add changeability filter
if(functionData.writeAccess)
{
const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
await connection.query(`
INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
VALUES (?,'changeable',?,?,?,'changeable','',0,?)
`,
[
filterUID, job.UIDGroup, functionData.name,'',JSON.stringify(functionData.writeAccess)
]
)
// link filter to the person holding the job
await connection.query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'list',?)`,[filterUID,job.UIDJob])
}
// rebuild visibility for the person holding the job
}
},
{
beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
})
await personRebuildAccess(job.UIDPerson)
}
}
/* * this is executed, when a family member has been added to a family and the organisation has family based fees
* action.UIDnewTarget is the family UID, which has been created or modified
* this will calulate the new membership fees for the family and update the family indices
* */
else if(action.Type==='family' || action.Type==='familyB')
{
// The link has in this case already been created when the member was created
// update family indices and calculate new membership fees
// faked req.session
rebuildFees(action.UIDnewTarget, HEX2uuid(action.UIDroot))
}
/* * syncs adresses, phone numbers, email adresses, accounts between family members
* this has to be called, when a fsmily phone, family email or family address has been created or modified for a member
* action.UIDBelongsTo is the family UID, which the member belongs to which has been created or modified
* */
else if(action.Type==='familySync' )
{
// snyc the data between family members (fmily maial, accounts, address...)
const persons = await query(`SELECT Member.UID,Member.Data,Links.UIDTarget AS MemberA,
ObjectBase.Data AS BaseData, ObjectBase.dindex, ObjectBase.Type,ObjectBase.SortName AS SortBase,
ObjectBase.ValidFrom, ObjectBase.ValidUntil,ObjectBase.dindex
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN Links ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID)
INNER JOIN ObjectBase AS MainGroup ON (Links.UIDTarget=MainGroup.UID AND MainGroup.Type='group')
WHERE ObjectBase.UID=? AND ObjectBase.Type IN ('person','extern')`,
[action.UIDBelongsTo],
{
log:false,
cast:['json']
})
if(persons.length>0)
familyAddress(/** @type {import('../familyAddress.js').FamilyMemberObject} */ (persons[0]), HEX2uuid(action.UIDroot))
}
/* * tree specific actions
* these actions are maintaining the membership tree strcuture of none persons
*/
else
{
// get membership of the target group
let delta=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType
FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget
AND Links.Type IN ('member','memberA','memberS') AND ObjectBase.Type IN('group','ggroup'))
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
WHERE Links.UID=?
GROUP BY ObjectBase.UID`,[action.UIDnewTarget],{log:true})
const deltaPlus=[...delta.map(d=>(d.UIDTarget)),action.UIDnewTarget]
// in case of guest or guest groups, the obejct can already be member of a higher level group
// in this case we do not want to add him again to the higher level group
const secondLevel=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType
FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget
AND Links.Type IN ('member','memberA') AND ObjectBase.Type IN('group','ggroup'))
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE Links.UID IN (?)`
,[deltaPlus])
let deltaVisual
if(secondLevel.length>0)
{
deltaVisual=[...delta,...secondLevel].reduce((result,actual)=>
{
if(!result.find(el=>el.UIDTarget.equals(actual.UIDTarget)))
{
return [...result,actual]
}
else
{
return result
}
},[])
}
else
{
deltaVisual=[...delta]
}
if (action.Type==='guest' || action.Type==='ggroup' )
{
// remove from delta already existing membership of groups or persons, which are allready part of the target groups
const existsDebug=
`SELECT UIDTarget,ObjectBase.Type, ObjectBase.Display FROM Links ${asOf}
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget)
WHERE Links.Type IN ('member','memberA','memberS') AND Links.UID=? AND ObjectBase.Type IN ('guest','person') `
//const existsProd= `SELECT UIDTarget FROM Links ${asOf}WHERE Type IN ('member','memberA', 'memberS') AND UID=? `
const exists=await query(existsDebug,[action.UIDBelongsTo])
delta=delta.filter(el=>(!exists.find(exist=>
(
exist.UIDTarget.equals(el.UIDTarget)
)
)))
}
if (action.Type==='group')
{
// group migration is currently supported for sister groups only
// now we have to trigger the tree queue for all these insertions as well)
const members=await query(`SELECT ObjectBase.UID,ObjectBase.Type, ObjectBase.UIDBelongsTo
FROM Links
INNER JOIN ObjectBase ON (Links.UID=ObjectBase.UID AND Links.Type IN ('member','memberA'))
WHERE ObjectBase.Type IN ('job','guest','extern','person') AND Links.UIDTarget=?`,
[action.UIDObjectID])
if(members.length>0)
{
// we have to add delta membership for the persons before the treeQueue action
const persons=members.filter(m=>m.Type==='extern' || m.Type==='person')
if(persons.length>0 && delta.length>0)
{
// add delta membership
const ADD= persons.flatMap(p=>
delta.map(gr=>([p.UID,gr.UIDTarget]))
)
await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
ADD,{backDate:timestamp,batch:true})
}
await query(`INSERT INTO TreeQueue(UIDRoot,UIDuser,Type,UIDObjectID,UIDBelongsTo,UIDoldTarget,UIDnewTarget,timestamp)
VALUES (?,?,?,?,?,?,?,?)`, members.map(m=>([action.UIDroot,action.UIDuser,m.Type,m.UID,m.UIDBelongsTo,null,action.UIDnewTarget,timestamp])),
{batch:true})
}
addUpdateList(action.UIDObjectID)
}
if (action.Type==='person')
await transaction(async (connection)=>
{
if(delta.length)
{
// delete possible old guest memberships, which would be duplications
connection.query(`DELETE Links FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UIDBelongsTo=? AND ObjectBase.Type='guest' AND ObjectBase.UID=Links.UID)
WHERE Links.UIDTarget IN (?)`,
[action.UIDObjectID,delta.map(el=>(el.UIDTarget))],
)
}
// add guests to groups linked by memberG Link (guest Groups)
// find the group, we have to add the guest as MemberA to (the group with link Type memberGA)
const groupAdmin= delta.find(d=>d.LinkType==='memberGA')
if(groupAdmin)
{
// create guests
const [guest]=await addGuests([action.UIDBelongsTo],groupAdmin.UIDTarget,'guest',HEX2uuid(action.UIDroot))
// now link it to all memberG groups
await connection.query(`INSERT INTO Links (UID,Type,UIDTarget) VALUES (?,'member',?) `,
[deltaVisual.map(UID=>([guest.UID,UID]))],
{batch:true})
}
},{backDate:timestamp})
if(action.Type==='job')
{
const functionTemplate= await query(`SELECT ObjectBase.Data FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='function')
WHERE Links.UIDTarget=?`,
[action.UIDObjectID],
{cast:['json']}
)
const functionData=functionTemplate[0].Data
// add visibility filter
if(functionData.access && Object.keys(functionData.access).length>0)
{
// add the visibility access includers for this job
// get the groups from the hierarchie of this job given by the key
const hGroups=await query(`SELECT ObjectBase.UID,JSON_VALUE(Member.Data,'$.hierarchie') AS Hierarchie,
JSON_VALUE(Member.Data,'$.gender') AS Gender,ObjectBase.Title
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID )
WHERE (Links.UID=? OR ObjectBase.UID=? ) AND ObjectBase.Type='group'
AND JSON_VALUE(Member.Data,'$.hierarchie') IN (?) GROUP BY ObjectBase.UID`,
[action.UIDnewTarget,action.UIDnewTarget,Object.keys(functionData.access)])
// add the includer to the groups which will later add the visibilty to the person
if(hGroups.length>0)
{
for(const hGroup of hGroups)
{
// add only filteres to combined groups. if a group with this both hierarchie exists
if(hGroup.Gender==='C' || !hGroups.find(hg=>hg.Hierarchie===hGroup.Hierarchie && hg.Gender==='C'))
{
// add filter to group for extraction of the visible objects and pointing to job
const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
await query(`
INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
VALUES (?,'visible',?,?,?,'visible','',0,?)
`,
[
filterUID, hGroup.UID, functionData.name,hGroup.Title,JSON.stringify(functionData.access[hGroup.Hierarchie])
]
)
queueAdd(action.UIDroot, action.UIDuser, 'visible', filterUID,hGroup.UID,null,action.UIDObjectID)
}
}
}
else
{
console.error('invalid hgroups in treeAdd.js')
}
}
// add changeability filter
if(functionData.writeAccess)
{
const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
await query(`
INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
VALUES (?,'changeable',?,?,?,'changeable','',0,?)
`,
[
filterUID, action.UIDnewTarget, functionData.name,'',JSON.stringify(functionData.writeAccess)
]
)
queueAdd(action.UIDroot, action.UIDuser, 'changeable', filterUID,action.UIDnewTarget,null,action.UIDObjectID)
}
}
if(['person','guest','ggroup','extern','job','group'/*,'list' most likely obsolete?*/].includes(action.Type))
{
const sourceDelta=[action.UIDnewTarget,...deltaVisual.map(el=>(el.UIDTarget))]
await addVisibility(action.UIDObjectID,sourceDelta)
if(action.Type !=='group' && action.Type !=='event' && action.Type !=='eventT' && action.Type!=='ggroup')
{
matchObjectsLists(action.UIDObjectID,sourceDelta, HEX2uuid(action.UIDroot))
}
}
// add delta membership
if(delta.length && !['person','extern', 'guest'].includes(action.Type))
{
// add delta membership
const ADD= delta.map(gr=>([action.UIDObjectID,gr.UIDTarget]))
await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
ADD,{backDate:timestamp,batch:true})
}
// guest group membership
if(['person','group','guest','ggroup','job'].includes(action.Type))
{
// get ggroups of target
const ggroups= await query(`SELECT ObjectBase.UID,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase ${asOf} WHERE ObjectBase.Type='ggroup' AND ObjectBase.UIDBelongsTo=?
`,
[action.UIDnewTarget])
if(ggroups.length)
{
if(action.Type==='person' || action.Type==='guest')
{
for(const group of ggroups)
{
const guest=await addGuests([action.UIDBelongsTo],group.UID,'guest',HEX2uuid(action.UIDroot),Math.max(action.timestamp,group.validFrom),action.UIDuser)
if(guest.length)
queueAdd(action.UIDroot,action.UIDuser,'guest',guest[0].UID,guest[0].UIDBelongsTo,null,group.UID,timestamp)
}
}
else if(action.Type==='group' || action.Type==='ggroup')
{
for(const group of ggroups)
addGroupGuest({session:{root:action.UIDroot, user: action.UIDuser}},
action.UIDObjectID,group.UID,action.timestamp)
}
}
}
// sending events for membership migrations to all groups where a membership change happened
if(['person','group','guest','extern','job','ggroup','eventJob'].includes(action.Type))
{
// now fire the events
for(const group of deltaPlus)
publishEvent(`/add/group/${action.Type}/${HEX2uuid(group)}`, {
data: [HEX2uuid(action.UIDObjectID)],
backDate: timestamp
})
}
// add mutation group as well
deltaVisual.push({UIDTarget:action.UIDnewTarget})
// update websockets ( for person and extern this has already been performed by migratePerson storage)
if(!['person','extern'].includes(action.Type))
addUpdateList(deltaVisual.map(el=>el.UIDTarget))
}
}
catch(e)
{
errorLoggerUpdate(e || new Error('treeAdd: Unknown error occurred'))
}
}