const _ = require('lodash')
const Client = require('@bespoken-sdk/store/lib/client')
const fs = require('fs')
const JSONUtil = require('@bespoken-sdk/shared/lib/json-util')
const logger = require('@bespoken-sdk/shared/lib/logger')('JOB')
const moment = require('moment')
const Record = require('./record')
const Result = require('./result')
/**
* Class that manages info and execution of a particular job
*/
class Job {
/**
* This routine loads a Job
* It checks first for it locally - if it's not there, it loads it remotely
* It then saves it locally for faster access
* @param {string} key
* @returns {Promise<Job>}
*/
static async lazyFetchJobForKey (key) {
const StoreClient = require('@bespoken-sdk/store/lib/client')
const store = new StoreClient()
if (!fs.existsSync('data')) {
fs.mkdirSync('data')
}
// If there is NOT a dash, means this key is in encrypted UUID format
// We decrypt by calling our server
let decryptedKey = key
if (!key.includes('-')) {
decryptedKey = await store.decrypt(key)
logger.info('JOB LAZYFETCH decrypted key: ' + decryptedKey)
}
let dataFile = `data/${decryptedKey}`
if (!dataFile.endsWith('.json')) {
dataFile = `${dataFile}.json`
}
let jobJSON
if (process.env.FORCE_RELOAD === undefined && fs.existsSync(dataFile)) {
jobJSON = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
} else {
jobJSON = await store.fetch(key)
fs.writeFileSync(dataFile, JSON.stringify(jobJSON, null, 2))
}
console.info('Job JSON: ' + JSON.stringify(jobJSON, null, 2))
const job = Job.fromJSON(jobJSON)
return job
}
/**
* Creates a new Job object from JSON
* @param {Object} json
* @returns {Job}
*/
static fromJSON (json) {
const job = new Job(json.name, json.run, json.config)
JSONUtil.fromJSON(job, json)
console.info('job.results: ' + job.results.length + ' job._results: ' + job._results.length)
// Loop through results and turn into objects
const resultObjects = []
for (const resultJSON of job.results) {
const record = Record.fromJSON(resultJSON.record)
const result = Result.fromJSON(record, resultJSON)
// Make the record property back into a record object - I know, we do similar stuff below :-)
resultObjects.push(result)
}
job._results = resultObjects
// Loop through the records and turn them into objects
const records = []
for (const record of job._records) {
records.push(Record.fromJSON(record))
}
job._records = records
return job
}
/**
* @param {string} name
* @param {string | undefined} run
* @param {any} config
*/
constructor (name, run, config) {
const now = moment().utc()
this._name = name
if (run) {
this._run = run
} else {
this._run = name + '_' + now.format('YYYY-MM-DDTHH-mm-ss')
}
this._timestamp = now.format()
this._config = config
this._key = undefined
this._records = []
this._results = []
this._processedCount = 0
this.totalCount = 0
this._rerun = false
}
/**
* @returns {any}
*/
get config () {
return this._config
}
/**
* The date the job was created (UTC)
* Saved in ISO 8601 format: YYYY-MM-DDThh:mm:ssZ
* Eg. 2020-05-21T15:50:13Z
* @type {string}
*/
get timestamp () {
return this._timestamp
}
/**
* @returns {string}
*/
get customer () {
return this.config.customer
}
/**
* @returns {string | undefined}
*/
get key () {
return this._key
}
/**
*
*/
set key (key) {
this._key = key
}
/**
* @returns {string}
*/
get name () {
return this._name
}
/**
* @returns {number}
*/
get processedCount () {
return this._processedCount
}
/**
* @returns {Record[]} The records for the job
*/
get records () {
return this._records
}
/**
*
*/
set records (records) {
this._records = records
}
/**
* @returns {boolean}
*/
get rerun () {
return this._rerun
}
/**
* Sets the rerun flag
*/
set rerun (rerun) {
this._rerun = rerun
}
/**
* @returns {Result[]} The results for the job
*/
get results () {
return this._results
}
/**
* The run name
* @type {string}
*/
get run () {
return this._run
}
/**
*
*/
set run (run) {
this._run = run
}
/**
* @returns {string}
*/
get status () {
let recordsToProcess = this.records.length
const limit = _.get(this, 'config.limit')
if (limit && limit < recordsToProcess) {
recordsToProcess = limit
}
if (this.processedCount === recordsToProcess) {
return 'COMPLETED'
} else {
return 'NOT_COMPLETED'
}
}
/**
* Captures a result of a record being processed
* @param {Result} result
* @returns {void}
*/
addResult (result) {
this._results.push(result)
}
/**
* Increments the number of records being processed
* @param {number} [count] Defaults to 1
* @returns {void}
*/
addProcessedCount (count = 1) {
this._processedCount += count
}
/**
* Iterates across all the results to see all the expected field values
* @returns {string[]} Return the list of expected field names
*/
expectedFieldNames () {
const fields = this._uniqueFields(this._records, 'expectedFields')
// logger.log(`JOB expectedFields: ${fields}`)
return fields
}
/**
* @param {number} index
* @returns {string}
*/
logURL (index) {
if (!this.key) {
return 'N/A'
}
return `https://store.bespoken.io/store/json/batch-runner/${this.key}?path=$..results[${index}].responses[-1:]`
}
/**
* @returns {string[]}
*/
outputFieldNames () {
// Add output fields from the records as well as the results
const fields = this._uniqueFields(this._results, 'outputFields')
// logger.log(`JOB ouputFields: ${fields}`)
return fields
}
/**
* @returns {any}
*/
toJSON() {
return JSONUtil.toJSON(this)
}
/**
* @private
* @param {Object[]} recordArray
* @param {string} resultProperty
* @returns {string[]}
*/
_uniqueFields (recordArray, resultProperty) {
const fields = []
recordArray.forEach(result => {
// logger.log(`RESULT ${resultProperty}: ${result[resultProperty]}`)
Object.keys(result[resultProperty]).forEach(field => {
if (fields.indexOf(field) === -1) {
fields.push(field)
}
})
})
return fields
}
}
module.exports = Job