/** * @author Nicolas Stein * @category MyEic * @subcategory Libraries * @extends WindozModel */ class WindozPluralModel extends WindozModel { constructor(businessObject, privileges, singletonClass) { super(businessObject, privileges); this.collection = []; //this.api = app.config.api.filter(item => item.businessObject == businessObject); this.singletonClass = singletonClass; } /** * Get all ids in the current collection * @return {Array} Ids (keys) of the current collection. */ get ids() { return this.collection.map(item => item.itemData.id); } /** * Creates empty singletons at the end of the collection. * @param {Array} ids - Ids of the singletons to create */ create(ids){ for(let id of ids){ this.collection[id] =new this.singletonClass; } } /** * Converts the data in a singleton & adds it to the collection. * If the id exists, will just override keys from the server (preserving eventual overrides) * @param {Object} data - key-value data used to create the singleton. * @return {Object} The collection length after insertion. */ add(data, id=null){ id = data.id || null; let item = this.collection.find(item => item.id == id); if(item){ Object.assign(item.itemData, data); } else { item = new this.singletonClass(); item.itemData = data; this.collection.push(item); } return(item); } /** * Removes and returns a singleton out of the collection. * @param {string} id - id of the singleton to pop. * @return {Object} The popped singleton. */ pop(id) { if(id in this.collection) { let singleton = this.collection[id]; this.delete(this.collection[id]); return(singleton); } else return(null); } /** * Returns the number of singletons in the collection. * @return {Int} Number of singletons. */ get length() { return this.collection.length; } /** * Empties the collection */ empty() { this.collection = []; } /** * Search the collection for matching singletons * @param {Object} matchValues - { {string} key: {string} valueToMatch } * @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent * @return {Array} Array of matching singletons. */ localFind(matchValues, matchExact){ return( this.collection.filter((singleton) => singleton.match(matchValues, matchExact)) ); } /** * Loads singletons from their IDs at the end of the current collection. * if no ids given, loads all * Expected server-contract: * [ {"id":"xxx", ...other singleton data...}, {"id":"yyy", ...other singleton data...}, ...] * @param {array} ids - singleton ids to be loaded * @return {promise} Promise returning the model */ load(endpoint, payload){ return( this.request(endpoint.uri, endpoint.method, payload) .then(async serverData => { this.fill(serverData); return(this); }) ) } fill(serverData) { if(serverData && serverData.length){ for(let singletonData of serverData) this.add(this.sanitize(singletonData)); } } sanitize(item) { return item; } /** * Loads singletons from search criteria at the end of the current collection. * @param {Object} matchValues - { {string} key: {string} valueToMatch } * @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent * @return {Array} Array of matching singletons. */ remotefind(matchValues, matchExact){ return( this.post(this.baseUrl+'search', { 'matchValues': matchValues, 'matchExact': matchExact }) .then(async serverData => { if(serverData){ for(let singletonData of serverData) this.add(singletonData); } return(this); }) ) } save(){ return( this.put(this.baseUrl, this.collection) .then(async serverData => { if(serverData){ for(let singletonData of data) this.add(singletonData); } return(this); }) ) } /** * Fills the collection from a raw CSV string. * (adapted from https://stackoverflow.com/users/10549827/matthew-e-brown) * @param {string} csv The raw CSV string. * @param {string[]} headers An optional array of headers to use. If none are * given, they are pulled from the first line of `text`. * @param {string} idName The name of the column to be used as id. * @param {string} quoteChar A character to use as the encapsulating character. * @param {string} delimiter A character to use between columns. */ //TODO NIKE: test ! & understand why in accumulator, merge uses [key] instead of key fromCsv(csv, headers = null, idName='id', quoteChar = '"', delimiter = ',') { const regex = new RegExp(`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, 'gs'); const match = line => [...line.matchAll(regex)] .map(m => m[2]) // we only want the second capture group .slice(0, -1); // cut off blank match at the end const lines = csv.split('\n'); const firstLine = lines.shift(); const heads = headers || match(firstLine); if(heads.indexOf(idName)<0) { console.warn(`id column "${idName}" not found in CSV headers !`); return; } heads[heads.indexOf(idName)]='id'; let cols; for(let line of lines){ cols = match(line); if(cols.length>=heads.length){ let singletonData = cols.reduce((acc, cur, i) => { // Attempt to parse as a number; replace blank matches with `null` const val = cur.length <= 0 ? null : Number(cur) || cur; const key = heads[i] [[]] `extra_${i}`; return { ...acc, [key]: val }; }, {}); this.add(singletonData); } }; } /** * Builds a CSV in a string from the collection. * @param {Array} keys model properties you want to include in the csv. It defaults to the least common denominator of properties across the collection. * @param {boolean} withHeaders If true, the first line has the properties names. * @param {string} quoteChar Will surround all values (and properties if withHeader). * @param {string} delimiter A character to use between columns. */ //TODO NIKE: test ! toCsv(keys=null, withHeader=true, quoteChar = '"', delimiter = ';', downloadName=null){ const _dwnldCsv = function(data, fname) { const blob = new Blob([data], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.setAttribute('href', url); a.setAttribute('download', fname+'.csv'); a.click(); } if(!keys) { // take min. common set of keys in collection (take 1st then successively intersect to itself) let ids = Object.keys(this.collection); keys = Object.keys(this.collection[ids[0]].itemData); for(let id of ids) { keys = keys.filter(x => (this.collection[id].itemData.hasOwnProperty(x))) } } let csv=''; let row=[]; if(withHeader===true){ for(let key of keys){ row.push(quoteChar+key+quoteChar); } csv += row.join(delimiter)+'\n'; } else if(Array.isArray(withHeader)) { for(let title of withHeader){ row.push(quoteChar+title+quoteChar); } csv += row.join(delimiter)+'\n'; } for(let id in this.collection){ row = []; for(let key of keys){ let item = '' if(typeof(key)=='string'){ // normal column item = this.collection[id].itemData for(let k of key.split('.')) { // Allow for sub-objects with dotted keys notation if(!item) break item = item[k] } } else if(typeof(key)=='function') { // computed column item = key(this.collection[id].itemData) } else console.warn('CSV: Bad column: ',key) row.push(quoteChar+item+quoteChar); } csv += row.join(delimiter)+'\n'; } if(downloadName) _dwnldCsv(csv, downloadName) return(csv); } } app.registerClass('WindozPluralModel',WindozPluralModel);