Name
global.Ci
Description
CI record data inspection and mutation against the DB, including reconciliaton for referral Cis.
Script
// Discovery
/**
* Ci
* CI record data inspection and mutation against the DB, including reconciliaton for referral and related Cis.
* Provides an event handler for changes to the Ci, its network, or custom events.
* Not thread-safe due to the way we handle walking through the network of Cis.
*
* Example:
* // create a new ci object. it may or may not exist in the db already.
* var ci = new Ci({table: 'cmdb_ci_foo_bar', indices: ['name']}, {
* name: 'item 01'
* };
*
* ci.data.ip_address = '127.0.0.1'; // alter directly
*
* // create a new CI that will have our original ci as a reference field, which can be reconciled
* var referralCi = new Ci('cmdb_ci_bar_foo', {
* foo: ci, // reference the parent ci
* });
*
* referralCi.setIndices(['name']); // via method rather than through options constructor
* ci.addReferral(referralCi); // adds our referall ci to the ci.referrals array, reconciliaton now possible
* ci.write(); // insert/update the main ci along with ALL referrals
*
* Based on the the original CIData class.
*
* @since dublin
* @author roy.laurie
*/
/**
* Constructor formats: new Ci(options, data), new Ci(data)
** An example Ci options object:
* var options = {
* table: string, // the table of this ci. same as ci.table.
* indices: [string field], // the unique fields used to read() this ci uniquely in a vacuum
* referrals: [ // defines the referral schema, one per table/reference field. See Ci.prototype.defineReferrals() for details.
* { table: string,
* referenceField: string,
* write: boolean|undefined // whether this referral type should be included during write() (and reconcile)
* deleteMissing: boolean|undefined // delete missing CI records on reconcile. Default: true
* retireMissing: boolean|undefined, // if the CI is missing on reconcile, set its install status to ABSENT. Default: false
* // also, when true, ABSENT referrals will be set to INSTALLED when rediscovered.
* },
* ],
* relations: [ // defines the relation schema, one per table/nature. See Ci.prototype.defineRelations() for details
* { table: string, // the (base) table to reconcile against
* nature: string, // the cmdb_rel_type.description
* isParent: boolean|undefined, // whether this CI is the parent (true|undefined) or child (false) in the relationship. Default: true
* write: boolean|undefined // whether this relation type should be included during write() (and reconcile)
* create: boolean|undefined // whether the relation record itself should be created. Default: true
* deleteMissing: boolean|undefined, // delete missing CI records on reconcile. Default: false
* retireMissing: boolean|undefined, // if the CI is missing on reconcile, set its install status to ABSENT. Default: false
* deleteMissingRelationships: boolean|undefined // delete the missing cmdb_rel_ci on reconcile. Default: true
* },
* ],
* listeners: [ // defines which event listeners apply to this Ci (and optionally, it's connections). See Ci.prototype.setListeners() for details
* { name: string,
* eventTypes: [Ci.Event.Type]|Ci.Event.Type,
* callback: Function,
* filters: [Ci.Filter]|undefined
* },
* ]
* }
**
* @class Ci
* @param {}|string|GlideRecord|undefined options Ci.Options object can be used rather than calling other setup methods. If string only, passes to ci.table.
* @param {}|GlideRecord|undefined data Any initial primitive data to be assigned to this.data.
*/
var Ci = function(options, data) {
/* @var {string field:string|int|float|boolean|Ci|null value} Ci.this.data For public mutation and insepection of record fields/values. */
this.data = {};
/* @var string Ci.this.table Table name of record */
this.table = null;
/* @var [string field] Ci.this._indices List of index fields to match on when determining record identity */
this._indices = [];
/* @var [Ci._Referral] Ci.this._referrals List of Cis that have a reference field that links to this specific Ci. */
this._referrals = [];
/* @var [Ci._Relation] Ci.this._relations List of Cis that have a (cmdb_rel_ci) child relationship with this Ci. */
this._relations = [];
/* @var [Ci._ReferralSchema] Ci.this._referralSchema Map of referring Ci record table -> reference field of referring Ci. */
this._referralSchemas = [];
/* @var [Ci._RelationSchema] this._relationSchema Map of related cmdb_ci table -> intended cmdb_rel_type description. */
this._relationSchemas = [];
this._affectedTimes = [];
/* @var GlideRecord|null Ci.this._gr The cached glide record, if available. */
this._gr = null;
/* @var [Ci._Listener] The listeners registered against this Ci */
this._listeners = [];
/* @var {string key:string|int|boolean|float|Ci|null value} Ci.prototype._lastPublishedData A clone of Ci.this.data, made on each publish */
this._lastPublishedData = null;
// allow options to be skipped, and just data provided - swapping parameter palces.
if (typeof data !== 'undefined') {
if (data instanceof GlideRecord) { // read glide records
if (!this.read(data))
throw 'Unable to read CI: ' + data.getTableName() + ':' + data.sys_id;
} else {
this.data = data; // normal data
}
// options may either be a (string) table or a map of actual options
if (typeof options === 'string') // then it's the table name
this.table = options;
else // then it's an options map, use the define method
this.define(options);
} else if (typeof options !== 'undefined') {
if (options instanceof GlideRecord) {
if (!this.read(options))
throw 'Unable to read CI: ' + options.getTableName() + ':' + options.sys_id;
} else {
this.data = options;
}
}
};
/**
* Describes how to select Cis in a given relational network of Cis.
* @class Ci.Filter
* @param Ci.Filter.Type|Ci.Filter type References, Relations, Referrals. May also be used as a single-param copy constructor.
* @param int depth The depth at which to stop affecting connected Cis. 0: direct connections, 1: direct connections of this Ci's direct connections, etc.
*/
Ci.Filter = function(type, depth) {
if (typeof type === 'undefined')
throw 'Invalid Ci.Filter.Type passed to Ci.Filter: ' + type;
if (type instanceof Ci.Filter) {
this.type = type.type;
this.depth = type.depth;
return;
}
this.type = type;
this.depth = ( typeof depth === 'undefined' ? Number.MAX_VALUE : depth );
this.tables = null;
};
/**
* The types of connections that a Ci can be affected by.
*/
Ci.Filter.Type = {
/* Affects any references by this Ci to another */
References: 'references',
/* Affects any referrals from a Ci to this Ci */
Referrals: 'referrals',
/* Affects any relationships (cmdb_rel_ci) between a this Ci and another */
Relations: 'relations'
};
/**
* Determines whether the specified set of filters is viral or not.
* @var Function Ci.Filter.viral
* @param [Ci.Filter] filters
*/
Ci.Filter.viral = function(filters) {
if (typeof filters === 'undefined')
return false;
if (Object.prototype.toString.call(filters) !== '[object Array]') // cast to eventTypes to an array // force to an array
filters = [filters];
for (var v = 0; v < filters.length; ++v) { // look for anything with depth available (gt0)
if (filters[v].depth >= 0)
return true;
}
return false;
};
/**
* @var Ci.Filter.clone
* @param [Ci.Filter]|undefined filters
* @param {string key:string|boolean|integer value}|undefined mergeData Optionally merges fields after cloning each.
*/
Ci.Filter.clone = function(filters, mergeData) {
if (typeof mergeData === 'undefined')
mergeData = {};
var clones = [];
for (var i = 0; i < filters.length; ++i) {
var clone = new Ci.Filter(filters[i]);
// merge valid data, if any
for (var key in mergeData) {
switch(key) {
case 'tables':
clone.tables = mergeData.tables;
break;
default:
throw 'Invalid key in Ci.Filter.clone(mergeData)';
}
}
clones.push(clone);
}
return clones;
};
/**
* An enum of common sets of filters. See Ci.Filter for more details.
* @var {string name:[Ci.Filter] filters} Ci.Filters
*/
Ci.Filters = {
/* Affects all associated Cis. */
Global: [new Ci.Filter(Ci.Filter.Type.References), new Ci.Filter(Ci.Filter.Type.Referrals), new Ci.Filter(Ci.Filter.Type.Relations)],
/* Affects only Cis associated by reference to a Ci or referral from a Ci. */
Ref: [new Ci.Filter(Ci.Filter.Type.References), new Ci.Filter(Ci.Filter.Type.Referrals)],
/* Affects all Cis that are directly associated to this Ci. */
Direct: [new Ci.Filter(Ci.Filter.Type.References, 0), new Ci.Filter(Ci.Filter.Type.Referrals, 0), new Ci.Filter(Ci.Filter.Type.Relations, 0)],
/* Affects only Cis that are directly associated to this Ci by referral to this Ci or reference from this Ci. */
DirectRef: [new Ci.Filter(Ci.Filter.Type.References, 0), new Ci.Filter(Ci.Filter.Type.Referrals, 0)],
/* Does not affect any other Cis */
Local: []
};
/**
* Passed to listeners from a Ci when an event occurs against it.
*
* @class Ci.Event
* @param Ci.Event.Type eventType
* @param Ci ci
* @param {string key:string|bool|int|float|Ci|null value} data The data map to pass with the event. data.filters will affect range of event within this Ci's network.
*/
Ci.Event = function(type, ci, data) {
this.type = type;
this.ci = ci;
this.filters = ( typeof data.filters === 'undefined' ? Ci.Filters.Local : filters ); // not supported yet
if (typeof data !== 'undefined') {
for (var key in data) {
this[key] = data[key];
}
}
};
/**
* An enum of out-of-box event types. Custom event types are supported as well. Use Ci.prototype.notify() to fire events.
* @var {string name:string name} Ci.Event.Type
*/
Ci.Event.Type = {
/* Notified solely by Ci.prototype.publish(), when the Ci.prototype.data object has inspected for changes. */
OnDataChange: 'OnDataChange', // { original: {}, current: {}}
/* Notified on Ci.prototype.addReferral(), when a new referral is about to be added to this Ci. */
OnReferral: 'OnReferral', // { referralCi: Ci }
/* Notified when a referral has been deleted during reconciliation. */
OnReferralDelete: 'OnReferralDelete', // { referralCi: Ci }
/* Notified when a referral was missing during reconciliation, regardless of action taken against it. */
OnReferralMissing: 'OnReferralMissing', // { referralCi: Ci }
/* Notified on Ci.prototype.addRelation(), when a new relation is about to be added to this Ci. */
OnRelation: 'OnRelation', // { nature: nature of relationship, relationCi: Ci }
/* Notified when a relation has been deleted during reconciliation */
OnRelationDelete: 'OnRelationDelete', // { relationCi: Ci }
/* NOtified when a relation was missing during reconciliaton, regardless of action taken against it */
OnRelationMissing: 'OnRelationMissing' // { relationCi: Ci }
};
Ci.Event.typeIn = function(type, types) {
for (var i = 0, n = types.length; i < n; ++i) {
if (types[i] === type)
return true;
}
return false;
};
/**
* Describes a listener and its configuration.
* @class Ci._Listener
* @param string name The unique name to give to this listener - used to reference it in Ci.prototype.removeListener().
* @param Ci.Event.Type eventType The type of event to listen for.
* @param Function(Ci.Event) callback The callback to run against the event.
* @param [Ci.Filter]|null|undefined filters The depth at which this listener should be spread to other Cis on the network. Default: Ci.Filters.Global
*/
Ci._Listener = function(name, eventTypes, callback, filters) {
this.name = name;
this.eventTypes = eventTypes;
this.callback = callback;
this.filters = ( typeof filters === 'undefined' || filters === null ? Ci.Filters.Global : filters );
};
/**
* Describes a referral from another Ci.
* @class Ci._Referral
* @var Ci ci The CI that is making the reference to this Ci
* @var Ci._ReferralSchema schema
*/
Ci._Referral = function(ci, schema) {
/* @var Ci Ci._Referral.this.ci The CI that is making the reference to this Ci */
this.ci = ci;
this.schema = schema;
};
/**
* Describes a relationship to another Ci.
* @class Ci._Relation
* @param Ci ci The cmdb_rel_ci.child
* @param Ci._RelationSchema schema
*/
Ci._Relation = function(ci, schema) {
this.ci = ci;
this.schema = schema;
};
/**
* Describes the schema for a given table/referenceField used in reconciliation.
** Example options object:
* var options = {
* deleteMissing: boolean, // deletes CIs that were missing during reconcile. Default: true
* write: boolean, // If false, skips any write() (or reconcile) calls to the referral. Default: true
* }
**
* @class Ci._ReferralSchema
* @param string table
* @param string referenceField
* @param {string key:string|int|boolean value} options
*/
Ci._ReferralSchema = function(table, referenceField, options) {
this.table = table;
this.tables = Ci.getTableHeirarchy(table);
this.referenceField = referenceField;
if (typeof options === 'undefined')
options = {};
// options parsing
this.deleteMissing = ( typeof options.deleteMissing === 'undefined' ? true : options.deleteMissing );
this.retireMissing = ( typeof options.retireMissing === 'undefined' ? false : options.retireMissing );
this.write = ( typeof options.write === 'undefined' ? true : options.write );
};
/**
* Describes the schema for a relationship between this Ci and another, identified on table/nature.
* @class Ci._RelationSchema
* @param string table
* @param string nature
* @param boolean|undefined(true) isParent
* @parem {string key:string|int|boolean value} options
*/
Ci._RelationSchema = function(table, nature, isParent, options) {
this.table = table;
this.nature = nature;
this.isParent = ( typeof isParent === 'undefined' ? true : isParent );
this.relationshipTypeId = Ci.getNatureId(nature);
if (typeof options === 'undefined')
options = {};
// options parsing
this.deleteMissing = ( typeof options.deleteMissing === 'undefined' ? false : options.deleteMissing );
this.retireMissing = ( typeof options.retireMissing === 'undefined' ? false : options.retireMissing );
this.deleteMissingRelationship = ( typeof options.deleteMissingRelationship === 'undefined' ? true : options.deleteMissingRelationship );
this.write = ( typeof options.write === 'undefined' ? true : options.write );
this.create = ( typeof options.create === 'undefined' ? this.write : options.create );
this.reconcileExtended = options.reconcileExtended;
};
/**
* Indices for Ci.prototype._affectedTimestamp
* @var {string name:int index} Ci._Affected
*/
Ci._Affected = {
WalkCallback: 0,
Walk: 1,
Write: 2,
Reconcile: 3
};
/**
* The event debug listener callback. Displays a shallow representation of the event.data map when Ci.prototype.notify()'d.
* @var Function Ci._logEvent
* @param Event event The event created by Ci.prototype.notify()
*/
Ci._logEvent = function(event) {
// copy the event into a separate object, log, shorting any Ci objects to their toString() equivalents
var log = {};
for (var key in event) {
if (typeof event[key] === 'object' && event[key] instanceof Ci) // show only a json list of indices/values for Ci objects
log[key] = event[key].toString();
else
log[key] = event[key]; // primitive values only
}
Debug.logObject(event.type, log); // use the event type as the log key
};
Ci.CACHE_NATURE_ID = 'com.snc.discovery.Ci:nature-id';
/**
* Retrieves the cmdb_rel_type.sys_id for the specified cmdb_rel_type.description.
* Inexpensive as it uses glide caching.
* @param string nature
* @return string The sysid
*/
Ci.getNatureId = function(nature) {
var id = GlideCacheManager.get(Ci.CACHE_NATURE_ID, nature);
if (id !== null)
return ''+id;
var matches = nature.split('::');
var parentDescriptor = matches[0];
var childDescriptor = matches[1];
var gr = new GlideRecord('cmdb_rel_type');
gr.addQuery('parent_descriptor', parentDescriptor);
gr.addQuery('child_descriptor', childDescriptor);
gr.setLimit(1);
gr.query();
if (!gr.next())
throw 'Unknown nature: ' + nature;
id = ''+gr.sys_id;
GlideCacheManager.put(Ci.CACHE_NATURE_ID, nature, id);
return id;
};
/**
* Returns a list of each ancestor table, in order, including the table itself.
* @param string table
*/
Ci.getTableHeirarchy = function(table) {
var tables = [];
var tableList = GlideDBObjectManager.getTables(table);
if (tableList === null)
throw 'Table heirarchy could not be loaded: ' + table;
for (var i = 0; i < tableList.size(); ++i)
tables.push(''+tableList.get(i));
return tables;
};
/**
* Returns whether the given table extends from the parent table.
* @param string tableName: the name of the table to check
* @param string parentTableName: the name of the parent table
*/
Ci.extendsFromTable = function(tableName, parentTableName) {
var heirarchy = GlideDBObjectManager.getTables(tableName);
return heirarchy.contains(parentTableName);
};
Ci.prototype = {
define: function(options) {
if (typeof options.table !== 'undefined')
this.table = options.table;
if (typeof options.indices !== 'undefined')
this._indices = options.indices;
if (typeof options.referrals !== 'undefined')
this.defineReferrals(options.referrals);
if (typeof options.relations !== 'undefined')
this.defineRelations(options.relations);
if (typeof options.listeners !== 'undefined')
this.setListeners(options.listeners);
if (typeof options.debug !== 'undefined' && options.debug !== false) { // value may be a list of filters (inherently, true)
if (options.debug === true)
this.debug(true);
else
this.debug(true, options.debug); // the value is an array of filters
}
},
/**
* Adds a Ci object that refers to this CI, to be reconciled later.
* Any listeners within range (Ci.Filter) on either this Ci or the referral Ci will be registered on the other and its networked Cis, within its filter.
* Fires a Ci.Event.Type.OnReferral event.
* @var Function Ci.prototype.addReferral
* @param Ci ci The Ci referred from.
* @param string|undefined table The (base) table to reconcile the referred Ci against. Typically defined by Ci.prototype.defineReferral(). Default: ci.table
*/
addReferral: function(ci, table, referenceField) {
// fire OnReferral event first, before bounds checking or anything.
// other CIs may alter the schema to make them valid
if (this.listening(Ci.Event.Type.OnReferral)) {
this.notify(Ci.Event.Type.OnReferral, {
referralCi: ci
});
}
// bounds checking
if (table === 'undefined' && ci.table === null)
throw 'Ci.prototype.addReferral(): No explicit table provided and subject CI is invalid: ' + ci.toString();
// scan the schemas first to determine if we have a match
// if the table isn't specified, attempt to match against the Ci's parent tables, in order of extended class first
var ciTables = ( typeof table === 'undefined' ? ci.getTableHeirarchy() : [table] );
var referralSchema = null;
for (var t = 0; t < ciTables.length; ++t) {
var ciTable = ciTables[t];
// search for this table in the schemas
for (var i = 0; i < this._referralSchemas.length && referralSchema === null; ++i) {
var schema = this._referralSchemas[i];
if (schema.table !== ciTable)
continue;
// adopt the ref field from the schema, if not specified
if (typeof referenceField === 'undefined') {
referralSchema = schema;
} else if (schema.referenceField === referenceField || referralSchema !== null) { // ref field specified or we already set it above in a previous iteration
referralSchema = schema;
}
}
}
if (referralSchema === null) // no schema found, exception
throw 'Referral from `' + ci + '` to `' + this + '` not valid based on Ci schema.';
// check for dupes - add if not found
for (var i = 0;i < this._referrals.length; ++i) {
var referral = this._referrals[i];
if (referral.ci === ci && referral.schema === referralSchema) // dupe found
return; // nothing to do
}
this._referrals.push(new Ci._Referral(ci, referralSchema)); // add the referral
ci.data[referralSchema.referenceField] = this; // add the ref on the specified ci, if not already done manually
this._exchangeVirals(ci); // trade listeners, etc.
},
/**
* Adds a child relationship to this Ci.
* Fires a Ci.Event.Type.OnRelation event.
* @var Function Ci.prototype.addRelation
* @param Ci ci The cmdb_rel_ci.child Ci
* @param string|undefined table nature The cmdb_rel_type.description. Default: Uses relation schema (nature).
* @param string|undefined nature The (base) table to reconcile against. Default: Uses relation schema (table).
* @param boolean|undefined isChild Whether the ci is the child (TRUE) or parent (FALSE). Default: Uses relation schema (isParent).
* @throws Error If table/nature was not specified and no schema definition could be found
*/
addRelation: function(ci, table, nature, isChild) {
// fire event first
if (this.listening(Ci.Event.Type.OnRelation)) {
this.notify(Ci.Event.Type.OnRelation, {
relationCi: ci
});
}
// scan the schemas first to determine if we have a match
// if the table isn't specified, attempt to match against the Ci's parent tables, in order of extended class first
var relationSchema = null;
var ciTables = ( table ? [table] : ci.getTableHeirarchy() );
for (var t = 0; t < ciTables.length; ++t) {
var ciTable = ciTables[t];
// search for this table in the schemas
for (var i = 0; i < this._relationSchemas.length && relationSchema === null; ++i) {
var schema = this._relationSchemas[i];
if (schema.table !== ciTable)
continue;
// adpot the nature from the schema, if not specified
if (typeof nature === 'undefined') {
relationSchema = schema;
} else if (schema.nature === nature || relationSchema !== null) { // nature specified or we already set it above in a previous iteration
if (typeof isChild !== 'undefined') {
if (schema.isParent === isChild) // schema should be parent, ci should be child - or vice versa
relationSchema = schema;
} else {
relationSchema = schema;
}
}
}
}
if (relationSchema === null) // schema not found, return
return;
//check for dupes, then add
for (var i = 0; i < this._relations.length; ++i) {
var relation = this._relations[i];
if (relation.ci === ci && relation.schema === relationSchema) // dupe found
return; // nothing to do
}
this._relations.push(new Ci._Relation(ci, relationSchema)); // add the new relation
ci.addRelation(this, null, relationSchema.nature); // make a corresponding relationship on the specified ci
this._exchangeVirals(ci); // trade listeners, etc.
},
/**
* Defines a schema for reconciling Cis related to this by the related Ci's table and the nature of the relationship (cmdb_rel_type.description)
* Will ignore dupes.
* @var Function Ci.prototype.defineRelation
* @param string table The related Ci's table
* @param string nature The CMDB Relation Type (description)
* @param boolean|undefined(true) isParent Whether this CI will be the parent (TRUE) or the child (FALSE)
* @param {string option:string|int|float|boolean value}|undefined options Schema options, mostly reconciliation.
*/
defineRelation: function(table, nature, isParent, options) {
if (typeof isParent === 'undefined')
isParent = true; // need this for dupe checking
var relationSchema = new Ci._RelationSchema(table, nature, isParent, options);
// attempt to find an existing schema of the same nature and table - overwite and return if found
for (var i = 0; i < this._relationSchemas.length; ++i) {
var schema = this._relationSchemas[i];
if (schema.nature === nature && schema.table === table && schema.isParent === isParent)
return;
}
this._relationSchemas.push(relationSchema); // not found, add
},
/**
* Defines relationship schemas for this Ci.
** Example usage:
* ci.defineRelations([
* { table: 'cmdb_ci_computer',
* nature: 'Consumes::Consumed by',
* deleteMissing: true
* },
* { table: 'cmdb_ci_application',
* nature: 'Distributes::Distributed by',
* isParent: false, // default true
* }
* ]);
**
* @var Function Ci.prototype.defineRelations
* @param {string table:string nature} options
*/
defineRelations: function(options) {
for (var i = 0; i < options.length; ++i) {
var schema = options[i];
this.defineRelation(schema.table, schema.nature, schema.isParent, schema);
}
},
/**
* Describes how a referral should be reconciled against this Ci.
* @var Function Ci.prototype.defineReferral
* @param string table The (base) table to reconcile against
* @param string referenceField The table's field to reconcile this Ci against
*/
defineReferral: function(table, referenceField, options) {
var referralSchema = new Ci._ReferralSchema(table, referenceField, options);
// attempt to find an existing schema of the same nature and table - overwite and return if found
for (var i = 0; i < this._referralSchemas.length; ++i) {
var schema = this._referralSchemas[i];
if (schema.table === table && schema.referenceField === referenceField) {
this._referralSchemas[i] = referralSchema;
return;
}
}
this._referralSchemas.push(referralSchema); // not found, add
},
/**
* Describes the referral schemas for this Ci.
** Example usage:
* ci.defineReferrals({
* cmdb_ci_foo: 'owner',
* cmdb_ci_bar: 'computer',
* });
**
* @param {string table:string referenceField} options
*/
defineReferrals: function(options) {
for (var i = 0; i < options.length; ++i) {
var schema = options[i];
this.defineReferral(schema.table, schema.referenceField, schema);
}
},
/**
* Enables or disables the debug logger listeners.
* @param bool enabled Enables / disables debugging.
* @param [Ci.Filter]|undefined filters By default, debugging will be enabled on all Cis connected to this one (Ci.Filters.Global). Change to limit range. Valid only when enabled.
*/
debug: function(enabled, filters) {
var eventTypes = [];
for (var eventType in Ci.Event.Type)
eventTypes.push(eventType);
if (enabled)
this.listen('debug', eventTypes, filters, Ci._logEvent);
else
this.removeListener('debug', filters);
},
/**
* Determines whether this Ci has listeners for the given Ci.Event.Type.
* @var Function Ci.prototype.listening
* @param Ci.Event.Type eventType
*/
listening: function(eventType) {
for (var i = 0; i < this._listeners.length; ++i) {
var listener = this._listeners[i];
if (Ci.Event.typeIn(eventType, listener.eventTypes)) {
// scan each listener filter - if it has table restrictions, compare those to this.table
for (var f = 0; f < listener.filters.length; ++f) {
var filter = listener.filters[f];
if (filter.tables !== null) { // filter further by table name
for (var t = 0; t < filter.tables.length; ++i) {
if (filter.tables[t] === this.table)
return true;
}
} else {
return true;
}
}
}
}
return false;
},
/**
* Adds a new listener to this Ci. Will transfer to other connected Cis depending on filters.
* If an existing listener by the same name is found, we will skip the operation entirely.
* @var Function Ci.prototype.listen
* @param string name The globally unique name of this listener.
* @param [Ci.Event.Type]|Ci.Event.Type eventTypes The type of event to listen for
* @param [Ci.Filter]|Function(Ci.Event event)|undefined The range of Cis that this listener should affect.
* Param used as 'callback' if undefined. Default: listener's filters, or Ci.Filters.Local if not found
* @param Function(Ci.Event event) callback The function called when the Event is fired
*/
listen: function(name, eventTypes, filters, callback) {
if (Object.prototype.toString.call(eventTypes) !== '[object Array]') // cast to eventTypes to an array
eventTypes = [eventTypes];
if (typeof callback === 'undefined') { // if the last arg is skipped, assume 'filters' is the actual callback
filters = null;
callback = filters;
}
// attempt to find an existing listener by the same name - skip operation if we do
for (var i = 0; i < this._listeners.length; ++i) {
if (this._listeners[i].name === name)
return;
}
// initialize OnDataChange so that the next publish() call will be relative to this moment
if (Ci.Event.typeIn(Ci.Event.Type.OnDataChange, eventTypes) && !this.listening(Ci.Event.Type.OnDataChange))
this._lastPublishedData = this._cloneData(this.data);
var listener = new Ci._Listener(name, eventTypes, callback, filters);
this._listeners.push(listener);
if (Ci.Filter.viral(filters))
this.walk(filters, function(ci) { ci.listen(name, eventTypes, callback, filters); });
},
/**
* Adds listeners schema for this Ci.
** Example usage:
* ci.setListeners([
* { name: 'fooListener',
* eventTypes: Ci.Event.Type.OnReferral
* callback: fooListener
* filters: Ci.Filters.Global
* },
* { name: 'barListener',
* eventTypes: [Ci.Event.Type.OnRelation, Ci.Event.Type.OnReferral],
* callback: barListener,
* // filters default: Ci.Filters.Local
* }
* ]);
**
* See Ci.prototype.listen() for details.
* @var Function Ci.prototype.
* @param [{name: string, eventTypes: [Ci.Event.Type]|Ci.Event.Type, callback: Function(Ci ci), filters: [Ci.Filter]|undefined}] options
*/
addListeners: function(options) {
for (var i = 0; i < options.length; ++i) {
var listenerOpt = options[i];
this.listen(listenerOpt.name, listenerOpt.eventTypes, listenerOpt.callback, listenerOpt.filters);
}
},
/**
* Removes a listener by name.
* @var Function Ci.prototype.removeListener
* @param string name
* @param [Ci.Viralitity]|undefined filters Affects the range of the removal within this Ci's network. Default: listener's filters or Ci.Filters.Local if not found
*/
removeListener: function(name, filters) {
var wasListeningDataChange = this.listening(Ci.Event.Type.OnDataChange);
// find the listener and delete it if we do
var listener = null;
for (var i = 0; i < this._listeners.length; ++i) {
if (this._listeners[i].name === name) {
listener = this._listeners[i];
delete this._listeners[i];
break;
}
}
// reset the last publish data if we were listening to data changes but are no longer
if (wasListeningDataChange && !this.listening(Ci.Event.Type.OnDataChange))
this._lastPublishData = null;
if (typeof filters === 'undefined' && listener !== null)
filters = listener.filters;
if (Ci.Filter.viral(filters))
this.walk(filters, function(ci) { ci.removeListener(name, filters); });
},
/**
* Walks along the Cis connected to this, based on a set of filters (references, referrals, relations) and their
* depths. Performs a specified callback against each Ci found in the network of Cis.
*
* Use Ci.Filters.* for common options, or create your own. See Ci.Filter for details.
* @var Function Ci.prototype.walk
* @param [Ci.Filter] filters The set of Ci.Filter objects to filter the search by
* @param Function(Ci) callback The callback to perform for each Ci found.
*/
walk: function(filters, callback) {
var timestamp = new Date().getTime();
if (this._affect(Ci._Affected.Walk, timestamp)) {
filters = Ci.Filter.clone(filters); // clone before a walk, so that we don't modify objects
this._walk(filters, callback, timestamp);
}
},
/**
* Performs the actual function of Ci.prototype.walk(), see that for details.
* Alters the depth of the provided filters.
* @var Function Ci.prototype._walk
* @param [Ci.Filter] filters The range that this walk will continue#mutable
* @param Function(ci) callback
* @param int affectedTime Used to prevent infinite loops.
*/
_walk: function(filters, callback, affectedTime) {
var referredCis = null; // re-use this in both loops, lazy load
// perform callback against directly connected Cis
for (var v = 0; v < filters.length; ++v) {
var filter = filters[v];
if (filter.depth < 0) // past range
continue;
switch (filter.type) {
case Ci.Filter.Type.References:
referredCis = this.getReferredCis();
for (var i = 0; i < referredCis.length; ++i) {
if (referredCis[i]._affect(Ci._Affected.WalkCallback, affectedTime)) // proceed if we affected
callback(referredCis[i]);
}
break;
case Ci.Filter.Type.Referrals:
for (var i = 0; i < this._referrals.length; ++i) {
if (this._referrals[i].ci._affect(Ci._Affected.WalkCallback, affectedTime))
callback(this._referrals[i].ci);
}
break;
case Ci.Filter.Type.Relations:
for (var i = 0; i < this._relations[i].length; ++i) {
if (this._relations[i].ci._affect(Ci._Affected.WalkCallback, affectedTime))
callback(this._relations[i].ci);
}
break;
}
--filter.depth; // decrement for the following walk()
}
// perform walk against directly connected cis
for (var v = 0; v < filters.length; ++v) {
if (filter.depth < 0) // depth has already been decremented once, otherwise this would be < 1
continue;
switch (filter.type) {
case Ci.Filter.Type.References:
for (var i = 0; i < referredCis.length; ++i) {
if (referredCis[i]._affect(Ci._Affected.Walk, affectedTime))
referredCis[i]._walk(filters, callback);
}
break;
case Ci.Filter.Type.Referrals:
for (var i = 0; i < this._referrals.length; ++i) {
if (this._referrals[i].ci._affect(Ci._Affected.Walk, affectedTime))
this._referrals[i].ci._walk(filters, callback);
}
break;
case Ci.Filter.Type.Relations:
for (var i = 0; i < this._relations[i].length; ++i) {
if (this._relations[i].ci._affect(Ci._Affected.Walk, affectedTime))
this._relations[i].ci._walk(filters, callback);
}
break;
}
}
},
/**
* Notifies all listeners registered on this Ci for the given event type. Creates a Ci.Event.
* @var Function Ci.prototype.notify
* @param Ci.Event.Type eventType
* @param {} data The event data to be passed to each listener. data.filters affects range.
*/
notify: function(eventType, data) {
var event = ( typeof data === 'undefined' ? eventType : new Ci.Event(eventType, this, data) );
// find each listener that is interested and call its callback
for (var l = 0; l < this._listeners.length; ++l) {
var listener = this._listeners[l];
for (var i = 0; i < listener.eventTypes.length; ++i) {
if (listener.eventTypes[i] === eventType) {
listener.callback(event);
break;
}
}
}
if (Ci.Filter.viral(data.filters))
this.walk(data.filters, function(ci) { ci.notify(event); });
},
/**
* If listeners are registered to Ci.Event.OnDataChange: Determines if there have been any changes to the Ci.prototype.data
* object and notify()'s with an event if it has.
* @var Function Ci.prototype.publish
*/
publish: function() {
if (!this.listening(Ci.Event.Type.OnDataChange)) // nobody cares, don't fire an event
return;
// create an object with only the diff'd data from the original version
var diff = {};
var current = {};
var diffEmpty = true;
for (var key in this.data) {
if (this.data[key] !== this._lastPublishedData[key]) {
diff[key] = ( typeof this._lastPublishedData[key] === 'undefined' ? null : this._lastPublishedData[key] );
current[key] = ( typeof this.data[key] === 'undefined' ? null : this.data[key] );
if (current[key] instanceof Ci)
current[key] = current[key].toString();
diffEmpty = false;
}
}
if (diffEmpty) // then skip cloning the data over as well as firing an event
return;
this._lastPublishedData = this._cloneData(this.data); // clone data over for next call
this.notify(Ci.Event.Type.OnDataChange, {
original: diff,
current: current
});
},
/**
* Ensures that all fields are primitive values, reformatting them if necessary.
* Validates all referrals as well.
* @var Function Ci.prototype.validate
* @throws string If table does not exist.
*/
validate: function() {
if (gs.nil(this.table))
throw 'Ci.table not specified for CI';
// This function previously ensured that no field had the value
// 'undefined', throwing an exception if anything did have that value.
// Throwing an exception that doesn't get caught isn't useful,
// so I removed this code.
},
/**
* Queries the DB for either a match by indices or direct sys id.
* Populates values in to data.
* @var Function Ci.prototype.read
* @param GlideRecord|undefined Optional glide record to read from.
*/
read: function(gr) {
if (typeof gr === 'undefined') {
gr = this.getRecord();
if (gr === null)
return false;
}
var fields = gr.getFields();
for (var i = 0; i < fields.size(); ++i) {
var field = ''+fields.get(i).getName();
var value = gr.getValue(field);
if (!gs.nil(value))
this.data[field] = ''+value;
}
this.table = ''+gr.getTableName();
this.data.sys_id = ''+gr.sys_id;
this.publish();
return true;
},
/**
* Retrieves the GlideRecord for this Ci, based on sys_id or indicies. Caches the record for later use.
* @var Function Ci.prototype.getRecord
* @return GlideRecord
*/
getRecord: function() {
if (this._gr !== null) // use lazy-loaded gr instance if available
return this._gr;
this.validate(); // ensure we're ready to read()
var gr = new GlideRecord(this.searchTable || this.table);
if (!gs.nil(this.data.sys_id)) { // use sys_id to match if available
if (!this.data.sys_id || !gr.get('sys_id', this.data.sys_id))
return null;
// PRB1034997: Caching the GlideRecord can cause excessive memory usage
// and doesn't seem to provide any performance benefit.
//this._gr = gr;
return gr;
}
// match values against index that we're configured for to determine identity
for (var i = 0; i < this._indices.length; ++i) {
var index = this._indices[i];
var value = this.data[index];
if (typeof value === 'undefined')
return null;
if (value instanceof Ci)
value = value.data.sys_id;
gr.addQuery(index, value);
}
gr.setLimit(1);
gr.query();
var id = ( gr.next() ? ''+gr.sys_id : null );
gr = new GlideRecord(this.searchTable || this.table);
if (id == null)
return null;
if (!gr.get(id)) // fetch the record directly
throw 'Unable to get Ci: ' + id;
// PRB1034997: Caching the GlideRecord can cause excessive memory usage
// and doesn't seem to provide any performance benefit.
//this._gr = gr;
return gr;
},
/**
* Sets the index fields used to uniquely identify this Ci DB-wide
* @var Function Ci.prototype.setIndicies
* @param [string] indices
*/
setIndices: function(indices) {
this._indices = indices;
},
/**
* Retrieves a list of Cis that this Ci currently references in its Ci.prototype.data object.
* @var Function Ci.prototype.getReferredCis
* @return [Ci]
*/
getReferredCis: function() {
this.validate();
var referredCis = [];
for (var field in this.data) {
if (this.data[field] instanceof Ci)
referredCis.push(this.data[field]);
}
return referredCis;
},
/**
* Retrieves all Cis referred to this Ci, grouped by table name.
* @var Function Ci.prototype.getReferralsByTable
* @param string|undefined Optinoally filters which table to return for. Default: Table filter disabled
* @return {string table: Ci referrals}
*/
getReferralsByTable: function(table) {
var singleTable = ( typeof table !== 'undefined' );
var map = {}; // referral table -> referral ci
if (singleTable)
map[table] = [];
for (var i = 0; i < this._referrals.length; ++i) {
var referral = this._referrals[i];
if (singleTable && referral.schema.table !== table)
continue;
if (typeof map[referral.schema.table] === 'undefined')
map[referral.schema.table] = [];
map[referral.schema.table].push(referral.ci);
}
return ( singleTable ? map[table] : map );
},
/**
* Returns a list of CIs for the given tables.
* @param string[]|undefined tables The tables to filter on. If undefined, returns all.
* @param string referenceField
* @return Ci[]
*/
getReferrals: function(table, referenceField) {
if (typeof table === 'undefined')
table = null;
var hasReferenceField = ( typeof referenceField !== 'undefined' );
var cis = [];
for (var i = 0; i < this._referrals.length; ++i) {
var referral = this._referrals[i];
if (hasReferenceField && referral.schema.referenceField !== referenceField)
continue;
if (table === null) // accept all
cis.push(referral.ci);
else if (JSUtil.contains(referral.schema.tables, table, true)) // schema table matches
cis.push(referral.ci);
else if (JSUtil.contains(Ci.getTableHeirarchy(referral.ci.table), table)) // actual ci table matches
cis.push(referral.ci);
}
return cis;
},
/**
* Retrieve all Cis related to this Ci, grouped by table name.
* @var Function Ci.prototype.getRelationsByTable
* @param string|undefined Optinoally filters which table to return for. Default: Table filter disabled
* @return {string table: Ci referrals}
*/
getRelationsByTable: function(table) {
var singleTable = ( typeof table !== 'undefined' );
var map = {}; // referral table -> relation ci
if (singleTable)
map[table] = [];
for (var i = 0; i < this._relations.length; ++i) {
var relation = this._relations[i];
if (singleTable && relation.schema.table !== table)
continue;
if (typeof map[relation.schema.table] === 'undefined')
map[relation.schema.table] = [];
map[relation.schema.table].push(relation.ci);
}
return ( singleTable ? map[table] : map );
},
/**
* Returns a JSON representation of this Ci including all references and relationships.
* @var Function Ci.prototype.toJson
* @param int timestamp Optional parameter that is used to check if a Ci has been walked already
* @return string
*/
toJson: function(timestamp) {
var json = new JSON();
var str = {
table: this.table,
referrals: [],
relations: []
};
if (typeof this.data.sys_id !== 'undefined')
str.sys_id = this.data.sys_id;
var json = new JSON();
for (var k = 0; k < this._indices.length; ++k) {
var key = this._indices[k];
var value = this.data[key];
if (typeof value === 'undefined') {
str[key] = '(undefined)';
} else if (value === null) {
str[key] = '(null)';
} else if (value instanceof Ci) {
var ci = this.data[key];
var ciObj = {
table: ci.table
};
if (typeof ci.data.sys_id !== 'undefined')
ciObj.sys_id = ci.data.sys_id;
for (var i = 0; i < ci._indices.length; ++i) {
var index = ci._indices[i];
if (typeof ci.data[index] === 'undefined')
ciObj[index] = '(undefined)';
else if (ci.data[index] === null)
ciObj[index] = '(null)';
else if (ci.data[index] instanceof Ci)
ciObj[index] = { "Ci" : ci.data[index].table };
else
ciObj[index] = ci.data[index];
}
str[key] = ciObj;
} else {
str[key] = value;
}
}
// Set a timestamp to make sure we don't end up in an infinite loop
// subsequent recursive calls of toJson will use this timestamp
// to make sure we aren't processing the same Ci
if (gs.nil(timestamp))
timestamp = new Date().getTime();
for (var k = 0; k < this._referrals.length; ++k) {
var ref = this._referrals[k].ci;
if (ref._affect(Ci._Affected.Walk, timestamp))
str['referrals'].push(ref.toJson(timestamp));
}
for (var k = 0; k < this._relations.length; ++k) {
var rel = this._relations[k].ci;
if (rel._affect(Ci._Affected.Walk, timestamp))
str['relations'].push(rel.toJson(timestamp));
}
if (JSUtil.nil(str['referrals']))
delete str['referrals'];
if (JSUtil.nil(str['relations']))
delete str['relations'];
return str;
},
/**
* Returns a shallow JSON representation of this Ci's indices.
* @var Function Ci.prototype.toString
* @return string
*/
toString: function() {
var str = {
table: this.table
};
if (typeof this.data.sys_id !== 'undefined')
str.sys_id = this.data.sys_id;
var json = new JSON();
for (var k = 0; k < this._indices.length; ++k) {
var key = this._indices[k];
var value = this.data[key];
if (typeof value === 'undefined') {
str[key] = '(undefined)';
} else if (value === null) {
str[key] = '(null)';
} else if (typeof value == 'object') {
if (value && value.ci)
ci = value.ci;
if (value instanceof Ci) {
var ci = this.data[key];
var ciObj = {
table: ci.table
};
if (typeof ci.data.sys_id !== 'undefined')
ciObj.sys_id = ci.data.sys_id;
for (var i = 0; i < ci._indices.length; ++i) {
var index = ci._indices[i];
if (typeof ci.data[index] === 'undefined')
ciObj[index] = '(undefined)';
else if (ci.data[index] === null)
ciObj[index] = '(null)';
else if (ci.data[index] instanceof Ci)
ciObj[index] = 'Ci { ' + ci.data[index].table + ' }';
else
ciObj[index] = ci.data[index];
}
var s = json.encode(ciObj)
.replace(/"(\w+)"\:/g, "$1:") // trim quotes from things like "table": ...
.replace(/"/g, '\''); // double quotes -> single quotes
str[key] = 'Ci ' + s;
} else {
str[key] = value;
}
} else
str[key] = value;
}
var s = json.encode(str)
.replace(/"(\w+)"\:/g, "$1:")
.replace(/"/g, '\'')
.replace(/'(Ci \{.*?'\})'/g, '$1'); // don't quote nested Ci{} portions
return 'Ci ' + s;
},
/**
* Returns a shallow object of the Ci.prototype.data and Ci.prototype.table.
* All Cis referenced are printed using Ci.prototype.toString()
* @var Function Ci.prototype.toShallowObj
* @return {}
*/
toShallowObj: function() {
var obj = {
table: this.table
};
for (var key in this.data) {
var value = this.data[key];
if (typeof value === 'undefined')
obj[key] = undefined;
else if (value instanceof Ci)
obj[key] = value.toString();
else
obj[key] = value;
}
return obj;
},
/**
* Writes only this Ci object to the DB.
* @var Function Ci.prototype.update
* @param options Named parameters for this method.
** Options parameters
* 'validate' (boolean): TRUE if validate() should be called (default).
**
*/
update: function(options) {
if (typeof options === 'undefined' || typeof options.validate === 'undefined' || options.validate === true)
this.validate();
var gr = this.getRecord();
if (gr === null)
gr = new GlideRecord(this.table);
if ((typeof this.searchTable == 'string') && ('' + gr.sys_class_name) && (this.table != ('' + gr.sys_class_name)))
new DiscoveryFunctions().reclassify(gr, this.table);
// update all fields
for (var field in this.data) {
// ignore auto-populated fields
if (field === 'first_discovered' || field === 'start_date')
continue;
// Ignore fields that we don't have a value for.
if (this.data[field] === undefined)
continue;
var refCi = this.data[field];
if ((refCi != this) && (refCi instanceof Ci)) {
if (gs.nil(refCi.data.sys_id)) {
// if the referenced Ci is not dependent on any other Cis to update(), call it directly
if (refCi._canUpdate()) {
refCi.update();
} else {
Debug.logObject('Failed Updating CI', this.toShallowObj());
Debug.logObject('Blocked On Referenced CI', refCi.toShallowObj());
throw 'Ci.data.' + field + '.data.sys_id is invalid. Cannot reference on update()';
}
}
gr.setValue(field, this.data[field].data.sys_id);
} else if (gr.getValue(field) !== this.data[field]) {
// sometimes we think setting the value is a harmless no-op, but something else
// has set the value correctly in the meantime. don't set the value unless we
// think we have a better value.
gr.setValue(field, this.data[field]);
}
}
gr.update();
// PRB1034997: Caching the GlideRecord can cause excessive memory usage
// and doesn't seem to provide any performance benefit.
//this._gr = gr;
this.data.sys_id = ''+gr.sys_id;
this.publish();
},
_canUpdate: function() {
for (var field in this.data) {
if (this.data[field] && (this.data[field] instanceof Ci) && gs.nil(this.data[field].data.sys_id))
return false;
}
return true;
},
/**
* Inserts/Updates for this Ci and all others in its network, reconciling relationships and referrals where defined.
* @var Function Ci.prototype.write
*/
write: function() {
var affectedTime = new Date().getTime();
if (this._affect(Ci._Affected.Write, affectedTime)) {
// immediately update() any Ci that isn't dependent on another Ci
this.walk(Ci.Filters.Global, function(ci) {
if (ci._canUpdate())
ci.update();
});
this._write(affectedTime);
}
if (this._affect(Ci._Affected.Reconcile, affectedTime))
this._reconcile(affectedTime);
},
/**
* Writes the CIs to the DB as well as all referrals.
* Updates all CIs first, then reconciles them all, if necessary.
* @var Function Ci.prototype._write
* @param int writeTime
*/
_write: function(affectedTime) {
var updated = false;
this.validate();
// update if we can
if (this._canUpdate()) {
this.update({ validate: false });
updated = true;
}
// Step 1: first, write cis that we refer to, so that we have the necessary sysids
var referredCis = this.getReferredCis();
for (var i = 0; i < referredCis.length; ++i) {
var refCi = referredCis[i];
if (refCi._affect(Ci._Affected.Write, affectedTime))
refCi._write(affectedTime);
}
// update this ci, if necessary
if (!updated)
this.update();
// Step2: write cis that refer to this ci, now that we have a sysid on this ci
// NOTE:
// This function is recursive.
// At a given level of recursion, it is possible that the ci's that refer
// to this ci do NOT have their sys_id defined when refCi._write() called
// and schema.isParent is false.
// The subsequent call to _createRelationships() function within this level
// of the recursive call shall therefore return without creating a relationship.
// By the time the top level recursive write completes all ci's that refer to
// this ci shall exist (sys_id defined), ensuring that the relationship
// is created successfully.
for (var i = 0; i < this._referrals.length; ++i) {
var refCi = this._referrals[i].ci;
var schema = this._referrals[i].schema;
if (schema.write && refCi._affect(Ci._Affected.Write, affectedTime))
refCi._write(affectedTime);
}
// Step3: write related cis
for (var i = 0; i < this._relations.length; ++i) {
var relationCi = this._relations[i].ci;
var schema = this._relations[i].schema;
if (schema.write && relationCi._affect(Ci._Affected.Write, affectedTime))
relationCi._write(affectedTime);
}
// Step4: Create [cmdb_rel_ci] when both Parent and Child ci records exist
this._createRelationships();
},
_createRelationships: function() {
// create a relationship record, if it doesn't exist already, for each relation
for (var i = 0; i < this._relations.length; ++i) {
var relation = this._relations[i];
var schema = relation.schema;
if (!schema.create)
continue;
// create a relationship if one does not exist
var relGr = new GlideRecord('cmdb_rel_ci');
relGr.addQuery('type', schema.relationshipTypeId);
var parent;
var child;
if (schema.isParent) {
parent = this.data.sys_id;
child = relation.ci.data.sys_id;
} else {
child = this.data.sys_id;
parent = relation.ci.data.sys_id;
}
// skip invalid relationships
if (JSUtil.nil(parent) || JSUtil.nil(child))
continue;
relGr.addQuery('parent', parent);
relGr.addQuery('child', child);
// skip if there is a relationship already
relGr.setLimit(1);
relGr.query();
if (relGr.next())
continue;
// we don't have a relationship, create one
relGr = new GlideRecord('cmdb_rel_ci');
relGr.setValue('type', schema.relationshipTypeId);
if (schema.isParent) {
relGr.setValue('parent', this.data.sys_id);
relGr.setValue('child', relation.ci.data.sys_id);
} else {
relGr.setValue('parent', relation.ci.data.sys_id);
relGr.setValue('child', this.data.sys_id);
}
relGr.insert();
}
},
/**
* Reconciles referrals and relations.
* @var Function Ci.prototype._reconcile
*/
_reconcile: function(affectedTime) {
// reconcile for references
var referredCis = this.getReferredCis();
for (var i = 0; i < referredCis.length; ++i) {
var refCi = referredCis[i];
if (refCi._affect(Ci._Affected.Reconcile, affectedTime))
refCi._reconcile(affectedTime);
}
// reconcile for referrals
for (var i = 0; i < this._referrals.length; ++i) {
var refCi = this._referrals[i].ci;
var schema = this._referrals[i].schema;
if (schema.write && refCi._affect(Ci._Affected.Reconcile, affectedTime))
refCi._reconcile(affectedTime);
}
// reconcile for relations
for (var i = 0; i < this._relations.length; ++i) {
var relationCi = this._relations[i].ci;
var schema = this._relations[i].schema;
if (schema.write && relationCi._affect(Ci._Affected.Reconcile, affectedTime))
relationCi._reconcile(affectedTime);
}
this._reconcileReferrals();
this._reconcileRelations();
},
/**
* Reconciles referred CIs by unique key sets.
* @var Function Ci.prototype._reconfileReferrals
*/
_reconcileReferrals: function() {
var listeningDelete = this.listening(Ci.Event.Type.OnReferralDelete);
var listeningMissing = this.listening(Ci.Event.Type.OnReferralMissing);
for (var s = 0; s < this._referralSchemas.length; ++s) {
var referralSchema = this._referralSchemas[s];
if (!referralSchema.write)
continue;
var table = referralSchema.table;
var referrals = this.getReferrals(table);
// iterate through all current referrals in db
var tableGr = new GlideRecord(table);
tableGr.addQuery(referralSchema.referenceField, this.data.sys_id);
tableGr.query();
while(tableGr.next()) {
// find the corresponding Ci in our new dataset
var referralCi = null;
for (var r = 0; r < referrals.length && referralCi === null; ++r) {
var ci = referrals[r];
referralCi = ci;
// match on indices
for (var i = 0; i < ci._indices.length; ++i) {
var index = ci._indices[i];
var grVal = ''+tableGr.getValue(index);
var ciVal = ci.data[index];
// use the sysID for Ci objects
if (ciVal instanceof Ci)
ciVal = ciVal.data.sys_id;
// Convert ciVal to a string in the case that it was not a number. The conversion is not
// applied for the numbers because for number x, x == 'x.0' but 'x' != 'x.0'.
// Fixes null != 'null'.
if (typeof ciVal != 'number')
ciVal = '' + ciVal;
// reset the referralCi variable if the values don't match
if (ciVal != grVal) {
referralCi = null;
break;
}
}
}
// referrral not found in new dataset. delete this record to reconcile.
if (referralCi === null) {
var missingCi = null;
if (listeningDelete || listeningMissing) {
missingCi = new Ci(tableGr); // for notifies later on
}
if (listeningMissing) {
this.notify(Ci.Event.Type.OnReferralMissing, {
referralCi: missingCi
});
}
if (referralSchema.deleteMissing) {
tableGr.deleteRecord();
if (listeningDelete) {
this.notify(Ci.Event.Type.OnReferralDelete, {
referralCi: missingCi
});
}
} else if (referralSchema.retireMissing) {
tableGr.install_status = 100;
tableGr.update();
}
continue;
}
// if the schema is set to mark missing as ABSENT, reverse it if we rediscover a referral
if (referralSchema.retireMissing && tableGr.install_status == 100) {
tableGr.install_status = 1;
tableGr.update();
}
referralCi.data.sys_id = ''+tableGr.sys_id; // set the sysid now that we've found it
}
}
},
/**
* @var Function Ci.prototype._reconcileRelations
*/
_reconcileRelations: function() {
var relatedToType, gr;
var listeningMissing = this.listening(Ci.Event.Type.OnRelationMissing);
var listeningDelete = this.listening(Ci.Event.Type.OnRelationDelete);
// for each relationship schema definition, reconcile all referral cis of that schema
for (var s = 0; s < this._relationSchemas.length; ++s) {
var schema = this._relationSchemas[s];
if (!schema.write) // skip this schema
continue;
// query for all CIs currently related to this Ci, by the same nature/role
var relatedGr = new GlideRecord('cmdb_rel_ci');
relatedGr.addQuery('type', schema.relationshipTypeId);
if (schema.isParent) {
relatedGr.addQuery('parent', this.data.sys_id);
if (!schema.reconcileExtended)
relatedGr.addQuery('child.sys_class_name', schema.table);
relatedToType = 'child';
} else {
relatedGr.addQuery('child', this.data.sys_id);
if (!schema.reconcileExtended)
relatedGr.addQuery('parent.sys_class_name', schema.table);
relatedToType = 'parent';
}
relatedGr.query();
grLoop: while (relatedGr.next()) {
if (schema.reconcileExtended) {
gr = new GlideRecord(relatedGr[relatedToType].sys_class_name);
if (!gr.instanceOf(schema.table))
continue;
}
// find each ci for this schema and attempt to match it to the query result
// delete any results which we can't find a match for
for (var r = 0; r < this._relations.length; ++r) {
var relation = this._relations[r];
if (relation.schema !== schema) // not current schema, skip
continue;
if (schema.isParent) {
if ((''+relatedGr.child.sys_id) === relation.ci.data.sys_id) // we found a match, next
continue grLoop;
} else { // schema is child
if ((''+relatedGr.parent.sys_id) === relation.ci.data.sys_id) // we found a match, next
continue grLoop;
}
}
// if we're here, it means that this gr has no match. determine whether we delete it or not by schema config
var missingCi = new Ci(( schema.isParent ? relatedGr.child : relatedGr.parent ));
if (schema.deleteMissing) {
relatedGr.child.deleteRecord();
relatedGr.deleteRecord();
if (listeningDelete) {
this.notify(Ci.Event.Type.OnRelationDelete, {
relationCi: missingCi
});
}
} else {
if (schema.deleteMissingRelationship)
relatedGr.deleteRecord();
if (schema.retireMissing) {
missingCi.data.install_status = 100;
missingCi.write();
}
}
if (listeningMissing) {
this.notify(Ci.Event.Type.OnRelationMissing, {
relationCi: missingCi
});
}
}
}
},
/**
* Retrieves a list of table names that this Ci's table extends from, in order.
* @return string[]
*/
getTableHeirarchy: function() {
return Ci.getTableHeirarchy(this.table);
},
/**
* Determines whether this Ci's table is the same as or extends from the specified parent tabe.
* @return boolean TRUE if extends, FALSE if not.
*/
extendsFromTable: function(parentTable) {
var heirarchy = this.getTableHeirarchy();
return JSUtil.contains(heirarchy, parentTable);
},
/**
* Determines whether the specified Ci._Affect type has been registered against the specified timestamp.
* If not registered, it will register the time and return TRUE. Otherwise, it will ignore and return FALSE.
* @var Function Ci.prototype._affect
* @param Ci._Affected type
* @param int timestamp
* @returns TRUE if the type is newly affected, FALSE if it has already been.
*/
_affect: function(type, timestamp) {
if (typeof this._affectedTimes[type] === 'undefined' || this._affectedTimes[type] !== timestamp) {
this._affectedTimes[type] = timestamp;
return true;
}
return false;
},
/**
* Performs a simple clone of Ci.prototype.data objects.
* @var Function Ci.prototype._cloneData
* @param {} source
* @return {}
*/
_cloneData: function(source) {
var data = {};
for (var key in source) {
data[key] = source[key];
}
return data;
},
/**
* Exchanges viral data (listeners) between this Ci and the specified, extending through the network.
* @var Function Ci.prototype._exchangeVirals
* @param Ci ci
* @param boolean halfDuplex Call the specified Ci's _exchangeVirals against this afterwards.
*/
_exchangeVirals: function(ci, halfDuplex) {
// transfer listeners from this ci to target
for (var l = 0; l < this._listeners.length; ++l) {
var listener = this._listeners[l];
if (Ci.Filter.viral(listener.filters))
ci.listen(listener.name, listener.eventTypes, listener.filters, listener.callback);
}
if (typeof halfDuplex === 'undefined')
ci._exchangeVirals(this, true);
},
/**
* Set all non-system fields(field names do not start with sys_) in data part to undefined. This function
* is used to track the fields that were modified by the code which creates the Ci. It is possible that
* the data that we have when we create the Ci was different from the data that we have in DB when we
* want to write the Ci in DB. We need some ways to identify which fields are modified in the same code and try
* to prevent over-write the fields that we didn't work on them.
*/
setDataUndefined: function() {
for (var field in this.data) {
if (this.data[field] instanceof Ci)
continue;
if (field.startsWith('sys_'))
continue;
this.data[field] = undefined;
}
}
};
Sys ID
a5e15ad037110100dcd48c00dfbe5d57