// @ts-check
/**
* @import {PublishEventOptions} from '../utils/events.js'
*/
import {query,transaction, HEX2uuid} from '@commtool/sql-query'
import { includeChangeability, includeVisibility, visibilityList, visibilityFilter } from './executeFilters/executeFilters.js'
import { matchObject } from '@commtool/object-filter'
import { addVisibility } from './matchObjects/matchObjects.js'
import { publishVisibilityDiff, resolveVisibilityOrganization } from './visibilityEvents.js'
import { errorLoggerUpdate } from '../utils/requestLogger.js'
import { ACCOUNTING_TARGETS, GENERIC_VISIBLE_TARGETS, typesIn } from './visibilityTargets.js'
import { NORMALIZED_VISIBLE_SCOPES } from '../config/featureFlags.js'
// Object types whose visibility is rebuilt by the generic list/visibility path.
const GENERIC_TARGET_TYPES_IN = typesIn([...GENERIC_VISIBLE_TARGETS]);
/**
* Was der Personen-Rebuild **loeschen** darf.
*
* Muss genau die Menge sein, die `personListRebuildAccess` danach wieder
* aufbaut — sonst loescht der eine Lauf, was der andere nicht zurueckholt.
* Genau das war der Fehler: die Loesch-Menge stand auf den Legacy-Typen fest,
* waehrend die Wiederaufbau-Menge um `project` und die Share-Typen gewachsen
* ist. Ein Projekt/ein Share verlor damit seine Personen-Zeile, ohne dass sie
* neu entstand.
*
* Accounting-Ziele sind ausgenommen, solange die normalisierte Ueberfuehrung
* laeuft — sie werden getrennt gerechnet.
*/
const REBUILD_CLEANUP_EXCLUDE = NORMALIZED_VISIBLE_SCOPES
? `ObjectBase.Type NOT IN ${typesIn([...GENERIC_VISIBLE_TARGETS, ...ACCOUNTING_TARGETS])}`
: `ObjectBase.Type NOT IN ${GENERIC_TARGET_TYPES_IN}`;
export const personRebuildAccess=async (UID,UIDOrga=null)=>
{
try {
// rebuild all visibilities for none list/dlist/email/location the user with this UID has
// delete all existing links for the user UID
//
// Der Lauf meldet **als Ganzes**: er loescht erst alle Zeilen dieses Typs
// und baut sie dann neu auf. Deshalb derselbe Ausschnitt vor und nach der
// Transaktion — und die Job-Filter darin melden nicht selbst
// (`report=false`), sonst kaeme dasselbe Recht zweimal an.
const scopeSql=`SELECT Visible.UID, Visible.Type, Visible.UIDUser, ObjectBase.Type AS ObjectType
FROM Visible INNER JOIN ObjectBase
ON (ObjectBase.UID=Visible.UID AND ${REBUILD_CLEANUP_EXCLUDE})
WHERE Visible.UIDUser=? AND Visible.Accounting IS NULL`
const oldVisibility=await query(scopeSql,[UID])
await transaction(async(connection)=>
{
//console.log('delete person visibility', HEX2uuid(UID))
await connection.query(`DELETE Visible FROM Visible INNER JOIN ObjectBase
ON (ObjectBase.UID=Visible.UID AND ${REBUILD_CLEANUP_EXCLUDE})
WHERE Visible.UIDUser=? AND Visible.Accounting IS NULL`,[UID])
// now build access for all functions of the person
// get all job filters and execute them
const jobsFilter=await connection.query(`SELECT filter.UID AS UIDFilter, filter.Type, Jobs.UID AS UIDJob
FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type='list')
INNER JOIN ObjectBase AS Jobs ON (Jobs.UID=Links.UIDTarget AND Jobs.Type='job')
WHERE filter.Type IN ('visible','changeable') AND Jobs.UIDBelongsTo=?
GROUP BY filter.UID`,[UID])
if(jobsFilter.length)
{
for (const filter of jobsFilter)
{
if(filter.Type==='visible')
await includeVisibility(filter.UIDFilter,filter.UIDJob,connection.connection,null,false)
else if(filter.Type==='changeable')
await includeChangeability(filter.UIDFilter,filter.UIDJob,connection.connection,null,false)
}
}
// enable user to edit himself
await connection.query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES(?,'changeable',?) ON DUPLICATE KEY UPDATE Type='changeable'`,[UID,UID])
},
{
beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
})
const newVisibility=await query(scopeSql,[UID])
publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga ?? await resolveVisibilityOrganization(UID))
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Rebuilds accounting visibility (Visible.Accounting) for a person based on
* their accountingVisible/accountingChangeable filter objects (rule-based access).
*
* Evaluates the filter rules and sets Visible.Accounting entries. Respects
* hierarchy: admin > changeable > visible — never overwrites a higher level.
*
* This is a separate step from personRebuildAccess and should only be called
* when accounting rules change (financialMaster toggle or accountingAccess/
* accountingWriteAccess changes), NOT on every visibility rebuild.
*
* @param {Buffer|string} UID - hex or UUID of the person
*/
export const personRebuildAccountingAccess = async (UID) => {
try {
// Clear rule-based Accounting (keep 'admin' from financialMaster)
await query(`UPDATE Visible SET Accounting=NULL
WHERE Visible.UIDUser=? AND Accounting IN ('visible','changeable')`,[UID])
const accountingFilters = await query(`SELECT filter.UID AS UIDFilter, filter.Type, Jobs.UID AS UIDJob
FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type='list')
INNER JOIN ObjectBase AS Jobs ON (Jobs.UID=Links.UIDTarget AND Jobs.Type='job')
WHERE filter.Type IN ('accountingVisible','accountingChangeable') AND Jobs.UIDBelongsTo=?
GROUP BY filter.UID`, [UID])
for (const filter of accountingFilters) {
const accountingLevel = filter.Type === 'accountingVisible' ? 'visible' : 'changeable'
const [filterData] = await query(`SELECT Data AS filterJSON, UIDBelongsTo AS sourceUID
FROM ObjectBase WHERE UID=?`, [filter.UIDFilter], {cast: ['json']})
if (!filterData) continue
const filtered = await visibilityFilter(filterData.filterJSON, filterData.sourceUID)
if (filtered && filtered.length) {
const [{user}] = await query(`SELECT UIDBelongsTo AS user FROM Objects WHERE UID=?`, [filter.UIDJob])
await query(`INSERT INTO Visible (UID, Type, Accounting, UIDUser) VALUES (?,'visible',?,?)
ON DUPLICATE KEY UPDATE Accounting =
CASE
WHEN VALUES(Accounting) = 'admin' THEN 'admin'
WHEN Accounting = 'admin' THEN Accounting
WHEN VALUES(Accounting) = 'changeable' THEN 'changeable'
WHEN Accounting = 'changeable' THEN Accounting
ELSE VALUES(Accounting)
END`,
filtered.map(o => ([o.UID, accountingLevel, user])),
{batch: true})
}
}
}
catch(e) {
errorLoggerUpdate(e)
}
}
export const objectRebuildAccess=async (UIDobject,targets,organization=null)=>
{
try {
// rebuild all visibility links for an Object when it is removed from targets
/* console.log('delete object visibility', HEX2uuid(UIDobject),targets.map(u=>HEX2uuid(u)))
await query(`DELETE Visible FROM Visible
INNER JOIN ObjectBase AS Jobs ON (Jobs.UIDBelongsTo=Visible.UIDUser AND Jobs.Type='job' )
INNER JOIN Links ON (Links.UID=Jobs.UID AND Links.Type IN ('member','memberA'))
WHERE Visible.UID=? AND Links.UIDTarget IN (?)`,[UIDobject,targets])*/
// now build access for all functions of the persons, which are not part of the target list
// get all job filters which are still belonging to the object and execute them
// rebuild access for actual targets its belonging to
const sources= await query(`SELECT UID FROM Links WHERE UIDTarget=? AND Type IN ('member','memberA')`,[UIDobject])
if(sources.length)
await addVisibility(UIDobject,sources.map(s=>s.UID),{andRemove:true,organization})
}
catch(e)
{
errorLoggerUpdate(e)
}
}
export const listRebuildAccess=async (UIDtarget, UIDOrga)=>
{
try {
// rebuild all visibility links for a specfic list,dlist,emeil,event,eventT,location supllied in UIDtarget
// delete all existing links for the list
const [{targetType}]=await query(`SELECT Type AS targetType FROM ObjectBase WHERE UID=? `,[UIDtarget])
// Ensure UIDOrga is in hex format for publishEvent
if (Buffer.isBuffer(UIDOrga)) {
UIDOrga = HEX2uuid(UIDOrga);
}
// `ObjectType` wird nicht gelesen, sondern gesetzt: alle Zeilen haengen an
// `UIDtarget`, dessen Typ oben schon feststeht. `publishVisibilityDiff`
// braucht das Feld, weil es den Event-Key daraus baut.
const oldVisibility=(await query(`SELECT Visible.UIDUser,Visible.Type
FROM Visible
WHERE Visible.UID=?`,[UIDtarget])).map(el=>({...el,UID:UIDtarget,ObjectType:targetType}))
await transaction(async (connection)=>
{
await connection.query(`DELETE Visible FROM Visible WHERE UID=?`,[UIDtarget])
const objectFilter=await query(`SELECT filter.UID AS UIDFilter,filter.Data AS Data, filter.UIDBelongsTo AS UIDSource
FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type IN ('list','dlist','email','event','eventT','location'))
WHERE filter.Type IN ('visible','changeable') AND Links.UIDTarget=? `,[UIDtarget])
if(objectFilter.length)
{
for (const filter of objectFilter)
{
// Die Organisation muss durchgereicht werden: `visibilityList`
// laeuft sonst auf seinen Default (Null-Organisation) und die
// von ihm gemeldeten /add|remove-Events tragen die falsche
// Organisation im Payload, obwohl die Zeile am richtigen
// Objekt landet.
await visibilityList(filter.UIDFilter,UIDtarget,connection,UIDOrga)
}
}
// now add the owner of the list to the visibility
await connection.query(`INSERT INTO Visible (UID,Type,UIDUser)
SELECT Links.UID,'admin',Links.UIDTarget
FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget AND ObjectBase.Type='person')
WHERE Links.Type ='member' AND Links.UID=?
ON DUPLICATE KEY UPDATE Type='admin'`,[UIDtarget])
// always add the organization superadmin (memberSys extern) so it survives filter replacements
await connection.query(`INSERT INTO Visible (UID,Type,UIDUser)
SELECT ?, 'admin', Links.UID
FROM Links
INNER JOIN ObjectBase AS Admin ON (Admin.UID=Links.UID AND Admin.Type='extern')
WHERE Links.Type='memberSys' AND Links.UIDTarget=U_UUID2BIN(?)
ON DUPLICATE KEY UPDATE Type='admin'`,[UIDtarget, UIDOrga])
},
{
beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
})
const newVisibility=(await query(`SELECT Visible.UIDUser,Visible.Type
FROM Visible
WHERE Visible.UID=?`,[UIDtarget])).map(el=>({...el,UID:UIDtarget,ObjectType:targetType}))
// Der Vertrag lebt in `visibilityEvents` — nicht mehr als Kopie der
// Schleife hier (siehe 057 §16).
publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga)
}
catch(e)
{
errorLoggerUpdate(e)
}
}
export const personListRebuildAccess= async (UID, UIDOrga)=>
{
// rebuild all visibilities for list/email/dlist/event/eventT the user with this UID has
// delete all exsiting links for the user UID
try {
// Ensure UIDOrga is in hex format for publishEvent
if (Buffer.isBuffer(UIDOrga)) {
UIDOrga = HEX2uuid(UIDOrga);
}
/** @type {any[]} */
let oldVisibility = []
/** @type {any[]} */
let newVisibility = []
// Read-only: get all filter pairs before the transaction to avoid long-lived locks
const filterPairs=await query(`SELECT filter.UID,filter.Data AS filterData,list.UID AS listUID,job.Type AS ObjectType, job.Data AS ExtraData,Member.Data,
Main.Data AS MainBaseData, list.Display,filter.Type
FROM ObjectBase AS filter
INNER JOIN Links AS gLink ON (gLink.UIDTarget=filter.UIDBelongsTo AND filter.Type IN ('visible','changeable' ) AND gLink.Type IN ('member','memberA','memberSys') )
INNER JOIN Links AS lLink ON (lLink.UID=filter.UID AND lLink.Type='list')
INNER JOIN ObjectBase AS list ON (list.UID=lLink.UIDTarget AND list.Type IN ${GENERIC_TARGET_TYPES_IN})
INNER JOIN ObjectBase AS job ON (gLink.UID=job.UID AND job.Type IN ('person','guest','extern','job'))
INNER JOIN Member ON (Member.UID=job.UIDBelongsTo)
INNER JOIN ObjectBase AS Main ON (Main.UID=Member.UID)
WHERE job.UIDBelongsTo=?
GROUP BY filter.UID,job.UID
`,[UID])
await transaction(async (connection)=>
{
//console.log('starte transaktion', HEX2uuid(UID))
oldVisibility=await connection.query(`SELECT Visible.UID,Visible.Type,Visible.UIDUser,ObjectBase.Type AS ObjectType
FROM Visible INNER JOIN ObjectBase
ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN})
WHERE Visible.UIDUser=?`,[UID])
await connection.query(`DELETE Visible FROM Visible INNER JOIN ObjectBase
ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN})
WHERE Visible.UIDUser=?`,[UID])
// now build pair of access of lists for all jobs of the person
// now execute all filter object pairs
const toBeAdded=filterPairs.map(el=>
({...el,filterData:JSON.parse(el.filterData),Data:JSON.parse(el.Data),ExtraData:JSON.parse(el.ExtraData),MainBaseData:JSON.parse(el.MainBaseData)})
)
.filter(fp=>(
matchObject(
{UID:UID, Type:fp.ObjectType, Data:fp.Data, ExtraData: fp.ExtraData, MainBaseData: fp.MainBaseData}
,fp.filterData)
))
.reduce((result,actual)=>{
const already=result.find(el=>el.listUID.equals(actual.listUID))
if(already)
{
if(actual.Type==='changeable')
{
if(actual.filterData.admin)
already.Type='admin'
else
already.Type='changeable'
}
return result
}
else
{
const Type= actual.Type==='changeable' ? actual.filterData.admin ? 'admin' :'changeable' :'visible'
return [...result,{listUID:actual.listUID,Type:Type}]
}
},[])
if(toBeAdded.length >0)
{
await connection.query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,?,?)
ON DUPLICATE KEY UPDATE
Type=IF(VALUE(Type)='admin','admin',IF(Type<>'admin'
AND VALUE(Type)='changeable','changeable',Type))`,
toBeAdded.map(el=>([el.listUID,el.Type,UID])),
{batch:true})
}
// Die Owner-Zeile (`member`-Link am Ziel) entsteht sonst nur beim
// **Ziel**-Rebuild (`listRebuildAccess`). Dieser Lauf loescht sie mit
// (sie liegt im generischen Ziel-Set) und kann sie nicht aus Filtern
// gewinnen — der Eigentuemer verloere sein `admin` bei jeder
// Personen-Aenderung. Deshalb hier dieselbe Zeile zurueckholen.
await connection.query(`INSERT INTO Visible (UID,Type,UIDUser)
SELECT Links.UID,'admin',Links.UIDTarget
FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget AND ObjectBase.Type='person')
INNER JOIN ObjectBase AS Target ON (Target.UID=Links.UID AND Target.Type IN ${GENERIC_TARGET_TYPES_IN})
WHERE Links.Type= 'member' AND Links.UIDTarget=?
ON DUPLICATE KEY UPDATE Type='admin'`,[UID])
newVisibility=await connection.query(`SELECT Visible.UID,Visible.Type,Visible.UIDUser,ObjectBase.Type AS ObjectType
FROM Visible INNER JOIN ObjectBase
ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN})
WHERE Visible.UIDUser=?`,[UID])
// console.log('beende transaktion', HEX2uuid(UID))
},
{
beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
})
// Ein Vertrag fuer alle Meldewege (siehe 057 §16): die Zeilen tragen hier
// bereits `ObjectType` aus dem Join.
publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga)
}
catch(e)
{
errorLoggerUpdate(e)
}
}