Name

global.DataExtractionImpl

Description

No description available

Script

var DataExtractionImpl = Class.create();
DataExtractionImpl.prototype = {
  initialize: function() {
      this.DATA_EXTRACTION_ERRORS = {};
      this.PROCESS = {};
      this.UTIL = new DataExtractionUtil();
      this.CONSTANTS = new DataExtractionConstants();
  },

  //Entry point to get training data
  //Inputs: (JSON) required. contract describing the use case and other features to query
  //        (JSON) optional. pagination_data describe the pagination information. empty in the first call
  getTrainingData: function(contract, pagination_data) {

      try {

          var contractInfo = new DataExtractionContractValidation().isJSONValid(contract);
          if (!contractInfo.isValid)
              return contractInfo.errors;

          // converting just the keys to upper case to avoid errors while validating encodedqueries (case sensitive)
          this.PROCESS.contract = this.UTIL.ConvertContractKeysToUpperCase(contract);
          this.PROCESS.useCase = contractInfo.useCase;

          this.initializeProcess(pagination_data);
          if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
              this.wrapUpProcess();
              return this.DATA_EXTRACTION_ERRORS;
          }
          var includeFeaturesAndLabels = true;
          this.processDataFromTargetTable(includeFeaturesAndLabels);
          if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
              this.wrapUpProcess();
              return this.DATA_EXTRACTION_ERRORS;
          }

          if (this.PROCESS.useCase != "SINGLE_TABLE") {
              this.PROCESS.source_sys_ids = {};
              this.processDataFromSource(includeFeaturesAndLabels);
              if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
                  this.wrapUpProcess();
                  return this.DATA_EXTRACTION_ERRORS;
              }
          }

          //processing other features from target after source validation to avoid processing possible records deleted because of inner join
          if (Object.keys(this.PROCESS.extractedData).length > 0 &&
              this.UTIL.isValidJSONArrayKey(this.PROCESS.contract.TARGET, "OTHER_FEATURES"))
              this.processOtherFeatures(this.PROCESS.contract.TARGET.TABLE,
                  this.PROCESS.contract.TARGET.OTHER_FEATURES,
                  Object.keys(this.PROCESS.extractedData));

          this.wrapUpProcess();

      } catch (err) {
          this.addError("DATA_ERROR", err);
          this.wrapUpProcess();
          return this.DATA_EXTRACTION_ERRORS;
      }

      if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS))
          return this.DATA_EXTRACTION_ERRORS;
      else
          return this.getExtractedData(includeFeaturesAndLabels);

  },

  getRecords: function(pagination_data) {
      try {
          this.initializeProcess(pagination_data);
          if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
              this.wrapUpProcess();
              return this.DATA_EXTRACTION_ERRORS;
          }
          var includeFeaturesAndLabels = false;
          this.processDataFromTargetTable(includeFeaturesAndLabels);
          if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
              this.wrapUpProcess();
              return this.DATA_EXTRACTION_ERRORS;
          }

          if (this.PROCESS.useCase != "SINGLE_TABLE") {
              this.PROCESS.source_sys_ids = {};
              this.processDataFromSource(includeFeaturesAndLabels);
              if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS)) {
                  this.wrapUpProcess();
                  return this.DATA_EXTRACTION_ERRORS;
              }
          }

          this.wrapUpProcess();

      } catch (err) {
          this.addError("DATA_ERROR", err);
          this.wrapUpProcess();
          return this.DATA_EXTRACTION_ERRORS;
      }

      if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS))
          return this.DATA_EXTRACTION_ERRORS;
      else
          return this.getExtractedData(includeFeaturesAndLabels);
  },

  getRecordCount: function(contract, minRecords) {
      try {
          var contractInfo = new DataExtractionContractValidation().isJSONValid(contract);
          if (!contractInfo.isValid)
              return contractInfo.errors;
          this.PROCESS.contract = this.UTIL.ConvertContractKeysToUpperCase(contract);
          var target = this.PROCESS.contract.TARGET;
          this.PROCESS.useCase = contractInfo.useCase;
          var maxNumberOfRecords = this.CONSTANTS.MAX_NUMBER_RECORDS_FOR_TRAINING;
          if (this.UTIL.isValidJSONKey(this.PROCESS.contract.TARGET, "MAX_NUMBER_RECORDS")) {
              maxNumberOfRecords = this.PROCESS.contract.TARGET.MAX_NUMBER_RECORDS;
          }
          var recordCount = 0;
          if (this.PROCESS.useCase == "SINGLE_TABLE") {
              var encodedQuery = this.UTIL.isValidJSONKey(target, "ENCODED_QUERY") ?
                  target.ENCODED_QUERY : "";
              if (this.PROCESS.info.apply_domain_separation && gr.isValidField("sys_domain")) {
                  encodedQuery = encodedQuery + "^" + "sys_domain=" + this.PROCESS.contract.DOMAIN_SYS_ID;
              }
              var agg = new GlideAggregate(target.TABLE);
              if (encodedQuery != "") agg.addEncodedQuery(encodedQuery);
              agg.addAggregate('COUNT');
              agg.setGroup(false);
              agg.query();
              if (agg.next()) {
                  recordCount = parseInt(agg.getAggregate('COUNT'), 0);
              }
          } else {
              var response = this.getRecords();
              if (response && response.hasOwnProperty(this.CONSTANTS.ERROR_TYPE_KEY)) return response;
              var paginationData = response["pagination_data"];
              var current_offset = paginationData["offset"];
              while (current_offset != -1) {
                  if (minRecords != null && paginationData.total_extracted_records >= parseInt(minRecords, 10)) break;
                  response = this.getRecords(paginationData);
                  paginationData = response["pagination_data"];
                  current_offset = paginationData["offset"];
              }
              recordCount = paginationData.total_extracted_records;
          }
          return maxNumberOfRecords < recordCount && maxNumberOfRecords != -1 ? maxNumberOfRecords : recordCount;
      } catch (err) {
          this.addError("DATA_ERROR", err);
          this.wrapUpProcess();
          return this.DATA_EXTRACTION_ERRORS;
      }
  },

  initializeProcess: function(pgn) {
      this.PROCESS.info = {};
      this.PROCESS.info.batch_start_time = new GlideDateTime().getDisplayValue();
      this.PROCESS.info.batch_extracted_attachments = 0;
      this.PROCESS.info.batch_extracted_records = 0;
      this.PROCESS.info.max_pagination_records = this.CONSTANTS.MAX_PAGINATION_RECORDS;

      //If pagination_data is null or empty is an indication of first run, 
      //therefore table rotation needs to be check to understand pagination
      if (this.UTIL.isNullOrEmpty(pgn)) {
          this.PROCESS.pgn = {};
          this.PROCESS.pgn.batch_count = 1;
          if (this.UTIL.isValidJSONKey(this.PROCESS.contract.TARGET, "MAX_NUMBER_RECORDS"))
              this.PROCESS.pgn.total_number_records_to_process = this.PROCESS.contract.TARGET.MAX_NUMBER_RECORDS;
          else
              this.PROCESS.pgn.total_number_records_to_process = this.CONSTANTS.MAX_NUMBER_RECORDS_FOR_TRAINING;


          this.PROCESS.pgn.table_rotation_info = {};
          this.PROCESS.pgn.total_extracted_records = 0;
          this.PROCESS.pgn.total_extracted_attachments = 0;
          this.setPagination();

      } else {
          this.PROCESS.pgn = pgn;
          this.PROCESS.pgn.batch_count++;
          if (this.PROCESS.pgn.table_rotation_info.active)
              this.resetPaginationForRotationValues();
          else
              this.resetPaginationValues();
      }

      if (this.UTIL.isValidJSONKey(this.PROCESS.contract.TARGET, "EXPECTED_NUMBER_RECORDS"))
          this.PROCESS.info.expected_number_records = this.PROCESS.contract.TARGET.EXPECTED_NUMBER_RECORDS;
      else
          this.PROCESS.info.expected_number_records = this.PROCESS.pgn.total_number_records_to_process;

      if (GlideDomainSupport.isDataOrProcessSeparationEnabled() && this.UTIL.isValidJSONKey(this.PROCESS.contract, "DOMAIN_SYS_ID")) {
          this.PROCESS.info.apply_domain_separation = true;
      } else {
          this.PROCESS.info.apply_domain_separation = false;
      }
  },

  setPagination: function() {
      this.PROCESS.pgn.table_rotation_info.rotation_schedule = "";
      this.PROCESS.pgn.table_rotation_info.rotation_offset = 0;
      this.PROCESS.pgn.offset = 0;
      this.PROCESS.pgn.limit = this.CONSTANTS.MAX_PAGINATION_RECORDS;

      if (this.UTIL.isValidJSONKey(this.PROCESS.contract, "AUTO_ROTATION_DISCOVERY") &&
          this.PROCESS.contract.AUTO_ROTATION_DISCOVERY.toUpperCase() == "FALSE") {
          this.PROCESS.pgn.table_rotation_info.active = false;

      } else {
          var tablesInContract = this.UTIL.getTableListFromContract(this.PROCESS.contract, this.PROCESS.useCase);
          var rotationGroupId = this.UTIL.getRotationSysId(tablesInContract);

          if (gs.nil(rotationGroupId))
              this.PROCESS.pgn.table_rotation_info.active = false;

          else {
              this.PROCESS.pgn.table_rotation_info.active = true;
              this.PROCESS.info.rtt = {};
              this.PROCESS.info.rtt.limit = this.CONSTANTS.MAX_PAGINATION_RECORDS;

              var targetInfo = this.UTIL.getTargetInfoForRotation(this.PROCESS.contract.TARGET.TABLE,
                  this.UTIL.isValidJSONKey(this.PROCESS.contract.TARGET, "ENCODED_QUERY") ?
                  this.PROCESS.contract.TARGET.ENCODED_QUERY : "");

              if (this.PROCESS.pgn.total_number_records_to_process == -1 ||
                  targetInfo.record_count < this.PROCESS.pgn.total_number_records_to_process)
                  this.PROCESS.pgn.total_number_records_to_process = targetInfo.record_count;

              var scheduleInfo = this.UTIL.getNextRotationSchedule(rotationGroupId, targetInfo.min_date);
              this.setRotationScheduleInfo(scheduleInfo);
          }
      }
  },


  setRotationScheduleInfo: function(scheduleInfo) {
      this.PROCESS.info.rtt.encoded_query = scheduleInfo.encoded_query;
      this.PROCESS.pgn.table_rotation_info.rotation_schedule = scheduleInfo.sys_id;
  },

  resetPaginationValues: function() {
      this.PROCESS.pgn.offset = this.PROCESS.pgn.offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;

      if (this.isLastBatch())
          this.PROCESS.pgn.limit = this.setRemainingAsLimit(this.PROCESS.pgn.offset);
      else
          this.PROCESS.pgn.limit = this.PROCESS.pgn.offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;
  },

  setRemainingAsLimit: function(offset) {
      return offset + (this.PROCESS.pgn.total_number_records_to_process - offset);
  },


  resetPaginationForRotationValues: function() {
      this.PROCESS.info.rtt = {};

      var scheduleInfo;
      //table_rotation_info.rotation_offset -1 = change in shard
      if (this.PROCESS.pgn.table_rotation_info.rotation_offset == -1) {
          scheduleInfo = this.UTIL.getNextRotationSchedule(this.PROCESS.pgn.table_rotation_info.rotation_schedule);
          this.PROCESS.pgn.table_rotation_info.rotation_offset = 0;
      } else {
          scheduleInfo = this.UTIL.getRotationSchedule(this.PROCESS.pgn.table_rotation_info.rotation_schedule);
          this.PROCESS.pgn.table_rotation_info.rotation_offset = this.PROCESS.pgn.table_rotation_info.rotation_offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;
          this.PROCESS.pgn.offset = this.PROCESS.pgn.offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;
      }

      this.setRotationScheduleInfo(scheduleInfo);

      if (this.isLastBatch()) {
          this.PROCESS.info.rtt.limit = this.setRemainingAsLimit(this.PROCESS.pgn.table_rotation_info.rotation_offset);
          this.PROCESS.pgn.limit = this.setRemainingAsLimit(this.PROCESS.pgn.offset);
      } else {
          this.PROCESS.info.rtt.limit = this.PROCESS.pgn.table_rotation_info.rotation_offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;
          this.PROCESS.pgn.limit = this.PROCESS.pgn.offset + this.CONSTANTS.MAX_PAGINATION_RECORDS;
      }
  },

  isLastBatch: function() {

      return ((this.PROCESS.pgn.offset + this.CONSTANTS.MAX_PAGINATION_RECORDS) >=
          this.PROCESS.pgn.total_number_records_to_process);
  },
  isLastBatchForRotation: function() {

      return ((this.PROCESS.pgn.table_rotation_info.rotation_offset + this.PROCESS.info.batch_total_number_target_records) >=
          this.PROCESS.info.rtt.shard_total_number_records_to_process);
  },


  //Prepare the extracted data to match the GraphQL contract expected result
  getExtractedData: function(includeRecords) {
      var extractedDataGQL = {};
      extractedDataGQL.tag = this.UTIL.isValidJSONKey(this.PROCESS.contract, "TAG") ? this.PROCESS.contract.TAG : "";
      extractedDataGQL.pagination_data = this.PROCESS.pgn;
      if (includeRecords) {
          extractedDataGQL.records = Object.keys(this.PROCESS.extractedData).map(function(key) {
              return this.PROCESS.extractedData[key];
          }, this);
      }

      if (this.PROCESS.info.expected_number_records != -1 && this.PROCESS.pgn.total_extracted_records >= this.PROCESS.info.expected_number_records) {
          extractedDataGQL.records = extractedDataGQL.records.slice(0, this.PROCESS.info.expected_number_records);
      }

      return extractedDataGQL;
  },

  wrapUpProcess: function() {

      this.PROCESS.info.batch_extracted_records = Object.keys(this.PROCESS.extractedData).length;
      this.PROCESS.info.total_extracted_attachments = this.PROCESS.info.batch_extracted_attachments + this.PROCESS.pgn.total_extracted_attachments;
      this.PROCESS.pgn.total_extracted_records = this.PROCESS.info.batch_extracted_records + this.PROCESS.pgn.total_extracted_records;
      this.PROCESS.pgn.total_extracted_attachments = this.PROCESS.info.total_extracted_attachments;
      this.PROCESS.info.batch_end_time = new GlideDateTime().getDisplayValue();

      if (this.PROCESS.pgn.table_rotation_info.active && this.isLastBatchForRotation()) {
          this.PROCESS.pgn.table_rotation_info.rotation_offset = -1;
          this.PROCESS.pgn.offset = this.PROCESS.pgn.offset + this.PROCESS.info.batch_total_number_target_records;
      }

      //adding validation for this.PROCESS.pgn.expected_number_records
      if (this.isLastBatch() || (this.PROCESS.info.expected_number_records != -1 && this.PROCESS.pgn.total_extracted_records >= this.PROCESS.info.expected_number_records))
          this.PROCESS.pgn.offset = -1;

      var debugMessage = " Use case " + this.PROCESS.useCase +
          " pagination " + JSON.stringify(this.PROCESS.pgn, null, 2) + " info " + JSON.stringify(this.PROCESS.info, null, 2);

      if (!this.UTIL.isNullOrEmpty(this.DATA_EXTRACTION_ERRORS))
          this._debug("Data Extraction has failed. " + debugMessage + ". Errors" + JSON.stringify(this.DATA_EXTRACTION_ERRORS, null, 2));

      else
          this._debug("Data Extraction has completed." + debugMessage);
  },


  //process Features and Labels (if applicable) from the target Table
  processDataFromTargetTable: function(includeFeaturesAndLabels) {
      if (!this.UTIL.isJSONObject(this.PROCESS.contract.TARGET)) {
          this.addError("DATA_ERROR", "Target was not provided");
          return;
      }
      var target = this.PROCESS.contract.TARGET;
      this.PROCESS.extractedData = {};

      try {
          var gr = new GlideRecord(target.TABLE);

          var eq = this.UTIL.isValidJSONKey(target, "ENCODED_QUERY") ?
              this.getRefinedEncodedQuery(target.ENCODED_QUERY, gr) : this.getRefinedEncodedQuery("", gr);

          if (!gs.nil(eq))
              gr.addEncodedQuery(eq);

          if (this.UTIL.isValidJSONKey(target, "ORDER")) {
              if (target.ORDER == "ASC")
                  gr.orderBy('sys_created_on');
              else if (target.ORDER == "DESC")
                  gr.orderByDesc('sys_created_on');
          }

          if (this.PROCESS.pgn.table_rotation_info.active)
              gr.chooseWindow(this.PROCESS.pgn.table_rotation_info.rotation_offset, this.PROCESS.info.rtt.limit);
          else
              gr.chooseWindow(this.PROCESS.pgn.offset, this.PROCESS.pgn.limit);
          gr.query();

          this.setTotalNumberRecordsToProcess(gr.getRowCount());
          this.PROCESS.info.batch_total_number_target_records = 0;

          while (gr.next()) {
              if (this.PROCESS.pgn.total_extracted_records + this.PROCESS.info.batch_total_number_target_records >= this.PROCESS.pgn.total_number_records_to_process) break;
              this.PROCESS.info.batch_total_number_target_records++;
              var recordSysId = gr.getValue('sys_id');
              var labelsData = [];
              var featuresData = [];

              if (includeFeaturesAndLabels) {
                  if (this.UTIL.isValidJSONKey(target, "LABELS") &&
                      target.LABELS.length > 0)
                      labelsData = this.UTIL.getSingleValueFields(gr, target.LABELS);

                  if (this.UTIL.isValidJSONKey(target, "FEATURES") &&
                      target.FEATURES.length > 0)
                      featuresData = this.UTIL.getSingleValueFields(gr, target.FEATURES);

                  this.PROCESS.extractedData[recordSysId] = {
                      target_sys_id: recordSysId,
                      source_sys_id: null,
                      sys_created_on: gr.getValue('sys_created_on'),
                      features: featuresData,
                      labels: labelsData,
                      other_features: []
                  };
              } else {
                  this.PROCESS.extractedData[recordSysId] = {
                      target_sys_id: recordSysId,
                      sys_created_on: gr.getValue('sys_created_on'),
                  };
              }
          }

      } catch (err) {
          this.addError("DATA_ERROR", "Something went wrong while processing target information." + err);
      }
  },

  getRefinedEncodedQuery: function(encodedQuery, gr) {
      if (this.PROCESS.pgn.table_rotation_info.active)
          encodedQuery = (gs.nil(encodedQuery) ? "" : encodedQuery + "^") + this.PROCESS.info.rtt.encoded_query;

      if (this.PROCESS.info.apply_domain_separation && gr.isValidField("sys_domain")) {
          encodedQuery = (gs.nil(encodedQuery) ? "" : encodedQuery + "^") + "sys_domain=" + this.PROCESS.contract.DOMAIN_SYS_ID;
      }
      if (gr instanceof GlideRecord) gr.setCategory(this.CONSTANTS.READ_REPLICA_DB_CATEGORY);

      return encodedQuery;
  },

  setTotalNumberRecordsToProcess: function(RecordsCount) {

      if (this.PROCESS.pgn.table_rotation_info.active) {
          this.PROCESS.info.rtt.shard_total_number_records_to_process = RecordsCount;

      } else if (this.PROCESS.pgn.batch_count == 1 &&
          (this.PROCESS.pgn.total_number_records_to_process == -1 ||
              RecordsCount < this.PROCESS.pgn.total_number_records_to_process))
          this.PROCESS.pgn.total_number_records_to_process = RecordsCount;
  },

  //process features from the source table
  processDataFromSource: function(includeFeaturesAndLabels) {
      var source = this.PROCESS.contract.SOURCE;

      if (gs.nil(this.PROCESS.useCase) ||
          !this.UTIL.isJSONObject(source) ||
          this.UTIL.isNullOrEmpty(this.PROCESS.extractedData)) {
          this.addError("DATA_ERROR", "Source, useCase or data from source was not provided");
          return;
      }

      this.processSingleFeaturesFromSource(includeFeaturesAndLabels);
      if (this.UTIL.isNullOrEmpty(this.PROCESS.source_sys_ids))
          return;


      if (this.UTIL.isValidJSONArrayKey(this.PROCESS.contract.SOURCE, "OTHER_FEATURES")) {

          //in case there is not single features specified for source, we still need to get the relationship to target
          if (this.UTIL.isNullOrEmpty(this.PROCESS.source_sys_ids))
              this.processSingleFeaturesFromSource(includeFeaturesAndLabels);

          if (this.UTIL.isNullOrEmpty(this.PROCESS.source_sys_ids))
              return;

          var source_ids = Object.keys(this.PROCESS.source_sys_ids).map(function(key) {
              return [this.PROCESS.source_sys_ids[key]];
          }, this);

          this.processOtherFeatures(this.PROCESS.contract.SOURCE.TABLE,
              this.PROCESS.contract.SOURCE.OTHER_FEATURES,
              source_ids);
      }
  },

  processSingleFeaturesFromSource: function(includeFeaturesAndLabels) {
      var source = this.PROCESS.contract.SOURCE;

      if (gs.nil(this.PROCESS.useCase) ||
          !this.UTIL.isJSONObject(source) ||
          this.UTIL.isNullOrEmpty(this.PROCESS.extractedData)) {
          this.addError("DATA_ERROR", "Source, useCase or data from source was not provided");
          return;
      }

      var encodedQuery = source.JOIN.TARGET_ID_FIELD + "IN" + Object.keys(this.PROCESS.extractedData).join(',');

      //if the join is using document id with target
      if (this.UTIL.isValidJSONKey(source, "TARGET_TABLE_FIELD"))
          encodedQuery += "^" + source.TARGET_TABLE_FIELD + "=" + this.PROCESS.contract.TARGET.TABLE;


      if (this.PROCESS.useCase == "TWO_TABLE_JOIN") {
          encodedQuery += "^" + (!this.UTIL.isValidJSONKey(source, "ENCODED_QUERY") ? "" : source.ENCODED_QUERY);

          if (includeFeaturesAndLabels)
              this.addSingleFeaturesFromSource(source.TABLE, source.FEATURES, encodedQuery, source.JOIN.TARGET_ID_FIELD);
          else
              this.addSingleFeaturesFromSource(source.TABLE, null, encodedQuery, source.JOIN.TARGET_ID_FIELD);

      } else if (this.PROCESS.useCase == "TWO_TABLE_JOIN_WITH_M2M") {
          encodedQuery += "^" + (!this.UTIL.isValidJSONKey(source.JOIN, "ENCODED_QUERY") ? "" : source.JOIN.ENCODED_QUERY);

          //if the join is using document id with source
          if (this.UTIL.isValidJSONKey(source, "SOURCE_TABLE_FIELD"))
              encodedQuery += "^" + source.SOURCE_TABLE_FIELD + "=" + source.TABLE;

          var m2mSysId = {};
          //first get the sys_id from m2m to join
          var m2mGR = new GlideRecord(source.JOIN.TABLE);
          encodedQuery = this.getRefinedEncodedQuery(encodedQuery, m2mGR);
          m2mGR.addEncodedQuery(encodedQuery);
          m2mGR.query();
          this.PROCESS.info.batch_total_number_source_records = m2mGR.getRowCount();
          while (m2mGR.next()) {
              if (this.UTIL.isValidJSONKey(m2mSysId, m2mGR.getValue(source.JOIN.SOURCE_ID_FIELD))) {
                  m2mSysId[m2mGR.getValue(source.JOIN.SOURCE_ID_FIELD)].push(m2mGR.getValue(source.JOIN.TARGET_ID_FIELD));
              } else {
                  m2mSysId[m2mGR.getValue(source.JOIN.SOURCE_ID_FIELD)] = [m2mGR.getValue(source.JOIN.TARGET_ID_FIELD)];
              }
          }

          //get the features from the source table
          encodedQuery =
              (!this.UTIL.isValidJSONKey(source, "ENCODED_QUERY") ? "" : source.ENCODED_QUERY + "^") +
              "sys_idIN" + Object.keys(m2mSysId).join(',');
          if (includeFeaturesAndLabels) {
              this.addSingleFeaturesFromSource(source.TABLE, source.FEATURES, encodedQuery, m2mSysId);
          } else {
              this.addSingleFeaturesFromSource(source.TABLE, null, encodedQuery, m2mSysId);
          }
          m2mSysId = null;
      }
  },

  //Add all features from source 
  //inputs: table: table from which the features will be extracted
  //.       features: array with the features to be extracted
  //        encodedQuery: encodedQuery to apply to the table
  //        fieldToJoin: if useCase "TWO_TABLE_JOIN" a fieldName is required
  //                     in useCase "TWO_TABLE_JOIN_WITH_M2M" a map with the relationship is required
  addSingleFeaturesFromSource: function(table, features, encodedQuery, fieldToJoin) {
      if (gs.nil(this.PROCESS.useCase) ||
          gs.nil(table) ||
          gs.nil(fieldToJoin) ||
          this.UTIL.isNullOrEmpty(this.PROCESS.extractedData)) {
          this.addError("DATA_ERROR", "Error while adding features");
          return;
      }

      try {
          var processFeaturesAtRecordCreation = (!this.UTIL.isValidJSONKey(this.PROCESS.contract, "RUNTIME") ||
              this.PROCESS.contract.RUNTIME.toUpperCase() == "CREATE");
          var gr = new GlideRecord(table);
          encodedQuery = this.getRefinedEncodedQuery(encodedQuery, gr);
          gr.addEncodedQuery(encodedQuery);
          gr.query();
          this.PROCESS.info.batch_total_number_source_records = gr.getRowCount();
          while (gr.next()) {
              var recordSysIds = null;
              if (this.PROCESS.useCase == "TWO_TABLE_JOIN")
                  recordSysIds = [gr.getValue(fieldToJoin)];

              else if (this.PROCESS.useCase == "TWO_TABLE_JOIN_WITH_M2M")
                  recordSysIds = fieldToJoin[gr.getUniqueValue()];

              for (var i = 0; i < recordSysIds.length; i++) {
                  var recordSysId = recordSysIds[i];
                  if (this.PROCESS.extractedData.hasOwnProperty(recordSysId)) {
                      if (processFeaturesAtRecordCreation &&
                          (gs.nil(features) || this.PROCESS.extractedData[recordSysId].sys_created_on <= gr.getValue("sys_created_on")))
                          continue;
                      this.PROCESS.source_sys_ids[recordSysId] = gr.getUniqueValue();
                      this.PROCESS.extractedData[recordSysId].source_sys_id = gr.getUniqueValue();
                      this.PROCESS.extractedData[recordSysId].features =
                          this.UTIL.getSingleValueFields(gr, features, this.PROCESS.extractedData[recordSysId].features);
                  }
              }
          }
          this.cleanUpRecords();

          if (this.UTIL.isNullOrEmpty(this.PROCESS.source_sys_ids))
              this._debug("", "No records found in source for batch " + this.PROCESS.pgn.batch_count);


      } catch (err) {
          this.addError("DATA_ERROR", "Error while adding features" + err);
      }
  },


  processOtherFeatures: function(table, otherFeatures, sysIds) {
      if (!this.UTIL.isValidArray(otherFeatures) ||
          !this.UTIL.isValidArray(sysIds) ||
          this.UTIL.isNullOrEmpty(this.PROCESS.extractedData)) {

          this.addError("DATA_ERROR", "Error while adding other features");
          return;
      }

      for (i = 0; i < otherFeatures.length; i++)
          this.addMultiValuedFeatures(table, otherFeatures[i], sysIds);

  },

  addMultiValuedFeatures: function(table, otherFeature, sysIds) {

      if (!this.UTIL.isJSONObject(otherFeature) ||
          !this.UTIL.isValidArray(sysIds) ||
          !this.UTIL.isJSONObject(this.PROCESS.extractedData)) {
          this.addError("DATA_ERROR", "Error while adding other features");
          return;
      }
      try {
          var processFeaturesAtRecordCreation = (!this.UTIL.isValidJSONKey(this.PROCESS.contract, "RUNTIME") ||
              this.PROCESS.contract.RUNTIME.toUpperCase() == "CREATE");

          var maxNumberExtraFeatureByRecord = this.UTIL.isValidJSONKey(otherFeature, "MAX_NUMBER_RECORDS") ?
              parseInt(otherFeature.MAX_NUMBER_RECORDS) : this.CONSTANTS.MAX_NUMBER_OTHER_FEATURES;

          var tableGR = this.UTIL.getOtherFeatureTable(otherFeature, table);
          var encodedQuery = (!this.UTIL.isValidJSONKey(otherFeature, "ENCODED_QUERY") ? "" : otherFeature.ENCODED_QUERY + "^");

          var fieldGR;
          if (otherFeature.TYPE.toLowerCase() == this.CONSTANTS.OTHER_FEATURES_TYPES["ATTACHMENT"]) {
              encodedQuery = "content_typeIN" + (!this.UTIL.isValidJSONKey(otherFeature, "ATTACHMENT_TYPES") ?
                      Object.keys(this.CONSTANTS.ALL_SUPPORTED_ATTACHMENT_TYPES).map(this.UTIL.getMIMEType) :
                      otherFeature.ATTACHMENT_TYPES.map(this.UTIL.getMIMEType)) +
                  "^size_bytes<=" + this.CONSTANTS.MAX_BYTE_SIZE_FOR_ATTACHMENTS +
                  "^table_name=" + table + "^table_sys_id" + "IN" + sysIds;
              fieldGR = "table_sys_id";
              otherFeature.LABEL = this.CONSTANTS.ATTACHMMENT_LABEL;

          } else if (otherFeature.TYPE.toLowerCase() == this.CONSTANTS.OTHER_FEATURES_TYPES["ARRAY"]) {
              encodedQuery = encodedQuery + otherFeature.JOIN_ID_FIELD + "IN" + sysIds;
              fieldGR = otherFeature.JOIN_ID_FIELD;
          }

          if (gs.nil(fieldGR)) {
              this.addError("DATA_ERROR", "Error while adding other features. fieldName was not found");
              return;
          }

          var gr = new GlideRecord(tableGR);
          encodedQuery = this.getRefinedEncodedQuery(encodedQuery, gr);
          gr.addEncodedQuery(encodedQuery);
          gr.query();

          var otherFeaturesRecordMap = {};
          while (gr.next()) {

              var recordSysId = null;
              recordSysId = gr.getValue(fieldGR);

              if (processFeaturesAtRecordCreation &&
                  this.PROCESS.extractedData[recordSysId].sys_created_on <= gr.getValue("sys_created_on"))
                  continue;

              if (!otherFeaturesRecordMap.hasOwnProperty(recordSysId))
                  otherFeaturesRecordMap[recordSysId] = [];

              if (maxNumberExtraFeatureByRecord == -1 ||
                  (maxNumberExtraFeatureByRecord > 0 &&
                      otherFeaturesRecordMap[recordSysId].length < maxNumberExtraFeatureByRecord)) {

                  if (otherFeature.TYPE.toLowerCase() == this.CONSTANTS.OTHER_FEATURES_TYPES["ATTACHMENT"]) {
                      if (this.getTotalExtractedAttachments() >= this.CONSTANTS.MAX_NUMBER_OF_ATTACHMENT_FOR_TRAINING)
                          break;


                      otherFeaturesRecordMap[recordSysId].push(this.UTIL.getAttachmentMetadata(gr));
                      this.PROCESS.info.batch_extracted_attachments++;


                  } else if (otherFeature.TYPE == "ARRAY") {
                      otherFeaturesRecordMap[recordSysId].push(gr.getValue(otherFeature.FEATURE));
                  }
              }
          }

          for (key in this.PROCESS.extractedData) {
              if (!this.PROCESS.extractedData[key].hasOwnProperty("other_features"))
                  this.PROCESS.extractedData[key].other_features = [];

              var sysId = (table == this.PROCESS.contract.TARGET.TABLE) ? key : this.PROCESS.source_sys_ids[key];

              if (!otherFeaturesRecordMap.hasOwnProperty(sysId))
                  this.PROCESS.extractedData[key].other_features.push(this.UTIL.getMultiValueFields(otherFeature.TYPE, table + "." + otherFeature.LABEL));

              else {
                  this.PROCESS.extractedData[key].other_features.push(this.UTIL.getMultiValueFields(otherFeature.TYPE, table + "." + otherFeature.LABEL,
                      otherFeaturesRecordMap[sysId]));
                  delete otherFeaturesRecordMap[sysId];
              }
          }
          otherFeaturesRecordMap = null;

      } catch (err) {
          this.addError("DATA_ERROR", "Error while adding features" + err);
      }
  },

  //clean up all records that were in target but not in source to guarantee an inner join.
  cleanUpRecords: function() {
      for (key in this.PROCESS.extractedData) {
          if (!this.PROCESS.source_sys_ids.hasOwnProperty(key))
              delete this.PROCESS.extractedData[key];

      }
  },


  //Adding errors to DATA_EXTRACTION_ERRORS and the log
  addError: function(type, message) {
      if (gs.nil(type) || !this.CONSTANTS.ERROR_TYPES.hasOwnProperty(type))
          type = this.CONSTANTS.UNKNOWN_TAG;

      message = this.CONSTANTS.ERROR_TYPES[type] + ". " + message;
      this._debug(type + ':' + message);

      if (!this.DATA_EXTRACTION_ERRORS.hasOwnProperty(this.CONSTANTS.ERROR_TYPE_KEY)) {
          this.DATA_EXTRACTION_ERRORS[this.CONSTANTS.ERROR_TYPE_KEY] = type;
          this.DATA_EXTRACTION_ERRORS[this.CONSTANTS.ERROR_MESSAGE_KEY] = [message];
      } else {
          this.DATA_EXTRACTION_ERRORS[this.CONSTANTS.ERROR_MESSAGE_KEY].push(message);
      }

  },

  _debug: function(msg) {
      new DataExtractionDebugUtil().debug(msg);
  },

  type: 'DataExtractionImpl'
};

Sys ID

3e3dbfdaa9a8ed94f877bee690644556

Offical Documentation

Official Docs: