/** * Field Configuration Scheme Housekeeping (Cloud) – Report + optional Delete * ------------------------------------------------------------------------- * Delete rule: * - Scheme darf nur gelöscht werden, wenn es mit KEINEM Projekt verknüpft ist. * - Extra-Schutz über PROTECTED_SCHEME_IDS und PROTECTED_NAME_PATTERNS. */ def PROTECTED_SCHEME_IDS = [ // "10000" ] def PROTECTED_NAME_PATTERNS = [ "default", "system" ] def DRY_RUN = true // erst auf false setzen, wenn Report passt def PROJECT_PAGE_SIZE = 50 // paging für project/search def SCHEME_PAGE_SIZE = 100 // paging für fieldconfigurationscheme logger.info("=== Field Configuration Scheme Housekeeping ===") logger.info("Protected scheme IDs: ${PROTECTED_SCHEME_IDS}") logger.info("Protected name patterns: ${PROTECTED_NAME_PATTERNS}") logger.info("DRY_RUN: ${DRY_RUN}") def isNameProtected = { String name -> def n = (name ?: "").toLowerCase() return PROTECTED_NAME_PATTERNS.any { p -> n.contains((p ?: "").toLowerCase()) } } def isIdProtected = { String id -> return PROTECTED_SCHEME_IDS.any { it?.toString() == id?.toString() } } /** * 1) Alle Projekte holen (id + key) */ def projects = [] def startAt = 0 while (true) { def resp = get("/rest/api/3/project/search?startAt=${startAt}&maxResults=${PROJECT_PAGE_SIZE}") .asObject(Map) if (resp.status != 200) { logger.error("ERROR|PROJECT_SEARCH_FAILED|status=${resp.status}|body=${resp.body}") break } def values = resp.body?.values ?: [] projects.addAll(values) def isLast = resp.body?.isLast if (isLast == true || values.isEmpty()) break startAt += PROJECT_PAGE_SIZE } logger.info("INFO|TOTAL_PROJECTS|${projects.size()}") def projectIdToKey = [:] projects.each { p -> def pid = p?.id?.toString() def pkey = p?.key?.toString() if (pid && pkey) projectIdToKey[pid] = pkey } /** * 2) Mapping: fieldConfigurationSchemeId -> [projectIds] * (Endpoint braucht projectId, daher über alle Projekte iterieren) * * REST: GET /rest/api/3/fieldconfigurationscheme/project?projectId={projectId} */ def schemeToProjectIds = [:].withDefault { [] } projectIdToKey.keySet().each { pid -> def resp = get("/rest/api/3/fieldconfigurationscheme/project?projectId=${pid}") .asObject(Map) if (resp.status != 200) { logger.warn("WARN|SCHEME_LOOKUP_FAILED|projectId=${pid}|status=${resp.status}") return } def values = resp.body?.values ?: [] values.each { row -> def schemeId = row?.fieldConfigurationScheme?.id?.toString() def pids = (row?.projectIds ?: []).collect { it?.toString() }.findAll { it != null } if (schemeId) { schemeToProjectIds[schemeId] = (schemeToProjectIds[schemeId] + pids).unique() } } } /** * 3) Alle Field Configuration Schemes holen * * REST: GET /rest/api/3/fieldconfigurationscheme */ def schemes = [] startAt = 0 while (true) { def resp = get("/rest/api/3/fieldconfigurationscheme?startAt=${startAt}&maxResults=${SCHEME_PAGE_SIZE}") .asObject(Map) if (resp.status != 200) { logger.error("ERROR|SCHEME_LIST_FAILED|status=${resp.status}|body=${resp.body}") break } def values = resp.body?.values ?: [] schemes.addAll(values) def isLast = resp.body?.isLast if (isLast == true || values.isEmpty()) break startAt += SCHEME_PAGE_SIZE } logger.info("INFO|TOTAL_SCHEMES|${schemes.size()}") /** * 4) Auswertung + optional Delete */ def keptAssociated = 0 def keptProtected = 0 def candidates = [] // [id,name,reason] schemes.each { s -> def schemeId = s?.id?.toString() def schemeName = s?.name?.toString() def assocProjectIds = schemeToProjectIds[schemeId] ?: [] def assocProjectKeys = assocProjectIds.collect { projectIdToKey[it] }.findAll { it != null }.unique() def nameProtected = isNameProtected(schemeName) def idProtected = isIdProtected(schemeId) if (idProtected || nameProtected) { keptProtected++ def why = [] if (idProtected) why << "ID_PROTECTED" if (nameProtected) why << "NAME_PROTECTED" logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|reason=${why}") return } if (!assocProjectIds.isEmpty()) { keptAssociated++ logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|reason=ASSOCIATED_TO_PROJECTS") return } logger.info("DEL?|schemeId=${schemeId}|name=${schemeName}|assocProjects=[]|reason=UNASSOCIATED") candidates << [schemeId, schemeName, "UNASSOCIATED"] if (!DRY_RUN) { def delResp = delete("/rest/api/3/fieldconfigurationscheme/${schemeId}") .asString() if (delResp.status == 204) { logger.info("DEL|OK|schemeId=${schemeId}|name=${schemeName}") } else { logger.error("DEL|FAIL|schemeId=${schemeId}|name=${schemeName}|status=${delResp.status}|body=${delResp.body}") } } } logger.info("=== SUMMARY ===") logger.info("Total schemes: ${schemes.size()}") logger.info("Kept (associated to any projects): ${keptAssociated}") logger.info("Kept (protected by rules): ${keptProtected}") logger.info("Delete candidates: ${candidates.size()}") candidates.each { c -> logger.info("CANDIDATE|schemeId=${c[0]}|name=${c[1]}|reason=${c[2]}") } logger.info("=== DONE ===")