4.5. Running scripts directly

In many occasions it is handy for administrators to run scripts directly. So, instead of having to create a custom operation script, then a custom operation, then granting permissions, refreshing the browser and running, there is a menu called Run script, which presents a text box where the script may be typed in or pasted, which can be executed directly. Of course, only the basic bindings are available.

The result of the script can be either a string, which is then displayed as plain text, or an object / map compatible with org.cyclos.model.system.scripts.ScriptResult. So, for example, to return an HTML text with a title, the script can return [title:"The result title", richText:"<b>Formatted</b> text"]. To show a notification, the the script can return [notification:"Notification text"]. The same prefixes available on notifications for custom operations are availeble on notifications: [INFO], [WARN] and [ERROR].

4.5.1. Examples

4.5.1.1. Remove all users, transactions and related data

Here is an example of a script to remove all regular users (not administrators) and related data, as well as all system to system transactions. When running in a network, will remove data on that network. When running in global mode, will affect the entire system. Be advised that there will be no confirmation, and all users and all related data will be removed.

The script works by first recreating all database constraints with the option ON DELETE CASCADE. Then, all users are removed, which will cascade the removal to accounts, transfers, advertisements, records, messages, notifications, references and so on. Be careful when running in systems where specific users are used in the configuration, such as fees that are paid by a specific user, or payment types which are restricted to specific users. In such configurations, all such related data will be removed as well. Also, note that it may take a while to run, so, please, wait before the script completes.

AGAIN: be very careful when using this script! Only run it on test instances and always have a database backup before running it.

import java.sql.Connection

import org.cyclos.impl.InvokerHandler
import org.cyclos.impl.access.SessionData
import org.cyclos.impl.search.SearchHandler
import org.cyclos.impl.system.ScriptHelper
import org.cyclos.impl.utils.cache.CacheHandler
import org.cyclos.impl.utils.cache.CacheType
import org.springframework.jdbc.core.ConnectionCallback
import org.springframework.jdbc.core.JdbcTemplate

import groovy.sql.GroovyRowResult
import groovy.sql.Sql
import groovy.transform.Field
import groovy.transform.TypeChecked

@Field SessionData sessionData = binding.sessionData
@Field ScriptHelper scriptHelper = binding.scriptHelper
@Field CacheHandler cacheHandler = binding.cacheHandler
@Field SearchHandler searchHandler = binding.searchHandler
@Field InvokerHandler invokerHandler = binding.invokerHandler
@Field JdbcTemplate jdbc = binding.jdbcTemplate
@Field Connection connection = jdbc.execute({ it } as ConnectionCallback)
@Field Sql sql = new Sql(connection)
@Field Long networkId = sessionData.network?.id


@Field List<GroovyRowResult> constraints = sql.rows("""
SELECT tc.table_name, kcu.column_name, tc.constraint_name,
    ccu.table_name AS foreign_table_name,
    ccu.column_name AS foreign_column_name
FROM
    information_schema.table_constraints AS tc
    JOIN information_schema.key_column_usage AS kcu
      ON tc.constraint_name = kcu.constraint_name
    JOIN information_schema.constraint_column_usage AS ccu
      ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = 'FOREIGN KEY'
""".toString())

def recreateConstraints(boolean cascade) {
    constraints.forEach { r ->
        String drop = """alter table ${r.table_name}
            drop constraint ${r.constraint_name}"""
        String create = """alter table ${r.table_name}
            add constraint ${r.constraint_name}
            foreign key (${r.column_name})
            references ${r.foreign_table_name} (${r.foreign_column_name})"""
        if (cascade) {
            create += " on delete cascade"
        }
        sql.execute(drop)
        sql.execute(create)
    }
}

@Field String accountsSql = """select a.id
from accounts a
inner join account_types at on a.account_type_id = at.id
inner join currencies c on at.currency_id = c.id
"""
if (networkId != null) {
    accountsSql += " where c.network_id = ${networkId}"
}

@TypeChecked
def execute() {
    // Recreate constraints with on delete cascade
    recreateConstraints(true)

    // First break the cycle on accounts -> account_rates,
    // otherwise system accounts with rates are deleted as well
    sql.executeUpdate("update accounts set account_rates_id = null"
            + " where id in (" + accountsSql + ")")

    // Remove banking data. This will include system accounts as well
    def tablesAndColumns = [
        transfers: ['from_id', 'to_id'],
        transactions: ['from_id', 'to_id'],
        amount_reservations: ['account_id'],
        closed_account_balances: ['account_id'],
        account_balances: ['account_id'],
        dirty_account_balances: ['account_id'],
        account_rates: ['account_id']]
    tablesAndColumns.forEach { table, cols ->
        cols.forEach { col ->
            sql.executeUpdate("delete from " + table +
                    " where " + col + " in (" + accountsSql + ")")
        }
    }

    // Remove users
    def removedUsers = sql.executeUpdate("""
        delete from users
        where (subclass = 'OPERATOR'
        or user_group_id in (
        select id from groups where subclass in ('MEMBER_GROUP', 'BROKER_GROUP')))
        """ + ((networkId == null) ? "" : " and network_id = ${networkId}"))

    // Remove some other tables which would be just placeholders with empty data
    sql.executeUpdate("delete from bulk_actions"
            + (networkId == null ? '' : " where network_id = ${networkId}"))
    sql.executeUpdate("delete from mailing_lists"
            + (networkId == null ? '' : " where network_id = ${networkId}"))
    sql.executeUpdate("delete from account_fee_logs"
            + (networkId == null ? '' : """
            where account_fee_id in (
            select af.id
            from account_fees af
            inner join account_types at on af.account_type_id = at.id
            inner join currencies c on at.currency_id = c.id
            where c.network_id = ${networkId})"""))

    // Recreate constraints as initial
    recreateConstraints(false)

    return "Removed ${removedUsers} users"
}

def output = execute()

// Clear all caches
CacheType.all().each { cacheHandler.scheduleClear(it) }

// Reindex all data
searchHandler.reindex()

// Execute the script
return output