Skip to content

Instantly share code, notes, and snippets.

@thisnameissoclever
Last active April 6, 2024 08:28
Show Gist options
  • Save thisnameissoclever/ba6c27ae917580e24445ba021b169bb7 to your computer and use it in GitHub Desktop.
Save thisnameissoclever/ba6c27ae917580e24445ba021b169bb7 to your computer and use it in GitHub Desktop.
Redact or delete a given journal entry in ServiceNow. Usage documentation: http://redactor.snc.guru/
/*
Script Include definition:
Name: JournalRedactor
Client Callable: false
Accessible from: All application scopes
Additional details:
Version: 1.3
Usage documentation: http://redactor.snc.guru/
License: https://gist.github.com/thisnameissoclever/767b8a738b929a0bd943965431061c1e
*/
var JournalRedactor = Class.create();
JournalRedactor.prototype = {
initialize: function(journalEntryID) {
this.journalEntryData = new this._JournalEntry();
if (journalEntryID) {
this.journalEntryData.journal_entry_id = journalEntryID;
this._populateJournalEntryData(journalEntryID);
}
this._verbose = false;
},
/**
* Redact or delete a given journal entry. This will redact or delete the sys_journal_field
* entry, the sys_audit record, and the sys_history_set (along with all sys_history_lines
* associated with it).
* @param deleteEntry {boolean} True if the journal entry should be deleted rather than
* replaced with some other text, false if it should be replaced. If this param is set to
* false, then the second param (newValue) must be specified.
* @param newValue {string=} The new value to replace the old journal text with. If deleteEntry
* is set to false, this param is mandatory. Otherwise, it is not necessary.
* @returns {boolean} True if redaction was successful; false if not. If redaction was
* unsuccessful, an error should be found in the system logs from the source "redact method
* of JournalRedactor Script Include".
*/
redact: function(deleteEntry, newValue) {
if (!this._readyForRedaction(this.journalEntryData)) {
gs.logError('Not ready for redaction. Some data is missing from journalEntryData: ' +
JSON.stringify(this.journalEntryData),
'redact method of JournalRedactor Script Include');
return false;
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', 'redact method of JournalRedactor Script Include');
return false;
} else if (!this.journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', 'redact method of JournalRedactor Script Include');
return false;
}
if (newValue) {
this.journalEntryData.new_journal_value = newValue;
}
var success = (this._redactJournalEntry(this.journalEntryData, deleteEntry, newValue) &&
this._redactAudit(this.journalEntryData, deleteEntry, newValue) &&
this._deleteHistory(this.journalEntryData));
return success;
},
/**
* This method allows you to explicitly set the journal entry you'd like to redact by
* specifying the sys_id of that record in the sys_journal_field table.
* You can also specify this in the constructor. If you don't know the journal entry
* sys_id, you can use the .findJournalID() method.
* @param journalEntryID {string} The sys_id of the record in the sys_journal_field
* table, corresponding to the journal entry you'd like to redact.
* @returns {boolean} true if successful, false if not.
*/
setJournalID: function(journalEntryID) {
if (!journalEntryID) {
gs.logWarning('setJournalID method of JournalRedactor Script Include called, ' +
'but no journal entry sys_id was specified. Clearing journal entry ID value.',
'JournalRedactor Script Include setJournalID method');
this.journalEntryData.journal_entry_id = '';
return false;
} else {
this.journalEntryData.journal_entry_id = journalEntryID;
}
return this._populateJournalEntryData(journalEntryID); //Need this data in the data object for future redaction steps.
},
/**
* Use this method if you don't already know the sys_id of the sys_journal_field
* record corresponding to the journal entry you'd like to redact or delete.
* This will grab all the necessary data and populate it into the journalEntryData
* object.
* This method accepts two, three, or four arguments.
* @param recordID {String}
* @param journalText {String}
* @param targetTableName {String=}
* @param journalFieldName {String=}
* @returns {Boolean} True if the journal entry was successfully located, or false if it was not.
*/
findJournalID: function(recordID, journalText, targetTableName, journalFieldName) {
var journalEntryID;
if (this._verbose) {
gs.log('Attempting to find record in sys_journal_field table, with ' +
arguments.length + ' arguments: ' + JSON.stringify(arguments));
}
/*//Set variables based on number of arguments specified
if (arguments.length == 2) {
recordID = arguments[0];
journalText = arguments[1];
} else if (arguments.length == 3) {
recordID = arguments[0];
targetTableName = arguments[1];
journalText = arguments[2];
} else if (arguments.length = 4) {
recordID = arguments[0];
targetTableName = arguments[1];
journalFieldName = arguments[2];
journalText = arguments[3];
}*/
if (arguments.length < 2 || arguments.length > 4) { //ERROR!
gs.logError('Incorrect number of arguments specified. ' + arguments.length +
' arguments specified, but 2, 3, or 4 arguments are expected.',
'getJournalID method of JournalRedactor Script Include');
return false;
}
/*if (!recordID || !journalText) {
gs.logError('getJournalID method of JournalRedactor Script Include could not ' +
'continue. Invalid argument(s) specified. recordID: ' + recordID + '. ' +
'journalText: ' + journalText + '.');
return this.journalEntryData;
}*/ //replaced by handling for different numbers of arguments above
//Update details in journalEntryData
this.journalEntryData.target_record_id = recordID;
this.journalEntryData.old_journal_value = journalText;
var grJournal = new GlideRecord('sys_journal_field');
grJournal.addQuery('element_id', recordID);
grJournal.addQuery('value', journalText);
if (targetTableName) {
grJournal.addQuery('name', targetTableName);
}
if (journalFieldName) {
grJournal.addQuery('element', journalFieldName);
}
grJournal.query();
if (grJournal.next()) {
journalEntryID = grJournal.getValue('sys_id');
journalFieldName = grJournal.getValue('element');
targetTableName = grJournal.getValue('name');
this.journalEntryData.journal_entry_id = journalEntryID;
this.journalEntryData.journal_field_name = journalFieldName;
this.journalEntryData.target_table_name = targetTableName;
} else {
return false;
}
if (grJournal.hasNext() && this._verbose) {
gs.log('findJournalID() method of JournalRedactor Script Include: ' +
'Additional journal entry was found, matching these arguments: ' +
JSON.stringify(arguments) + '. You may want to run this script again.');
}
return journalEntryID;
},
/**
* Set verbose mode to true or false. This enables additional logging if true.
* @param b {boolean}
*/
setVerbose: function(b) {
this._verbose = b;
},
/******PRIVATE METHODS BELOW******/
_readyForRedaction: function(journalEntryData) {
var p;
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Checking if journalEntryData is ready for redaction: ' +
JSON.stringify(journalEntryData));
}
for (p in journalEntryData) {
if (journalEntryData.hasOwnProperty(p)) {
if (p != 'old_journal_value' && p != 'new_journal_value' && !journalEntryData[p]) {
if (this._verbose) {
gs.log('journalEntryData not ready for redaction: ' +
JSON.stringify(journalEntryData));
}
return false;
}
}
}
if (this._verbose) {
gs.log('journalEntryData IS ready for redaction: ' +
JSON.stringify(journalEntryData));
}
return true;
},
_redactJournalEntry: function(journalEntryData, deleteEntry, newValue) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Beginning redaction of sys_journal_field record: ' +
JSON.stringify(journalEntryData));
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', '_redactJournalEntry method of JournalRedactor Script Include');
return false;
} else if (!journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_redactJournalEntry method of JournalRedactor Script Include');
return false;
}
var grJournal = new GlideRecord('sys_journal_field');
grJournal.get(journalEntryData.journal_entry_id);
if (deleteEntry) {
grJournal.deleteRecord();
if (this._verbose) {
gs.log('Deleted journal entry with ID ' + journalEntryData.journal_entry_id);
}
} else {
grJournal.setValue('value', newValue);
grJournal.update();
if (this._verbose) {
gs.log('Updated journal entry with ID ' + journalEntryData.journal_entry_id +
' to new value: ' + newValue);
}
}
if (this._verbose) {
gs.log('redaction complete: ' +
JSON.stringify(journalEntryData));
}
return true;
},
_redactAudit: function(journalEntryData, deleteEntry, newValue) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Beginning redaction of sys_audit record: ' +
JSON.stringify(journalEntryData));
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', '_redactAudit method of JournalRedactor Script Include');
return false;
} else if (!journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_redactAudit method of JournalRedactor Script Include');
return false;
}
var grAudit = new GlideRecord('sys_audit');
grAudit.addQuery('tablename', journalEntryData.target_table_name);
grAudit.addQuery('documentkey', journalEntryData.target_record_id);
grAudit.addQuery('newvalue', journalEntryData.old_journal_value);
//field names are not misspelled, they're just weird in this table.
grAudit.query();
while (grAudit.next()) {
if (deleteEntry) {
if (this._verbose) {
gs.log('Deleting audit record because deleteEntry param is set to ' +
deleteEntry);
}
grAudit.deleteRecord();
} else {
if (this._verbose) {
gs.log('Updating audit record because deleteEntry param is set to ' +
deleteEntry + ' and newValue is set to ' + newValue);
}
grAudit.setValue('newvalue', newValue);
grAudit.update();
}
}
if (this._verbose) {
gs.log('_redactAudit() finished');
}
return true;
},
_deleteHistory: function(journalEntryData) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('_deleteHistory() running with journalEntryData: ' +
JSON.stringify(journalEntryData));
}
if (!journalEntryData.target_record_id || !journalEntryData.target_table_name) {
gs.logError(
'Journal entry ID or target table name not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_deleteHistory method of JournalRedactor Script Include');
return false;
}
var grHistorySet = new GlideRecord('sys_history_set');
grHistorySet.addQuery('id', journalEntryData.target_record_id);
grHistorySet.addQuery('table', journalEntryData.target_table_name);
grHistorySet.deleteMultiple();
if (this._verbose) {
gs.log('_deleteHistory() finished with journalEntryData: ' +
JSON.stringify(journalEntryData));
}
return true;
},
/**
* Populates journalEntryData object using a sys_journal_field record sys_id.
* @param journalEntryID {string}
* @returns {boolean}
* @private
*/
_populateJournalEntryData: function(journalEntryID) {
if (this._verbose) {
gs.log('_populateJournalEntryData() running with journalEntryID: ' + journalEntryID);
}
var grJournal = new GlideRecord('sys_journal_field');
if (grJournal.get(journalEntryID)) {
this.journalEntryData.target_record_id = grJournal.getValue('element_id');
this.journalEntryData.target_table_name = grJournal.getValue('name');
this.journalEntryData.old_journal_value = grJournal.getValue('value');
this.journalEntryData.journal_field_name = grJournal.getValue('element');
grJournal.update();
if (this._verbose) {
gs.log('_populateJournalEntryData() finished with journalEntryData: ' +
JSON.stringify(this.journalEntryData));
}
return true;
} else {
gs.logError('sys_journal_field record with sys_id ' + journalEntryID + ' not found.',
'_populateJournalEntryData method of JournalRedactor Script Include');
return false;
}
},
_JournalEntry: function() {
this.journal_entry_id = '';
this.target_record_id = '';
this.target_table_name = '';
this.old_journal_value = '';
this.journal_field_name = '';
this.new_journal_value = '';
},
type: 'JournalRedactor'
};
@thisnameissoclever
Copy link
Author

Hi, could you add a license to this?

What do you mean? This does not require a license.

@slank
Copy link

slank commented Apr 6, 2021

This might be technically or even legally true, but it's quite common for folks to explicitly license their gists. Some of us work for companies with strict open source policies. Adding a license helps us benefit from your work in our jobs! Thanks for the quick response.

@thisnameissoclever
Copy link
Author

This might be technically or even legally true, but it's quite common for folks to explicitly license their gists. Some of us work for companies with strict open source policies. Adding a license helps us benefit from your work in our jobs! Thanks for the quick response.

Ohh, you meant the gist! My bad, I thought you were referring to an application license within ServiceNow.
Sure, I will add a license of some sort to make it clear that it's free and open source as soon as I get back to my computer tonight.

@thisnameissoclever
Copy link
Author

Hi, could you add a license to this?

Hey @slank - I've added a blanket license for all of my gists. You can find it here: https://gist.github.com/thisnameissoclever/767b8a738b929a0bd943965431061c1e

@slank
Copy link

slank commented Apr 7, 2021

Thanks, Tim!

@GirlGeek98
Copy link

GirlGeek98 commented Apr 22, 2021

Anyone getting this error when they run it:

journalEntryData not ready for redaction:

@thisnameissoclever
Copy link
Author

Hey @GirlGeek98 - Is there no data after the colon in the error message?

That error occurs when you haven't specified a journal entry to redact, or when such a journal entry was not found. The line of code responsible for that error can be found on line 183 in this gist (based on the version available at the time of writing).
Please see my page on SN Pro Tips about this tool for a complete guide to usage and syntax. The example code for several of the methods show the proper way to use it. There are more examples in the article about this tool, here.

If this does not solve your problem, please post your code. Without seeing your code, I'm afraid there's probably nothing anyone can do for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment