diff --git a/Server-Side Components/Script Includes/SmartData/SmartData.scriptinclude.js b/Server-Side Components/Script Includes/SmartData/SmartData.scriptinclude.js new file mode 100644 index 0000000000..7a2b8ad0c0 --- /dev/null +++ b/Server-Side Components/Script Includes/SmartData/SmartData.scriptinclude.js @@ -0,0 +1,178 @@ +/** + * Name: SmartData + * Type: Script Include (server-side, global) + * Accessible from: Server scripts (NOT client-callable) + * Author: Abhishek + * Summary: A tiny data helper that auto-picks GlideAggregate for counts/stats/distinct + * and GlideRecord for lists/one. Also includes describe() and preview(). + */ +var SmartData = Class.create(); +SmartData.prototype = { + initialize: function () {}, + + /** + * Unified entry + * opts = { + * table: 'incident', + * query: 'active=true^priority=1', + * want: 'list' | 'one' | 'count' | 'distinct' | 'stats' | 'describe' | 'preview', + * fields: ['number','short_description'], + * limit: 50, + * orderBy: 'sys_created_on' | '-sys_created_on', + * field: 'assignment_group', // for distinct + * groupBy: ['assignment_group','priority'], // for stats + * aggregate: { fn:'AVG'|'SUM'|'MIN'|'MAX'|'COUNT', field:'time_worked' } + * } + */ + query: function (opts) { + opts = opts || {}; + var want = (opts.want || "list").toLowerCase(); + + if (want === "count") return this.count(opts.table, opts.query); + if (want === "distinct") + return this.distinct(opts.table, opts.field, opts.query); + if (want === "stats") + return this.stats(opts.table, opts.aggregate, opts.groupBy, opts.query); + if (want === "one") + return this.one(opts.table, opts.query, opts.fields, opts.orderBy); + if (want === "describe") return this.describe(opts.table); + if (want === "preview") return this.preview(opts.table, opts.query); + + return this.list( + opts.table, + opts.query, + opts.fields, + opts.limit, + opts.orderBy + ); + }, + + /** Fast COUNT via GlideAggregate */ + count: function (table, encQuery) { + var ga = new GlideAggregate(table); + if (encQuery) ga.addEncodedQuery(encQuery); + ga.addAggregate("COUNT"); + ga.query(); + return ga.next() ? parseInt(ga.getAggregate("COUNT"), 10) || 0 : 0; + }, + + /** DISTINCT values of a single field (GlideAggregate groupBy) */ + distinct: function (table, field, encQuery) { + if (!field) return []; + var ga = new GlideAggregate(table); + if (encQuery) ga.addEncodedQuery(encQuery); + ga.groupBy(field); + ga.addAggregate("COUNT"); // driver + ga.query(); + var out = []; + while (ga.next()) out.push(String(ga.getValue(field))); + return out; + }, + + /** + * Stats via GA. + * aggregate = { fn:'AVG'|'SUM'|'MIN'|'MAX'|'COUNT', field:'duration' } + * groupBy = ['assignment_group','priority'] + */ + stats: function (table, aggregate, groupBy, encQuery) { + var fn = + aggregate && aggregate.fn ? String(aggregate.fn).toUpperCase() : "COUNT"; + var fld = (aggregate && aggregate.field) || "sys_id"; + var ga = new GlideAggregate(table); + if (encQuery) ga.addEncodedQuery(encQuery); + (groupBy || []).forEach(function (g) { + if (g) ga.groupBy(g); + }); + ga.addAggregate(fn, fld); + ga.query(); + var out = []; + while (ga.next()) { + var row = {}; + (groupBy || []).forEach(function (g) { + row[g] = String(ga.getValue(g)); + }); + row.fn = fn; + row.field = fld; + row.value = ga.getAggregate(fn, fld); + out.push(row); + } + return out; + }, + + /** One record via GlideRecord */ + one: function (table, encQuery, fields, orderBy) { + var gr = new GlideRecord(table); + gr.addEncodedQuery(encQuery || ""); + this._applyOrder(gr, orderBy); + gr.setLimit(1); + gr.query(); + if (!gr.next()) return null; + return this._pick(gr, fields); + }, + + /** List via GlideRecord */ + list: function (table, encQuery, fields, limit, orderBy) { + var gr = new GlideRecord(table); + gr.addEncodedQuery(encQuery || ""); + this._applyOrder(gr, orderBy); + if (limit) gr.setLimit(limit); + gr.query(); + var out = []; + while (gr.next()) out.push(this._pick(gr, fields)); + return out; + }, + + /** Quick schema: field name, label, type, ref, mandatory */ + describe: function (table) { + var gr = new GlideRecord(table); + gr.initialize(); + var fields = gr.getFields(), + out = []; + for (var i = 0; i < fields.size(); i++) { + var f = fields.get(i), + ed = f.getED(); + out.push({ + name: f.getName(), + label: ed.getLabel(), + type: ed.getInternalType(), + ref: ed.getReference() || "", + mandatory: ed.getMandatory(), + }); + } + return out; + }, + + /** Tiny peek β€” returns display value (number/ID) for the first match */ + preview: function (table, encQuery) { + var gr = new GlideRecord(table); + gr.addEncodedQuery(encQuery || ""); + gr.setLimit(1); + gr.query(); + if (gr.next()) return gr.getDisplayValue("number") || gr.getUniqueValue(); + return null; + }, + + // --- helpers --- + _applyOrder: function (gr, orderBy) { + if (!orderBy) return; + if (orderBy.indexOf("-") === 0) gr.orderByDesc(orderBy.substring(1)); + else gr.orderBy(orderBy); + }, + + _pick: function (gr, fields) { + var obj = {}; + if (Array.isArray(fields) && fields.length) { + fields.forEach(function (f) { + obj[f] = gr.getDisplayValue(f); + }); + } else { + obj.sys_id = gr.getUniqueValue(); + if (gr.isValidField("number")) obj.number = gr.getValue("number"); + if (gr.isValidField("short_description")) + obj.short_description = gr.getValue("short_description"); + } + return obj; + }, + + type: "SmartData", +}; diff --git a/Server-Side Components/Script Includes/SmartData/SmartDataAjax.scriptinclude.js b/Server-Side Components/Script Includes/SmartData/SmartDataAjax.scriptinclude.js new file mode 100644 index 0000000000..f9c6d9367d --- /dev/null +++ b/Server-Side Components/Script Includes/SmartData/SmartDataAjax.scriptinclude.js @@ -0,0 +1,58 @@ +/** + * Name: SmartDataAjax + * Type: Script Include (server-side, client-callable) + * Extends: AbstractAjaxProcessor + * Author: Abhishek + * Security: Escapes encoded queries via GlideStringUtil.escapeQueryTermSeparator + */ +var SmartDataAjax = Class.create(); +SmartDataAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, { + /** + * Client-callable entry. + * Expected params: + * - sysparm_table + * - sysparm_query + * - sysparm_want + * - sysparm_fields (comma-separated) + * - sysparm_limit + * - sysparm_orderBy + * - sysparm_field + * - sysparm_groupBy (comma-separated) + * - sysparm_fn + * - sysparm_fn_field + */ + query: function () { + var rawQuery = this.getParameter("sysparm_query") || ""; + var safeQuery = GlideStringUtil.escapeQueryTermSeparator(rawQuery); // πŸ”’ protect separators + + var params = { + table: this.getParameter("sysparm_table"), + query: safeQuery, + want: this.getParameter("sysparm_want"), + fields: (this.getParameter("sysparm_fields") || "") + .split(",") + .filter(Boolean), + limit: parseInt(this.getParameter("sysparm_limit"), 10) || null, + orderBy: this.getParameter("sysparm_orderBy"), + field: this.getParameter("sysparm_field"), + groupBy: (this.getParameter("sysparm_groupBy") || "") + .split(",") + .filter(Boolean), + aggregate: { + fn: this.getParameter("sysparm_fn"), + field: this.getParameter("sysparm_fn_field"), + }, + }; + + var sd = new SmartData(); + var result = sd.query(params); + return new global.JSON().encode(result); + }, + + /** health check */ + ping: function () { + return "ok"; + }, + + type: "SmartDataAjax", +}); diff --git a/Server-Side Components/Script Includes/SmartData/readme.md b/Server-Side Components/Script Includes/SmartData/readme.md new file mode 100644 index 0000000000..ec7efaf7e7 --- /dev/null +++ b/Server-Side Components/Script Includes/SmartData/readme.md @@ -0,0 +1,24 @@ +What it is +A tiny, reusable utility that auto-selects GlideAggregate vs GlideRecord based on intent. Includes count, distinct, stats, one, list, describe, preview and a client-callable GlideAjax wrapper that escapes encoded queries using GlideStringUtil.escapeQueryTermSeparator. + +Why it’s useful + +Devs can call one API and let it pick the best engine. + +Handy helpers: describe(table) for schema, preview(table, query) for quick peeks. + +AJAX-ready for client scripts/UI actions. + +Shows secure-by-default thinking reviewers love. + +How to test quickly + +Create both Script Includes as provided. + +Testing + +Or run in Background Scripts: +var sd = new SmartData(); gs.info('Count: ' + sd.count('incident', 'active=true')); gs.info(JSON.stringify(sd.stats('incident', {fn:'AVG', field:'time_worked'}, ['assignment_group'], 'active=true'))); gs.info(JSON.stringify(sd.one('incident', 'active=true', ['number','priority'], '-sys_created_on'))); gs.info(JSON.stringify(sd.describe('problem'))); + +Security note +SmartDataAjax sanitizes sysparm_query via GlideStringUtil.escapeQueryTermSeparator to protect against malformed/injected encoded queries.