ServiceNow is a powerful platform for IT service management, offering extensive customization through Script Includes. Implementing design patterns can significantly enhance the maintainability and scalability of your ServiceNow applications. In this blog post, we'll delve into the Repository Pattern tailored for ServiceNow, leveraging inheritance to create a robust and flexible structure for interacting with ServiceNow tables.
In ServiceNow, provisional data and non-provisional data serve distinct roles within the platform. These two data types are treated differently, especially when handling CRUD (Create, Read, Update, Delete) operations. Understanding these differences is crucial for ensuring proper data management, security, and access control when developing applications on the platform.
What is Provisional Data? Provisional data refers to the data related to the configuration and development of an application. This data is system-oriented and is typically tracked using Update Sets. Provisional data includes:
In essence, provisional data is related to the structure and design of the application itself, not the actual business or user data. It supports how the system functions and evolves but is not part of the transactional data in the system.
What is Non-Provisional Data?
Non-provisional data refers to transactional or user data that resides in the system and is directly related to business processes or user activities. This data is often created by end users or automated processes and is not tracked via update sets. Non-provisional data is permanent (until explicitly deleted) and includes information that helps drive the operational aspects of the platform.
Relation to GlideRecord and GlideRecordSecure
The distinction between provisional and non-provisional data directly impacts how developers should approach data access, especially when it comes to security and governance. This is where GlideRecord and GlideRecordSecure come into play.
GlideRecord – Unrestricted Access to Data (Potential ACL Bypass)
GlideRecordSecure – Enforced ACLs (Default for Non-Provisional Data)
The Repository Pattern is a design pattern that mediates between the domain and data mapping layers, providing a collection-like interface for accessing domain objects. In the context of ServiceNow, this pattern can abstract the complexities of interacting with different tables, promoting cleaner and more maintainable code.
By implementing the Repository Pattern through Script Includes, developers can encapsulate data access logic, making it reusable and easier to manage, especially in large-scale ServiceNow applications.
The foundation of our Repository Pattern is the Repository base class. This class encapsulates common CRUD (Create, Read, Update, Delete) operations and query mechanisms that can be shared across various ServiceNow tables.
In the base Repository class, GlideRecordSecure is used by default. This is an essential design choice because GlideRecordSecure respects the Access Control Rules (ACLs) defined in ServiceNow. ACLs play a crucial role in controlling access to sensitive data, ensuring that only authorized users can view or manipulate certain records.
If your application uses GlideRecord instead of GlideRecordSecure, you're effectively bypassing these ACLs, which can lead to unintended security vulnerabilities. For example, sensitive data in HR, Finance, or other confidential records could be exposed to users who shouldn't have access.
By defaulting to GlideRecordSecure, we ensure that our repository pattern is compliant with ServiceNow's security best practices. Developers must explicitly override this behavior if they are confident that GlideRecord is needed, and they must document the reasons why this is necessary.
/**
* @class
* Repository for interacting with ServiceNow tables.
*/
var Repository = Class.create();
Repository.prototype = {
/**
* Initializes the Repository for a given table.
* @param {string} tableName - The name of the ServiceNow table.
* @param {boolean} [overrideACL=false] - Determines whether to use GlideRecord (true) or GlideRecordSecure (false).
*/
initialize: function(tableName, overrideACL) {
this.overrideACL = overrideACL;
this.tableName = tableName;
if (!tableName) {
throw new Error("A table name must be provided to the Repository.");
}
if (overrideACL) {
this.record = new GlideRecord(this.tableName);
} else {
this.record = new GlideRecordSecure(this.tableName);
}
},
/**
* Adds an encoded query to the Repository's record.
* @param {string} query - Encoded query string.
* @returns {Repository} Returns the Repository instance for chaining.
*/
addEncodedQuery: function(query) {
this.record.addEncodedQuery(query);
return this;
},
/**
* Gets a single record based on the provided query.
* @param {string} sysId - The sysId of the record to fetch.
* @param {boolean} [isSerialized=false] - Determines if the record should be serialized.
* @returns {(GlideRecord|null|Object)} Returns the GlideRecord, serialized record, or null.
*/
getSingle: function(isSerialized) {
this.record.setLimit(1);
this.record.query();
if (this.record.next()) {
if (isSerialized) {
return this._serializeRecord(this.record);
} else {
return this.record;
}
} else {
return undefined;
}
},
/**
* Gets multiple records based on the current query.
* @param {boolean} [isSerialized=false] - Determines if the records should be serialized.
* @returns {(Array.<Object>|GlideRecord)} Returns an array of serialized records or a GlideRecord.
*/
getMultiple: function(isSerialized) {
this.record.query();
if (isSerialized) {
var results = [];
while (this.record.next()) {
results.push(this._serializeRecord(this.record));
}
return results;
} else {
return this.record;
}
},
// ... [Other CRUD and query methods as defined in the base Repository class] ...
/**
* Type definition for easier identification of this script include.
* @type {string}
*/
type: 'Repository'
};
Example:
Get single record
taskGr = new Repository('task', false).addEncodedQuery('active=true').getSingle();
Get multiple records
var taskGr = new Repository('task', false).addEncodedQuery('active=true').getMultiple() ;
Key Components
Building upon the base Repository class, we can create specialized repositories for specific tables. This is achieved through inheritance, allowing each specialized repository to extend the base functionality with table-specific logic.
Task Repository
/**
* @class
* Repository for interacting with the Task table in ServiceNow.
* @extends Repository
*/
var TaskRepository = Class.create();
TaskRepository.prototype = Object.extendsObject(Repository, {
/**
* Initializes the TaskRepository for the Task table.
* @param {boolean} [overrideACL=false] - Determines whether to use GlideRecord (true) or GlideRecordSecure (false).
*/
initialize: function(tableName, overrideACL) {
// Calling the parent's initialize method with the Task table.
var table = tableName || 'task';
Repository.prototype.initialize.call(this, table, overrideACL);
},
/**
* Retrieves tasks assigned to current user.
* @param {boolean} [isSerialized=false] - Determines if the records should be serialized.
* @returns {(Array.<Object>|GlideRecord)} Returns an array of serialized records or a GlideRecord.
*/
getByAssignedToMe: function(isSerialized) {
this.addQuery('assigned_to=' + gs.getUserID());
return this.getMultiple(isSerialized);
},
// ... [Other Task-specific methods] ...
/**
* Type definition for easier identification of this script include.
* @type {string}
*/
type: 'TaskRepository'
});
HRTaskRepository Extending further, the HRTaskRepository is specialized for the HR Task table, which inherits from the Task table.
/**
* @class
* Repository for interacting with the HR Task table in ServiceNow.
* @extends TaskRepository
*/
var HRTaskRepository = Class.create();
HRTaskRepository.prototype = Object.extendsObject(TaskRepository, {
/**
* Initializes the HRTaskRepository for the HR Task table.
* @param {boolean} [overrideACL=false] - Determines whether to use GlideRecord (true) or GlideRecordSecure (false).
*/
initialize: function(overrideACL) {
TaskRepository.prototype.initialize.call(this, 'sn_hr_core_task', overrideACL);
},
/**
* Retrieve HR tasks associated with parent case specific HR Service.
* @param {string} hrServiceId - The sys_id of the HR Service.
* @param {boolean} [isSerialized=false] - Determines if the records should be serialized.
* @returns {(Array.<Object>|GlideRecord)} Returns an array of serialized records or a GlideRecord.
*/
getByHRService: function(hrServiceId, isSerialized) {
this.addQuery('parent.ref_sn_hr_core_case.hr_service=' + hrServiceId);
return this.getMultiple(isSerialized);
},
// ... [Other HR Task-specific methods] ...
/**
* Type definition for easier identification of this script include.
* @type {string}
*/
type: 'HRTaskRepository'
});
Benefits of Inheritance
Implementing the Repository Pattern with inheritance in ServiceNow offers several advantages:
Figure 1: Inheritance structure of Repository, TaskRepository, and HRTaskRepository.
Description: The diagram illustrates the inheritance hierarchy where TaskRepository extends Repository, and HRTaskRepository extends TaskRepository. This hierarchical structure allows each repository to inherit and augment functionalities as needed.
Adopting the Repository Pattern in ServiceNow through Script Includes and leveraging inheritance provides a robust framework for managing data interactions across various tables. This approach promotes cleaner code, enhances maintainability, and scales effectively with the growing complexities of ServiceNow applications.
By structuring your repositories in this manner, you ensure a consistent and efficient way to handle data operations, paving the way for more manageable and scalable ServiceNow solutions.