UNPKG

rally-tools

Version:
1,762 lines (1,485 loc) 230 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('chalk'), require('os'), require('fs'), require('child_process'), require('perf_hooks'), require('request-promise'), require('path'), require('moment'), require('node-fetch')) : typeof define === 'function' && define.amd ? define(['exports', 'chalk', 'os', 'fs', 'child_process', 'perf_hooks', 'request-promise', 'path', 'moment', 'node-fetch'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.RallyTools = {}, global.chalk$1, global.os, global.fs, global.child_process, global.perf_hooks, global.rp, global.path, global.moment, global.nodeFetch)); })(this, (function (exports, chalk$1, os, fs, child_process, perf_hooks, rp, path, moment, nodeFetch) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk$1); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var rp__default = /*#__PURE__*/_interopDefaultLegacy(rp); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var moment__default = /*#__PURE__*/_interopDefaultLegacy(moment); var nodeFetch__default = /*#__PURE__*/_interopDefaultLegacy(nodeFetch); function _asyncIterator(iterable) { var method; if (typeof Symbol === "function") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); } function _AwaitValue(value) { this.wrapped = value; } function _AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; var wrappedAwait = value instanceof _AwaitValue; Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) { if (wrappedAwait) { resume("next", arg); return; } settle(result.done ? "return" : "normal", arg); }, function (err) { resume("throw", err); }); } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { _AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } _AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; _AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; _AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; function _wrapAsyncGenerator(fn) { return function () { return new _AsyncGenerator(fn.apply(this, arguments)); }; } function _awaitAsyncGenerator(value) { return new _AwaitValue(value); } function _asyncGeneratorDelegate(inner, awaitWrap) { var iter = {}, waiting = false; function pump(key, value) { waiting = true; value = new Promise(function (resolve) { resolve(inner[key](value)); }); return { done: false, value: awaitWrap(value) }; } if (typeof Symbol === "function" && Symbol.iterator) { iter[Symbol.iterator] = function () { return this; }; } iter.next = function (value) { if (waiting) { waiting = false; return value; } return pump("next", value); }; if (typeof inner.throw === "function") { iter.throw = function (value) { if (waiting) { waiting = false; throw value; } return pump("throw", value); }; } if (typeof inner.return === "function") { iter.return = function (value) { return pump("return", value); }; } return iter; } exports.configFile = null; if (os.homedir) { exports.configFile = os.homedir() + "/.rallyconfig"; } exports.configObject = void 0; function loadConfig(file) { if (file) exports.configFile = file; if (!exports.configFile) return; exports.configObject = { hasConfig: true }; try { let json = fs.readFileSync(exports.configFile); exports.configObject = JSON.parse(json); exports.configObject.hasConfig = true; } catch (e) { if (e.code == "ENOENT") { exports.configObject.hasConfig = false; //ok, they should probably make a config } else { throw e; } } } function loadConfigFromArgs(args) { let tempConfig = { hasConfig: true, ...args.config }; exports.configObject = tempConfig; } function setConfig(obj) { exports.configObject = obj; } //function retuns obj.a.b.c function deepAccess(obj, path) { let o = obj; for (let key of path) { if (!o) return []; o = o[key]; } return o; } //This takes a class as the first argument, then adds a getter/setter pair that //corresponds to an object in this.data function defineAssoc(classname, shortname, path) { path = path.split("."); let lastKey = path.pop(); Object.defineProperty(classname.prototype, shortname, { get() { return deepAccess(this, path)[lastKey]; }, set(val) { deepAccess(this, path)[lastKey] = val; } }); } function spawn(options, ...args) { if (typeof options !== "object") { args.unshift(options); options = {}; } //todo options return new Promise((resolve, reject) => { let start = perf_hooks.performance.now(); let stdout = ""; let stderr = ""; let cp = child_process.spawn(...args); let write = global.write; if (options.noecho) { write = () => {}; } if (cp.stdout) cp.stdout.on("data", chunk => { stdout += chunk; write(chunk); }); if (cp.stderr) cp.stderr.on("data", chunk => { stderr += chunk; write(chunk); }); if (options.stdin) { options.stdin(cp.stdin); } cp.on("error", reject); cp.on("close", code => { let end = perf_hooks.performance.now(); let time = end - start; let timestr = time > 1000 ? (time / 100 | 0) / 10 + "s" : (time | 0) + "ms"; resolve({ stdout, stderr, exitCode: code, time, timestr }); }); }); } async function runGit(oks, ...args) { if (typeof oks === "number") { oks = [oks]; } else if (typeof oks === "undefined") { oks = [0]; } let g = await spawn({ noecho: true }, "git", args); if (exports.configObject.verbose) log(`git ${args.join(" ")}`); if (!oks.includes(g.exitCode)) { log(g.stderr); log(g.stdout); throw new AbortError(chalk`Failed to run git ${args} {red ${g.exitCode}}`); } return [g.stdout, g.stderr]; } global.chalk = chalk__default["default"]; global.log = (...text) => console.log(...text); global.write = (...text) => process.stdout.write(...text); global.elog = (...text) => console.error(...text); global.ewrite = (...text) => process.stderr.write(...text); global.errorLog = (...text) => log(...text.map(chalk__default["default"].red)); class lib { //This function takes 2 required arguemnts: // env: the enviornment you wish to use // and either: // 'path', the short path to the resource. ex '/presets/' // 'path_full', the full path to the resource like 'https://discovery-dev.sdvi.com/presets' // // If the method is anything but GET, either payload or body should be set. // payload should be a javascript object to be turned into json as the request body // body should be a string that is passed as the body. for example: the python code of a preset. // // qs are the querystring parameters, in a key: value object. // {filter: "name=test name"} becomes something like 'filter=name=test+name' // // headers are the headers of the request. "Content-Type" is already set if // payload is given as a parameter // // fullResponse should be true if you want to receive the request object, // not just the returned data. static async makeAPIRequest({ env, path, path_full, fullPath, payload, body, method = "GET", qs, headers = {}, fullResponse = false, timeout = exports.configObject.timeout || 20000 }) { var _configObject$api; //backwards compatability from ruby script if (fullPath) path_full = fullPath; //Keys are defined in enviornment variables let config = exports.configObject === null || exports.configObject === void 0 ? void 0 : (_configObject$api = exports.configObject.api) === null || _configObject$api === void 0 ? void 0 : _configObject$api[env]; if (!config) { throw new UnconfiguredEnvError(env); } if (method !== "GET" && !exports.configObject.dangerModify) { if (env === "UAT" && exports.configObject.restrictUAT || env === "PROD") { throw new ProtectedEnvError(env); } } let rally_api_key = config.key; let rally_api = config.url; if (path && path.startsWith("/v1.0/")) { rally_api = rally_api.replace("/api/v2", "/api"); } path = path_full || rally_api + path; if (payload) { body = JSON.stringify(payload, null, 4); } if (payload) { headers["Content-Type"] = "application/vnd.api+json"; } let fullHeaders = { //SDVI ignores this header sometimes. Accept: "application/vnd.api+json", "X-SDVI-Client-Application": "Discovery-rtlib-" + (exports.configObject.appName || "commandline"), ...headers }; if (exports.configObject.vvverbose) { log(`${method} @ ${path}`); log(JSON.stringify(fullHeaders, null, 4)); if (body) { log(body); } else { log("(No body"); } } let requestOptions = { method, body, qs, uri: path, timeout, auth: { bearer: rally_api_key }, headers: fullHeaders, simple: false, resolveWithFullResponse: true }; let response; try { response = await rp__default["default"](requestOptions); if (exports.configObject.vverbose || exports.configObject.vvverbose) { log(chalk__default["default"]`${method} @ ${response.request.uri.href}`); } } catch (e) { if ((e === null || e === void 0 ? void 0 : e.cause.code) === "ESOCKETTIMEDOUT") { throw new APIError(response || {}, requestOptions, body); } else { throw e; } } //Throw an error for any 5xx or 4xx if (!fullResponse && ![200, 201, 202, 203, 204].includes(response.statusCode)) { throw new APIError(response, requestOptions, body); } let contentType = response.headers["content-type"]; let isJSONResponse = contentType === "application/vnd.api+json" || contentType === "application/json"; if (exports.configObject.vvverbose) { log(response.body); } if (fullResponse) { return response; } else if (isJSONResponse) { var _response, _response$body; if ([200, 201, 202, 203, 204].includes(response.statusCode) && !((_response = response) === null || _response === void 0 ? void 0 : (_response$body = _response.body) === null || _response$body === void 0 ? void 0 : _response$body.trim())) return {}; try { return JSON.parse(response.body); } catch (e) { log(response.body); throw new AbortError("Body is not valid json: "); } } else { return response.body; } } //Index a json endpoint that returns a {links} field. //This function returns the merged data objects as an array // //Additonal options (besides makeAPIRequest options): // - Observe: function to be called for each set of data from the api static async indexPath(env, path) { let opts = typeof env === "string" ? { env, path } : env; opts.maxParallelRequests = 1; let index = new IndexObject(opts); return await index.fullResults(); } static clearProgress(size = 30) { if (!exports.configObject.globalProgress) return; process.stderr.write(`\r${" ".repeat(size + 15)}\r`); } static async drawProgress(i, max, size = process.stdout.columns - 15 || 15) { if (!exports.configObject.globalProgress) return; if (size > 45) size = 45; let pct = Number(i) / Number(max); //clamp between 0 and 1 pct = pct < 0 ? 0 : pct > 1 ? 1 : pct; let numFilled = Math.floor(pct * size); let numEmpty = size - numFilled; this.clearProgress(size); process.stderr.write(`[${"*".repeat(numFilled)}${" ".repeat(numEmpty)}] ${i} / ${max}`); } static async keepalive(funcs) { for (let f of funcs) { await f(); } } //Index a json endpoint that returns a {links} field. // //This function is faster than indexPath because it can guess the pages it //needs to retreive so that it can request all assets at once. // //This function assumes that the content from the inital request is the //first page, so starting on another page may cause issues. Consider //indexPath for that. // //Additional opts, besides default indexPath opts: // - chunksize[10]: How often to break apart concurrent requests static async indexPathFast(env, path) { let opts = typeof env === "string" ? { env, path } : env; let index = new IndexObject(opts); return await index.fullResults(); } static isLocalEnv(env) { return !env || env === "LOCAL" || env === "LOC"; } static envName(env) { if (this.isLocalEnv(env)) return "LOCAL"; return env; } } class AbortError extends Error { constructor(message) { super(message); Error.captureStackTrace(this, this.constructor); this.name = "AbortError"; } } class APIError extends Error { constructor(response, opts, body) { super(chalk__default["default"]` {reset Request returned} {yellow ${response === null || response === void 0 ? void 0 : response.statusCode}}{ {green ${JSON.stringify(opts, null, 4)}} {green ${body}} {reset ${response.body}} =============================== {red ${!response.body ? "Request timed out" : "Bad response from API"}} =============================== `); this.response = response; this.opts = opts; this.body = body; //Error.captureStackTrace(this, this.constructor); this.name = "ApiError"; } } class UnconfiguredEnvError extends AbortError { constructor(env) { super("Unconfigured enviornment: " + env); this.name = "Unconfigured Env Error"; } } class ProtectedEnvError extends AbortError { constructor(env) { super("Protected enviornment: " + env); this.name = "Protected Env Error"; } } class FileTooLargeError extends Error { constructor(file) { super(`File ${file.parentAsset ? file.parentAsset.name : "(unknown)"}/${file.name} size is: ${file.sizeGB}g (> ~.2G)`); this.name = "File too large error"; } } class ResoultionError extends Error { constructor(name, env) { super(chalk__default["default"]`Error during name resolution: '{blue ${name}}' is not mapped on {green ${env}}`); this.name = "Name Resoultion Error"; } } class Collection$1 { constructor(arr) { this.arr = arr; } [Symbol.iterator]() { return this.arr[Symbol.iterator](); } findById(id) { return this.arr.find(x => x.id == id); } findByName(name) { return this.arr.find(x => x.name == name); } findByNameContains(name) { return this.arr.find(x => x.name.includes(name)); } log() { for (let d of this) { if (d) { log(d.chalkPrint(true)); } else { log(chalk__default["default"]`{red (None)}`); } } } get length() { return this.arr.length; } } class RallyBase { static handleCaching() { if (!this.cache) this.cache = []; } static isLoaded(env) { if (!this.hasLoadedAll) return; return this.hasLoadedAll[env]; } static async getById(env, id, qs) { this.handleCaching(); for (let item of this.cache) { if (item.id == id && item.remote === env || `${env}-${id}` === item.metastring) return item; } let data = await lib.makeAPIRequest({ env, path: `/${this.endpoint}/${id}`, qs }); if (data.data) { let o = new this({ data: data.data, remote: env, included: data.included }); this.cache.push(o); return o; } } static async getByName(env, name, qs) { this.handleCaching(); for (let item of this.cache) { if (item.name === name && item.remote === env) return item; } let data = await lib.makeAPIRequest({ env, path: `/${this.endpoint}`, qs: { ...qs, filter: `name=${name}` + (qs && qs.filter ? qs.filter : "") } }); //TODO included might not wokr correctly here if (data.data[0]) { let o = new this({ data: data.data[0], remote: env, included: data.included }); this.cache.push(o); return o; } } static async getAllPreCollect(d) { return d; } static async getAll(env) { this.handleCaching(); let datas = await lib.indexPathFast({ env, path: `/${this.endpoint}`, pageSize: "50", qs: { sort: "id" } }); datas = await this.getAllPreCollect(datas); let all = new Collection$1(datas.map(data => new this({ data, remote: env }))); this.cache = [...this.cache, ...all.arr]; return all; } static async removeCache(env) { this.handleCaching(); this.cache = this.cache.filter(x => x.remote !== env); } //Specific turns name into id based on env //Generic turns ids into names async resolveApply(type, dataObj, direction) { let obj; if (direction == "generic") { obj = await type.getById(this.remote, dataObj.id); if (obj) { dataObj.name = obj.name; } else { throw new ResoultionError(`(id = ${dataObj.id})`, this.remote); } } else if (direction == "specific") { obj = await type.getByName(this.remote, dataObj.name); if (obj) { dataObj.id = obj.id; } else { throw new ResoultionError(dataObj.name, this.remote); } } return obj; } //Type is the baseclass you are looking for (should extend RallyBase) //name is the name of the field //isArray is true if it has multiple cardinailty, false if it is single //direction gets passed directly to resolveApply async resolveField(type, name, isArray = false, direction = "generic") { // ignore empty fields let field = this.relationships[name]; if (!(field === null || field === void 0 ? void 0 : field.data)) return; if (isArray) { return await Promise.all(field.data.map(o => this.resolveApply(type, o, direction))); } else { return await this.resolveApply(type, field.data, direction); } } cleanup() { for (let [key, val] of Object.entries(this.relationships)) { //Remove ids from data if (val.data) { if (val.data.id) { delete val.data.id; } else if (val.data[0]) { for (let x of val.data) delete x.id; } } delete val.links; } // organization is unused (?) delete this.relationships.organization; // id is specific to envs // but save source inside meta string in case we need it this.metastring = this.remote + "-" + this.data.id; delete this.data.id; // links too delete this.data.links; } } function sleep(time = 1000) { return new Promise(resolve => setTimeout(resolve, time)); } function* zip(...items) { let iters = items.map(x => x[Symbol.iterator]()); for (;;) { let r = []; for (let i of iters) { let next = i.next(); if (next.done) return; r.push(next.value); } yield r; } } function unordered(_x) { return _unordered.apply(this, arguments); } function _unordered() { _unordered = _wrapAsyncGenerator(function* (proms) { let encapsulatedPromises = proms.map(async (x, i) => [i, await x]); while (encapsulatedPromises.length > 0) { let [ind, result] = yield _awaitAsyncGenerator(Promise.race(encapsulatedPromises.filter(x => x))); yield result; encapsulatedPromises[ind] = undefined; } }); return _unordered.apply(this, arguments); } function* range(start, end) { if (end === undefined) { end = start; start = 0; } while (start < end) yield start++; } class IndexObject { //normal opts from any makeAPIRequest //Note that full_response and pages won't work. // //if you want to start from another page, use `opts.start` //opts.observe: async function(jsonData) => jsonData. Transform the data from the api //opts.maxParallelRequests: number of max api requests to do at once //opts.noCollect: return [] instead of the full data constructor(opts) { this.opts = opts; } linkToPage(page) { return this.baselink.replace(`page=1p`, `page=${page}p`); } async initializeFirstRequest() { //Create a copy of the options in case we need to have a special first request this.start = this.opts.start || 1; let initOpts = { ...this.opts }; if (this.opts.pageSize) { initOpts.qs = { ...this.opts.qs }; initOpts.qs.page = `${this.start}p${this.opts.pageSize}`; } this.allResults = []; //we make 1 non-parallel request to the first page so we know how to //format the next requests let json = await lib.makeAPIRequest(initOpts); if (this.opts.observe) json = await this.opts.observe(json); if (!this.opts.noCollect) this.allResults.push(json); this.baselink = json.links.first; this.currentPageRequest = this.start; this.hasHit404 = false; } getNextRequestLink() { this.currentPageRequest++; return [this.currentPageRequest, this.linkToPage(this.currentPageRequest)]; } ///promiseID is the id in `currentPromises`, so that it can be marked as ///done inside the promise array. promiseID is a number from 0 to ///maxparallel-1 async getNextRequestPromise(promiseID) { let [page, path_full] = this.getNextRequestLink(); return [promiseID, page, await lib.makeAPIRequest({ ...this.opts, path_full, fullResponse: true })]; } cancel() { this.willCancel = true; } async fullResults() { await this.initializeFirstRequest(); let maxParallelRequests = this.opts.maxParallelRequests || this.opts.chunksize || 20; let currentPromises = []; //generate the first set of requests. Everything after this will re-use these i promiseIDs for (let i = 0; i < maxParallelRequests; i++) { currentPromises.push(this.getNextRequestPromise(currentPromises.length)); } for (;;) { let [promiseID, page, requestResult] = await Promise.race(currentPromises.filter(x => x)); if (this.willCancel) { return null; } if (requestResult.statusCode === 404) { this.hasHit404 = true; } else if (requestResult.statusCode === 200) { let json = JSON.parse(requestResult.body); if (this.opts.observe) json = await this.opts.observe(json); if (!this.opts.noCollect) this.allResults.push(json); if (json.data.length === 0) this.hasHit404 = true; } else { throw new APIError(requestResult, `(unknown args) page ${page}`, null); } if (this.hasHit404) { currentPromises[promiseID] = null; } else { currentPromises[promiseID] = this.getNextRequestPromise(promiseID); } if (currentPromises.filter(x => x).length === 0) break; } let all = []; for (let result of this.allResults) { for (let item of result.data) { all.push(item); } } return all; } } function orderedObjectKeys(obj) { let keys = Object.keys(obj).sort(); let newDict = {}; for (let key of keys) { if (Array.isArray(obj[key])) { newDict[key] = obj[key].map(x => orderedObjectKeys(x)); } else if (typeof obj[key] === "object" && obj[key]) { newDict[key] = orderedObjectKeys(obj[key]); } else { newDict[key] = obj[key]; } } return newDict; } class Provider extends RallyBase { constructor({ data, remote }) { super(); this.data = data; this.meta = {}; this.remote = remote; } //cached async getEditorConfig() { if (this.editorConfig) return this.editorConfig; this.editorConfig = await lib.makeAPIRequest({ env: this.remote, path_full: this.data.links.editorConfig }); this.editorConfig.fileExt = await this.getFileExtension(); return this.editorConfig; } static async getAllPreCollect(providers) { return providers.sort((a, b) => { return a.attributes.category.localeCompare(b.attributes.category) || a.attributes.name.localeCompare(b.attributes.name); }); } async getFileExtension() { let config = await this.getEditorConfig(); let map = { python: "py", text: "txt", getmap(key) { if (this.name === "Aurora") return "zip"; if (this.name === "Vantage") return "zip"; if (this.name === "ffmpeg") return "txt"; if (this[key]) return this[key]; return key; } }; return map.getmap(config.lang); } chalkPrint(pad = true) { let id = String(this.id); if (pad) id = id.padStart(4); return chalk`{green ${id}}: {blue ${this.category}} - {green ${this.name}}`; } } defineAssoc(Provider, "id", "data.id"); defineAssoc(Provider, "name", "data.attributes.name"); defineAssoc(Provider, "category", "data.attributes.category"); defineAssoc(Provider, "remote", "meta.remote"); defineAssoc(Provider, "editorConfig", "meta.editorConfig"); Provider.endpoint = "providerTypes"; class Notification extends RallyBase { constructor({ data, remote }) { super(); this.data = data; this.meta = {}; this.remote = remote; } static async getAllPreCollect(notifications) { return notifications.sort((a, b) => { return a.attributes.type.localeCompare(b.attributes.type) || a.attributes.name.localeCompare(b.attributes.name); }); } chalkPrint(pad = false) { let id = String("N-" + this.id); if (pad) id = id.padStart(4); return chalk`{green ${id}}: {blue ${this.type}} - {green ${this.name}}`; } } defineAssoc(Notification, "id", "data.id"); defineAssoc(Notification, "name", "data.attributes.name"); defineAssoc(Notification, "address", "data.attributes.address"); defineAssoc(Notification, "type", "data.attributes.type"); defineAssoc(Notification, "remote", "meta.remote"); Notification.endpoint = "notificationPresets"; if (os.homedir) { os.homedir(); } const colon = /:/g; const siloLike = /(silo\-\w+?)s?\/([^\/]+)\.([\w1234567890]+)$/g; function pathTransform(path) { if (path.includes(":")) { //Ignore the first colon in window-like filesystems path = path.slice(0, 3) + path.slice(3).replace(colon, "--"); } if (exports.configObject.invertedPath) { path = path.replace(siloLike, "$2-$1.$3"); } if (path.includes("\\342\\200\\220")) { path = path.replace("\\342\\200\\220", "‐"); } return path; } function readFileSync(path, options) { return fs__default["default"].readFileSync(pathTransform(path), options); } //Create writefilesync, with ability to create directory if it doesnt exist function writeFileSync(path$1, data, options, dircreated = false) { path$1 = pathTransform(path$1); try { return fs__default["default"].writeFileSync(path$1, data, options); } catch (e) { if (dircreated) throw e; let directory = path.dirname(path$1); try { fs__default["default"].statSync(directory); throw e; } catch (nodir) { fs__default["default"].mkdirSync(directory); return writeFileSync(path$1, data, options, true); } } } class Rule extends RallyBase { constructor({ path: path$1, data, remote, subProject } = {}) { super(); if (path$1) { path$1 = path.resolve(path$1); try { let f = readFileSync(path$1, "utf-8"); data = JSON.parse(readFileSync(path$1, "utf-8")); } catch (e) { if (e.code === "ENOENT") { if (exports.configObject.ignoreMissing) { this.missing = true; return undefined; } else { throw new AbortError("Could not load code of local file"); } } else { throw new AbortError(`Unreadable JSON in ${path$1}. ${e}`); } } } this.meta = {}; this.subproject = subProject; if (!data) { data = Rule.newShell(); } this.data = data; this.remote = remote; delete this.data.relationships.transitions; delete this.data.meta; delete this.data.attributes.updatedAt; delete this.data.attributes.createdAt; this.isGeneric = !this.remote; } static newShell() { return { "attributes": { "description": "-", "priority": "PriorityNorm", "starred": false }, "relationships": {}, "type": "workflowRules" }; } async acclimatize(env) { this.remote = env; await this.resolveField(Preset, "preset", false, "specific"); await this.resolveField(Rule, "passNext", false, "specific"); await this.resolveField(Rule, "errorNext", false, "specific"); await this.resolveField(Provider, "providerType", false, "specific"); await this.resolveField(Rule, "dynamicNexts", true, "specific"); await this.resolveField(Notification, "enterNotifications", true, "specific"); await this.resolveField(Notification, "errorNotifications", true, "specific"); await this.resolveField(Notification, "passNotifications", true, "specific"); } async saveA(env) { if (lib.isLocalEnv(env)) return; return await this.createIfNotExist(env); } async saveB(env) { if (!this.isGeneric) { await this.resolve(); } this.cleanup(); if (lib.isLocalEnv(env)) { log(chalk`Saving rule {green ${this.name}} to {blue ${lib.envName(env)}}.`); writeFileSync(this.localpath, JSON.stringify(orderedObjectKeys(this.data), null, 4)); } else { await this.acclimatize(env); return await this.uploadRemote(env); } } get immutable() { return false; } async createIfNotExist(env) { write(chalk`First pass rule {green ${this.name}} to {green ${env}}: `); if (this.immutable) { log(chalk`{magenta IMMUTABLE}. Nothing to do.`); return; } //First query the api to see if this already exists. let remote = await Rule.getByName(env, this.name); this.idMap = this.idMap || {}; if (remote) { this.idMap[env] = remote.id; log(chalk`exists ${remote.chalkPrint(false)}`); return; } //If it exists we can replace it write("create, "); let res = await lib.makeAPIRequest({ env, path: `/workflowRules`, method: "POST", payload: { data: { attributes: { name: this.name }, type: "workflowRules" } } }); this.idMap = this.idMap || {}; this.idMap[env] = res.data.id; write("id "); log(this.idMap[env]); } async patchStrip() { delete this.data.attributes.createdAt; delete this.data.attributes.starred; delete this.data.attributes.updatedAt; this.nexts = this.data.relationships.dynamicNexts; delete this.data.relationships.dynamicNexts; // TEMP FIX FOR BUG IN SDVI if (this.relationships.passMetadata && this.relationships.passMetadata[0]) { log("HAS PASS"); log(this.name); log("HAS PASS"); } delete this.relationships.passMetadata; if (this.relationships.errorMetadata && this.relationships.errorMetadata[0]) { log("HAS PASS"); log(this.name); log("HAS PASS"); } delete this.relationships.errorMetadata; // This is commented out because it was fixed. //for(let key in this.relationships){ //let relationship = this.relationships[key]; //if(!relationship.data || relationship.data instanceof Array && !relationship.data[0]){ //delete this.relationships[key]; //} //} } async uploadRemote(env) { write(chalk`Uploading rule {green ${this.name}} to {green ${env}}: `); if (this.immutable) { log(chalk`{magenta IMMUTABLE}. Nothing to do.`); return; } if (this.idMap[env]) { this.remote = env; await this.patchStrip(); this.data.id = this.idMap[env]; this.relationships.transitions = { data: await this.constructWorkflowTransitions() }; //If it exists we can replace it write("replace, "); let res = await lib.makeAPIRequest({ env, path: `/workflowRules/${this.idMap[env]}`, method: "PUT", payload: { data: this.data }, fullResponse: true }); log(chalk`response {yellow ${res.statusCode}}`); if (res.statusCode > 210) { return `Failed to upload: ${res.body}`; } } else { throw Error("Bad idmap!"); } } get localpath() { return this._localpath || path.join(exports.configObject.repodir, this.subproject || "", "silo-rules", this.name + ".json"); } async resolve() { let preset = await this.resolveField(Preset, "preset", false); //log(preset); let pNext = await this.resolveField(Rule, "passNext", false); let eNext = await this.resolveField(Rule, "errorNext", false); let proType = await this.resolveField(Provider, "providerType", false); //log("Dynamic nexts") let dynamicNexts = await this.resolveField(Rule, "dynamicNexts", true); //log(dynamicNexts); let enterNotif = await this.resolveField(Notification, "enterNotifications", true); let errorNotif = await this.resolveField(Notification, "errorNotifications", true); let passNotif = await this.resolveField(Notification, "passNotifications", true); //TODO Unsupported delete this.relationships["enterMetadata"]; delete this.relationships["errorMetadata"]; this.isGeneric = true; return { preset, proType, pNext, eNext, dynamicNexts, errorNotif, enterNotif, passNotif }; } chalkPrint(pad = true) { let id = String("R-" + (this.remote && this.remote + "-" + this.id || "LOCAL")); let sub = ""; if (this.subproject) { sub = chalk`{yellow ${this.subproject}}`; } if (pad) id = id.padStart(10); try { return chalk`{green ${id}}: ${sub}{blue ${this.name}}`; } catch (e) { return this.data; } } async constructWorkflowTransitions() { var _this$nexts; let transitions = []; let dynamicNexts = ((_this$nexts = this.nexts) === null || _this$nexts === void 0 ? void 0 : _this$nexts.data) || []; if (dynamicNexts.length == 0) return; write(chalk`transition mapping: `); for (let transition of dynamicNexts) { write(chalk`{green ${transition.meta.transition}}:`); let filters = { toWorkflowRuleId: transition.id, name: transition.meta.transition, fromWorkflowRuleId: this.id }; let res = await lib.makeAPIRequest({ env: this.remote, path: `/workflowTransitions`, method: "GET", qs: { filter: JSON.stringify(filters) } }); let newTransitionId = 0; if (res.data.length > 0) { write(chalk`{blue found} `); let firstTransition = res.data[0]; newTransitionId = firstTransition.id; } else { write(chalk`{magenta create} `); let newTransitionPayload = { "attributes": { "name": filters.name }, "relationships": { "fromWorkflowRule": { "data": { "id": filters.fromWorkflowRuleId, "type": "workflowRules" } }, "toWorkflowRule": { "data": { "id": filters.toWorkflowRuleId, "type": "workflowRules" } } }, "type": "workflowTransitions" }; let newTransition = await lib.makeAPIRequest({ env: this.remote, path: `/workflowTransitions`, method: "POST", payload: { data: newTransitionPayload } }); newTransitionId = newTransition.data.id; } transitions.push({ "id": newTransitionId, "type": "workflowTransitions" }); write(chalk`{yellow ${newTransitionId}}, `); } write(chalk`t. done, `); return transitions; } } defineAssoc(Rule, "name", "data.attributes.name"); defineAssoc(Rule, "description", "data.attributes.description"); defineAssoc(Rule, "id", "data.id"); defineAssoc(Rule, "relationships", "data.relationships"); defineAssoc(Rule, "isGeneric", "meta.isGeneric"); defineAssoc(Rule, "remote", "meta.remote"); defineAssoc(Rule, "subproject", "meta.project"); defineAssoc(Rule, "idMap", "meta.idMap"); defineAssoc(Rule, "nexts", "meta.nexts"); Rule.endpoint = "workflowRules"; const inquirer = importLazy("inquirer"); const readdir = importLazy("recursive-readdir"); let hasAutoCompletePrompt = false; function addAutoCompletePrompt() { if (hasAutoCompletePrompt) return; hasAutoCompletePrompt = true; inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt")); } async function loadLocals(path$1, Class) { let basePath = exports.configObject.repodir; let objs = (await readdir(basePath)).filter(name => name.includes(path$1)).filter(name => !path.basename(name).startsWith(".")).map(name => new Class({ path: name })); return objs; } async function selectLocal(path, typeName, Class, canSelectNone = true) { addAutoCompletePrompt(); let objs = await loadLocals(path, Class); let objsMap = objs.map(x => ({ name: x.chalkPrint(true), value: x })); return await selectLocalMenu(objsMap, typeName, canSelectNone); } async function selectLocalMenu(objs, typeName, canSelectNone = true) { let none = { name: chalk` {red None}: {red None}`, value: null }; if (canSelectNone) objs.unshift(none); let q = await inquirer.prompt([{ type: "autocomplete", name: "obj", message: `What ${typeName} do you want?`, source: async (sofar, input) => { return objs.filter(x => input ? x.name.toLowerCase().includes(input.toLowerCase()) : true); } }]); return q.obj; } async function selectPreset({ purpose = "preset", canSelectNone }) { return selectLocal("silo-presets", purpose, Preset, canSelectNone); } async function askInput(question, def) { return (await inquirer.prompt([{ type: "input", name: "ok", message: question, default: def }])).ok; } async function askQuestion(question) { return (await inquirer.prompt([{ type: "confirm", name: "ok", message: question }])).ok; } async function saveConfig(newConfigObject, { ask = true, print = true } = {}) { //Create readable json and make sure the user is ok with it let newConfig = JSON.stringify(newConfigObject, null, 4); if (print) log(newConfig); //-y or --set will make this not prompt if (ask && !(await askQuestion("Write config to disk?"))) return; fs.writeFileSync(exports.configFile, newConfig, { mode: 0o600 }); log(chalk`Created file {green ${exports.configFile}}.`); } class File extends RallyBase { constructor({ data, remote, included, parent }) { super(); this.data = data; this.meta = {}; this.remote = remote; this.parentAsset = parent; } chalkPrint(pad = false) { let id = String("F-" + (this.remote && this.remote + "-" + this.id || "LOCAL")); if (pad) id = id.padStart(15); return chalk`{green ${id}}: {blue ${this.data.attributes ? this.name : "(lite file)"}} {red ${this.sizeHR}}`; } canBeDownloaded() { return this.sizeGB <= .2; } async getContent(force = false, noRedirect = false) { if (!this.canBeDownloaded() && !force) { throw new FileTooLargeError(this); } let d = lib.makeAPIRequest({ env: this.remote, fullPath: this.contentLink, qs: { "no-redirect": noRedirect } }); if (noRedirect) { return (await d).links.content; } else { return d; } } async delete(remove = true) { return lib.makeAPIRequest({ env: this.remote, fullPath: this.selfLink, method: "DELETE" }); } get size() { return Object.values(this.data.attributes.instances)[0].size; } get sizeGB() { return Math.round(this.size / 1024 / 1024 / 1024 * 10) / 10; } get sizeHR() { let units = ["B", "K", "M", "G", "T"]; let unitIdx = 0; let size = this.size; while (size > 1000) { size /= 1024; unitIdx++; } if (size > 100) { size = Math.round(size); } else { size = Math.round(size * 10) / 10; } return size + units[unitIdx]; } get instancesList() { let instances = []; for (let [key, val] of Object.entries(this.instances)) { let n = { id: key }; Object.assign(n, val); instances.push(n); } return instances; } static rslURL(instance) { return `rsl://${instance.storageLocationName}/${instance.name}`; } } defineAssoc(File, "id", "data.id"); defineAssoc(File, "name", "data.attributes.label"); defineAssoc(File, "contentLink", "data.links.content"); defineAssoc(File, "selfLink", "data.links.self"); defineAssoc(File, "label", "data.attributes.label"); defineAssoc(File, "md5", "data.attributes.md5"); defineAssoc(File, "sha512", "data.attributes.sha512"); defineAssoc(File, "tags", "data.attributes.tagList"); defineAssoc(File, "instances", "data.attributes.instances"); File.endpoint = null; async function findLineInFile(renderedPreset, lineNumber) { let trueFileLine = lineNumber; let linedRenderedPreset = renderedPreset.split("\n").slice(2, -2); renderedPreset = renderedPreset.split("\n").slice(2, -2).join("\n"); let includeLocation = renderedPreset.split("\n").filter(x => x.includes("@include")); let endIncludeNumber = -1, addTabDepth = 2; let lineBeforeIncludeStatement = ''; let withinInclude = true; if (lineNumber > linedRenderedPreset.indexOf(includeLocation[includeLocation.length - 1])) { addTabDepth = 0; withinInclude = false; } for (let index = includeLocation.length - 1; index >= 0; index--) { let currIncludeIndex = linedRenderedPreset.indexOf(includeLocation[index]); let tabDepth = includeLocation[index].split(" ").length; if (lineNumber > currIncludeIndex) { if (includeLocation[index].split(" ").filter(Boolean)[1] != "ERROR:") { if (lineBeforeIncludeStatement.split(" ").length == tabDepth && withinInclude) { trueFileLine = trueFileLine - currIncludeIndex; break; } else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth && endIncludeNumber == -1) { endIncludeNumber = currIncludeIndex; } else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth) { trueFileLine = trueFileLine - (endIncludeNumber - currIncludeIndex); endIncludeNumber = -1; } } } else { lineBeforeIncludeStatement = includeLocation[index]; } } let funcLine = ""; for (let line of linedRenderedPreset.slice(0, lineNumber).reverse()) { let match = /def (\w+)/.exec(line); if (match) { funcLine = match[1]; break; } } let includeFilename; if (lineBeforeIncludeStatement != "") { includeFilename = lineBeforeIncludeStatement.slice(1).trim().slice(14, -1); } else { includeFilename = null; } if (includeLocation.length !== 0) { trueFileLine -= 1; lineNumber -= 1; } return { lineNumber: trueFileLine, includeFilename, line: linedRenderedPreset[lineNumber], funcLine }; } function printOutLine(eLine) { return log(chalk`{blue ${eLine.includeFilename || "Main"}}:{green ${eLine.lineNumber}} in ${eLine.funcLine} ${eLine.line}`); } async function getArtifact(env, artifact, jobid) { let path = `/jobs/${jobid}/artifacts/${artifact}`; let art = lib.makeAPIRequest({ env, path }).catch(_ => null); return await art; } async function getInfo(env, jobid) { let trace = getArtifact(env, "trace", jobid); let renderedPreset = getArtifact(env, "preset", jobid); let result = getArtifact(env, "result", jobid); let error = getArtifact(env, "error", jobid); let output = getArtifact(env, "output", jobid); [trace, renderedPreset, result, output, error] = await Promise.all([trace, renderedPreset, result, output, error]); return { trace, renderedPreset, result, output, error }; } const tracelineRegex = /^(?:[\d.]+) ([\w ]+):(\d+): (.+)/; function parseTraceLine(line) { let info = tracelineRegex.exec(line); if (!info) { return { full: line, parsed: false, content: line }; } return { absoluteTime: info[0], presetName: info[1], lineNumber: info[2], text: info[3], content: info[3], full: line, parsed: true }; } async function parseTrace(env, jobid) { let { trace, renderedPreset } = await getInfo(env, jobid); let errorLines = []; let shouldBreak = 0; for (let tr of trace.split("\n\n").reverse()) { errorLines.push(tr); shouldBreak--; if (tr.includes("Exception")) shouldBreak = 1; if (tr.includes("raised")) shouldBreak = 1; if (!shouldBreak) break; } let errorList = []; for (let errLine of errorLines) { let info = parseTraceLine(errLine); if (!info.parsed) { errorList.push((await findLineInFile(renderedPreset, info.lineNumber)));