Here is the code for each of the example mentioned in GAS lists and validation.

The custom functions

You need this in your local workbook. You can find it in the examples sheet. These are the custom functions you can call directly from your sheets. The function documentation is here
// custom functions for use with cBlister class
// this module is required in any spreadsheet you want to use these custom functions in.
// they should need changing
// you also need to include the blister library
//
// v1.1 sep 2013
// creative commons license
// bruce@mcpher.com
// ramblings.mcpher.com
//----------------------
/**
  * return the values in a blister and return the value(s) in the same position(s)
  * @param {*} value  we're looking for
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * @param {string|number=} optReturnListId can be a column name, or a column number starting/default at 1
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches
  * return {*|<array>.*} the found value(s)
  */
function blisterLookup ( listName, value, optListId, optReturnListId ,  optSortId , optSortDescending,  optMaxMatch ) {

    // get a new blister
    var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} );
    
    // match it
    var idx = b.matchWorker ( value , {
        listId : optListId || undefined,  
        maxMatch: optMaxMatch || undefined 
      });
      
    // get the values - its already sorted so no need to do it again
    return blister.transposeArray(b.indexWorker (idx , {
        listId :  optReturnListId || optListId || undefined,  
      }));

}
/**
  * get value(s) at the specified index in the given list
  * @param {string} listName the name of the list to find it in
  * @param {number|<array>.number} idx the item number starting at 1, or an array of start positions.
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches
  * return {*|<array>.*} the found value(s)
  */
function blisterIndex ( listName, idx , optListId, optSortId, optSortDescending ) {  
    
    // get a new blister
    var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } );
    
    return blister.transposeArray(b.indexWorker (idx, { 
        listId : optListId || undefined
      }));
}
/**
  * lookup the values in a blister and return the position of the match(es)
  * @param {*} value  we're looking for
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches
  * return {*|<array>.*} the found value(s)
  */
function blisterMatch (listName,value, optListId, optSortId, optSortDescending,  optMaxMatch ) {

    // get a new blister
    var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} );
    
    return blister.transposeArray(b.matchWorker ( value, { 
        listId : optListId || undefined,  
        maxMatch: optMaxMatch || undefined
      }));
}
/**
  * return the unique values in a list column
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return,  0 is all matches and the default
  * @param {string=} optFilterId can be a column name, or a column number starting at/default 1
  * @param {string=} optFilterValue value the filter has to be.. there can be lots of these
  * return {<array>.*} the array of unique values
  */
function blisterUnique ( listName, optListId,   optSortId, optSortDescending,  optMaxMatch ,optFilterId, optFilterValue ) {
  
    
    // get a new blister
    var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} );
    
    return blister.transposeArray(b.uniqueWorker ( { 
        listId : optListId || undefined,  
        maxMatch: optMaxMatch || undefined,
        filters: makeFilterArgs (5, Array.slice(arguments))
      } ));
                                            
}
/**
  * return the values in a list column
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return,  0 is all matches and the default
  * @param {string=} optFilterId can be a column name, or a column number starting at/default 1
  * @param {string=} optFilterValue value the filter has to be.. there can be lots of these
  * return {<array>.*} the array of values
  */
function blisterList (  listName, optListId,   optSortId, optSortDescending,  optMaxMatch , optFilterId, optFilterValue  ) {
  
  // get a new blister
  var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } );
    
  return blister.transposeArray(b.listWorker ( { 
          listId : optListId || undefined,  
          maxMatch: optMaxMatch || undefined,
          filters: makeFilterArgs (5, Array.slice(arguments))
        } ));
  
}

/**
  * return the headers in a list column
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optListId can be a column name, or a column number starting at/default 1
  * return {<array>.*} the array of values
  */
function blisterHeaders (  listName, optListId  ) {
  
  // get a new blister
  var b = blisterAssister (listName, {} );
    
  return b.getListHeaders ( optListId || undefined );
  
}
/**
  * return the description of a blister
  * @param {string} listName the name of the list to find it in
  * return {string} the description
  */
function blisterDescription (  listName ) {
  
  // get a new blister
  var b = blisterAssister (listName, {} );
  return b.getDescription ();
}
/**
  * return the last update date of a blister
  * @param {string} listName the name of the list to find it in
  * return {string} the description
  */
function blisterUpdateDate (  listName ) {
  
  // get a new blister
  var b = blisterAssister (listName, {} );
  return b.getUpdateDate ();
}
/**
  * return the last update date of a blister
  * @param {string} optLibrary the name of the library to directory - default, this one
  * return {<array>.String} the descriptions
  */
function blisterDirectory (optLibrary) {
  
  // get a directory of blisters in this library
  var packages = blister.getBlisterPackages ( { db: blisterPickScriptDb( {library: optLibrary || undefined} )});
  var dir =[];
  for (var i=0;i<packages.length;i++){
    dir.push ([]);
    dir[dir.length-1].push(packages[i].package.name);
    dir[dir.length-1].push(packages[i].package.description);
  }
  return dir;
}
/**
  * return all the values in a list
  * @param {string} listName the name of the list to find it in
  * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort
  * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending
  * @param {number=} optMaxMatch maximum matches to return,  0 is all matches and the default
  * @param {string=} optFilterId can be a column name, or a column number starting at/default 1
  * @param {string=} optFilterValue value the filter has to be.. there can be lots of these
  * return {<array>.*} the array(s) of values
  */
function blisterData ( listName,   optSortId, optSortDescending,  optMaxMatch , optFilterId, optFilterValue) {  
  
  // get a new blister
  var b = blisterAssister (listName,  
      {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } );
    
  return blister.transposeArray(b.listWorker ( { 
        maxMatch: optMaxMatch || undefined,
        listId: null,
        filters: makeFilterArgs (4, Array.slice(arguments))
      } ));

}
/**
  * return an exsiting or a new blister
  * @param {string} listName the name of the list to find it in
  * @param {options} options any options passed to the function
  * return {cBlister} the blister to use
  */

function blisterAssister ( listName,  options) {
  
  // the default place to look is in the scriptDB associated with this sheet. 
  // change it by either specifying a library name that blister knows about 
  // eg. options.library = "blister" (this is an optional param on all the custom functions)
  // or by passing some library scriptdb below
  // if you do not want to pass any scriptdbs, just delete the blisterPickScriptDb argument below, and it will only be able
  // to operate on the global blister list.
  options = options || {};
  // the library can prefix the listName
  var lib = listName.split (".");
  if (lib.length > 1) {
    listName = lib[1];
    options.library = lib[0];
  }
  
  return blister.blisterHelper (listName,  options ,  blisterPickScriptDb(options) );  
}
/**
 * return the scriptDB to pass to blister
 * @param {options} options any options passed to the function - here we are interested in options.library
 * return {null|scriptDBInstance} the scriptdb to use
 */
function blisterPickScriptDb(options) {
  
  // to use the library associated with this spreadsheet - options.library is blank
  // to use another library, options.library should be the name of a library (which needs to be known by this script)
  // to use the global blister library, options.library = "blister"
  
  if (options.library === 'blister' ) {
    // use global blister library - no action required - blister will take care of it
    return null;

  }
  else  {
    // here you need to find a way to pass the scriptdb of the given library.
    // by convention, I create a function in any library that needs to share its scriptdb as showMyScriptDb()
    try {
      return eval ( (options.library ? options.library + '.' : '' ) + 'showMyScriptDb')();
    }
    catch (e) {
      throw ("error " + e + " opening library " + options.library);
    }
  }

}
function makeFilterArgs (filterArgStartsAt, args) {
    var filterArgs;
    if (args.length > filterArgStartsAt) {
      filterArgs = [];
      for (  var i = filterArgStartsAt; i < args.length ; i += 2) {
       filterArgs.push( {listId:args[i], value:args[i+1] } );
      }
    }
    return filterArgs;
}

The examples showing how to set up validation and populate lists

You need this in your local workbook. You can find it in the examples sheet. These are the custom functions you can call directly from your sheets. The function documentation is here
function generateValidations () {
  // this is where you maintain the validations to apply to this sheet.
  return ( { blisters: [
        { listName : 'blister.currencies' ,  blister: [ {listId: 'ISO', sortId: 'ISO' } ], applies: [ {sheet:'enterCurrencies', range:'a2:a'} ] },
        { listName : 'car_list' , applies: [ {sheet:'enterCars', range:'a2:c'} ] },
        { listName : 'blister.airlines' , applies: [ {sheet:'enterAirlines', range:'a2:a'} ] }
      ] }) ;
}

// these need to be in your script but dont need touched
function onOpen() {
  // this runs on opening and applies all the known validations
  
  // this applies current known validations for this sheet and should be triggered onOpen
  return blister.applyValidations( generateValidations(), blisterAssister ) ;
}

function onEdit(e) {
   
   if(e.range.getDataValidations()) { 
     reApplyIfNeeded (e,generateValidations());
   }
}


function reApplyIfNeeded(e, blisterValidations) {

  return blister.reApply ( e, blisterValidations, blisterAssister);

}

function blisterFromList () {

  
  new blister.cBlister('car_list', { db: showMyScriptDb() , sortId:'make', description: 'local test using car inventory list'})
                      .makeBlisterFromRange( blister.getRangeFromItem({sheet:'Sheet1', range:'a:d'}), true);
  
  new blister.cBlister('languageCodes', { db: blister.showMyScriptDb() , sortId:'language', 
                                                description : 'list of language codes by country'})
                      .makeBlisterFromRange( blister.getRangeFromItem({sheet:'languageCodes', range:'a:b'}), true);      

}


function loadCurrencies () {
 new blister.cBlister('currencies', { db: blister.showMyScriptDb() , sortId:'Country', description: 'list of currencies by country'})
     .makeBlisterFromRange( blister.getRangeFromItem({sheet:'Currencies', range:'a:c'}), true);      
}

function loadLanguageCodes () {
 new blister.cBlister('languages', { db: blister.showMyScriptDb() , sortId:'language', description: 'list of language codes'})
     .makeBlisterFromRange( blister.getRangeFromItem({sheet:'languageCodes', range:'a:b'}), true);      
}

function loadCse() {
  loadCseLanguageCodes();
  loadCseParameters();
}

function loadCseLanguageCodes () {
 new blister.cBlister('cselanguages', { db: blister.showMyScriptDb() , sortId:'language', description: 'list of cse language codes'})
     .makeBlisterFromRange( blister.getRangeFromItem({sheet:'cseLanguages', range:'a:h'}), true);      
}

function loadCseParameters () {
 new blister.cBlister('cseparameters', { db: blister.showMyScriptDb() , sortId:'item', description: 'list of cse defaults and parameters'})
     .makeBlisterFromRange( blister.getRangeFromItem({sheet:'cseParameters', range:'a:b'}), true);      
}

function caution() {
  deleteAll(blister.showMyScriptDb());
  //deleteAll(showMyScriptDb());
  //deleteAll(university.showMyScriptDb());
}

function deleteAll(db) {

  while (true) {
    var result = db.query({}); // get everything, up to limit
    if (result.getSize() == 0) {
      break;
    }
    while (result.hasNext()) {
       db.remove(result.next());
    }
  }
}
function showAll(optDb) {
  var db = optDb || showMyScriptDb();
  var results = db.query({});

  while (results.hasNext()) {
    var result = results.next();
    Logger.log(JSON.stringify(result));
  }
}

function showAllBlister() {
  showAll (blister.showMyScriptDb());
}

function showAllUniversity() {
  showAll (university.showMyScriptDb());

}
The rest of the code is shared out from the blister library . You’ll need to add resource MTP3ATeczKthcdvb_u278yiz3TLx7pV4j to any sheets or scripts using these functions. Documentation is here.

The cBlister Object

/** updated this to be a data abstraction handler
 */
/**
  * create a cBlister object
  * @param {string} name name to give this new blister
  * @param {object=} optOptions options (db,sortId,sortDescending)
  * return {cBlister} a new cBlister object
  */
  
var cBlister = function (name, optOptions) {
  var self = this;
  var pName = name;
  var pOptions = optOptions || {};
  var FAKELISTCOLUMN = '__listColumn';
  var pContent = null;
  
  var pSilo = new siloItem("", pOptions.db || showMyScriptDb());
  var pSortId = pOptions.sortId;
  var pSortDescending = pOptions.sortDescending;
  var pDescription = pOptions.description ;
  
  
  if (!pName) throw ('new blister needs a name');
  
  /**
  * get the name of the blister
  * return {string} the blister name
  */
  
  self.name = function () {
    return pName;
  }
  /**
  * search template for this blister
  * return {object} the template for querying
  */
  
  self.template = function () {
    return  {  package: { name: self.name()  } };
  }
  
  
  
  /**
 * create an empty data place
 * return {object} an empty blister
 */
   
   self.empty = function () {
     pContent = self.emptyPackage();
     return pContent;
   }

  /**
 * create an empty data place
 * return {object} an empty blister
 */
   
   self.emptyPackage = function () {
     var p  = self.template();
     p.package.items = [];
     p.package.keys = [];
     return p;
   }
  
  /**
  * get the description of the blister
  * return {string} the blister description
  */
  
  self.getDescription = function () {
    var c= self.content();
    return (c && c.package.description) ?  c.package.description  : self.name() ;
  }
  
 /**
  * get the update date of the blister
  * return {string} the blister description
  */
  
  self.getUpdateDate = function () {
    var c= self.content();
    return c ? new Date(c.updateDate) : null ;
  }
  
  /**
  * get the any options for the blister
  * return {string} the blister name
  */
  
  self.options = function () {
    return pOptions;
  }

  self.items = function () {
    return self.content().package.items
  }

  
/**
  * replace or add the blister with current pContent 
  * return {cBlister} the blister for chaining
  */
  
  self.replace= function (optNewContent) {

    // write update
    if (!isUndefined(optNewContent)) pContent = optNewContent;
    if (!pContent || ! pContent.package) throw ('trying to write null content for ' + self.name());
    self.sort();
    
    // if exists, delete it, then write it
    pSilo.remove (self.template());
    pContent.updateDate = new Date().getTime();
    pContent.package.description = pDescription || self.getDescription();
    
    pSilo.save (pContent);
    return self;
  }

/**
  * sort the current payload according to the column specified in pSortId
  * return {cBlister} the blister for chaining
  */
  
  self.sort = function () {
    // sort the content
    if (!pSortId || !pContent.package.keys.length || !pContent.package.items.length) return self;
    
    var kInx = self.getKeyItemIndex (pSortId)
    if (kInx === -1) { 
      //.. just dont sortthrow ("invalid sortKey " + pSortId);
      return self;
    }

    
    // since we have to sort multiple columns, better to sort a map of the data
    var map = pContent.package.items[kInx].map(function(e, i) {
        return {index: i, value: e};
      });

    // sort the map 
    map.sort(function(a, b) {
      return a.value === b.value ? 0 : ( pSortDescending ? (a.value < b.value ? 1 : -1) : (a.value > b.value ? 1 : -1)) ;
    });

    // reconstitute from the map order
    for ( var i = 0 ; i < pContent.package.items.length; i++) {
        var ns = map.map(function(e) {
            return pContent.package.items[i][e.index];
          });          
        pContent.package.items[i] = ns;
    }

    return self;
  } 
  /**
  * lookup the values in a blister and return the position of the match(es)
  * @param {*} value  we're looking for
  * @param {options} options sortId,sortDescending,maxMatch,library
  * return {*|<array>.*} the found value(s)
  */

  self.matchWorker = function  ( value, options ) {
    options = options || {};
    var maxMatch = fixOptional ( options.maxMatch , 1);
    var data = self.getListValues (options.listId);
    if (data) {
      if (maxMatch === 1) {
        var idx = data.indexOf(value);
        if(idx != -1) {
          // all is good
          return idx+1;
        }
        else {
          throw ("no matches for " + value + " in list " + self.name());
        }
      }
      else {
        // will provide an array of results
        var results = [];
        for ( var i=0; i < data.length && (results.length < maxMatch || maxMatch === 0 )  ; i++) {
          if (data[i] === value ) results.push(i+1);
        }
        return results;
      }
    }
    else if (isUndefined(data)) {
      throw (self.name() + ":" + options.listId +  ":" + " combination not found in library " + self.options().library); 
    }
    return data;
  }
  
  /**
  * get value(s) at the specified index in the given list
  * @param {number|<array>.number} idx the item number starting at 1, or an array of start positions.
  * @param {options} options sortId,sortDescending,maxMatch,library
  * return {*|<array>.*} the found value(s)
  */
  
  self.indexWorker = function ( idx , options ) {
    options = options || {};
    var listId = options.listId || 1;
  
    // get the list required
    var data = self.getListValues(listId);
    if (data) {
      if (isArray(idx)) {
        var results = [];
        for (var i =0; i < idx.length ; i++ ){
          results.push(data[idx[i]-1]);
        }
        return results;
      }
      else {
        return data[idx-1];
      }
    }
    else if (isUndefined(data)) {
      throw (self.name()  + ":" + listId + ":" + " combination not found in library " + self.options().library);
    }
    return data;
  }

  /** 
   * add a new column to the list
   */
   
  self.findOrAddListId = function (listId) {
    var heading, c = self.content();
    
    // if it's undefined then we'll be adding one with no header
    if (isUndefined(listId)) { 
      listId = pContent.package.keys.length();
      heading = FAKELISTCOLUMN + listId;
    }
    else {
      heading = listId;
    }
    
    // cheack it doesnt exist
    var k = self.getKeyItemIndex(listId);
    if (k < 0 ) {
      // need to add it

      pContent.package.keys.push(heading);
      pContent.package.items.push([]);
      k = pContent.package.keys.length-1;
      self.replace();
    }
    
    return k;
  }
/**
  * given a listId return the column index number 
  * @param {string|number} listId can be a column name, or a column number starting at 1
  * return {number} the column index number starting at 0, -1 if doesnt exist
  */
  
  self.getKeyItemIndex = function(listId) {

    if(self.content()) {
      var kName = listId, kInx =  parseInt(listId);
      if ( !isNaN(kInx) && pContent.package.keys.length >= parseInt(listId) && parseInt(listId) >=1 ) {
        return kInx - 1;
      }
      else {
        for (var i =0 ; i < pContent.package.keys.length ; i++) {
           if (pContent.package.keys[i] === kName) return i;
        }
        return -1;
      }
    }
    else {
      return -1;
    }
  }
  
/**
  * return the values in a list column
  * @param {options} options listId,sortId,sortDescending,maxMatch,library
  * return {<array>.*} the array of values
  */
  self.listWorker = function  (options) {

    options = options || {};
    var listId = fixOptional( options.listId , 1);
    var maxMatch = fixOptional ( options.maxMatch , 0);
  
    // get the list required
    var result = self.getListValues(listId, options.filters);
    
    if (result) {
      if (maxMatch === 0 || result.length === 0) {
        return result;
      }
      else {
        if (isArray(result[0])) {
          // its a multi dimentsional thing
          var data =[];
          for ( var i=0; i < result.length ; i++ ) {
            data.push( result.slice (0,Math.min(maxMatch,result[i].length)));
          }
          return data;
        }
        else {
          return [result.slice (0,Math.min(maxMatch,result.length))];
        }
      }
    }
    else if (isUndefined(result)) {
        throw (self.name() + ":" + listId + ":" + " combination not found in library "+JSON.stringify(self.content()));
  
    }
    return result;
    
  }  
/**
  * given a listId return the headings for that column (or all columns), or null if invalid listId
  * @param {string|number=} optListId can be a column name, or a column number starting at 1
  * return {<Array>.*|null|undefined} the data associated with the given listId
  */
  
  self.getListHeaders = function(optListId) {
    
    var p = self.content();
    if (p) {
      
      var data = p.package.keys;

    // narrow down to a particular column if necessary
     if (optListId) {
       var idx = self.getKeyItemIndex (optListId);
       if (idx < 0 ) {
         return undefined;
       }
       else {
         return data[idx];
       }
     }
     else {
        return data;
      }
    }
    return p;

  }
  

/**
  * given a listId return the data for that column (or all columns), or null if invalid listId
  * @param {string|number=} optListId can be a column name, or a column number starting at 1
  * @param {<Array>.object|null} optFilters listid and value pairs that have to match for data to be included
  * return {<Array>.*|null|undefined} the data associated with the given listId
  */
  
  self.getListValues = function(optListId,optFilters) {
    
    var p = self.content();
    if (p) {
      
      var data = p.package.items;
      if (optFilters) {
       // need to do this row-wise, currently its columnwise
        var data = transposeArray(data);
        
        // apply each filter, only including rows that all the filters are true
        for ( var i=0; i < optFilters.length;i++) {
          var filter = optFilters[i];
          
          // get the filter column
          var k = self.getKeyItemIndex (filter.listId);
          if (k < 0) { 
            return undefined ;
          }
          
          // an undefined filter value will be the same as no filter
          if (!isUndefined(filter.value)) {
            data = data.filter ( function (v,ind,tab) { 
              return filter.value === v[k] ;
            } );
          }
           
        }
        // organize back to column wise
        data = transposeArray(data);
      }

    // narrow down to a particular column if necessary
     if (optListId) {
       var idx = self.getKeyItemIndex (optListId);
       if (idx < 0 ) {
         return undefined;
       }
       else {
         return data[idx];
       }
     }
     else {
        return data;
      }
    }
    return p;

  }
  
  
/**
  * return the unique values in a list column
  * @param {options} options listId,sortId,sortDescending,maxMatch,library
  * return {<array>.*} the array of unique values
  */
  self.uniqueWorker  = function  (options) {
    
    var listId = options.listId || 1;
    var maxMatch = fixOptional ( options.maxMatch , 0);
  
      // get the list required
    var data = self.getUniqueListValues(listId, options.sortId, options.filters);
    if (data) {
      if (maxMatch === 0) {
        return data;
      }
      else {
        return data.slice (0,Math.min(maxMatch,data.length));
      }
    }
    else if (isUndefined(data)) {
      throw (self.name() + ":" + listId + ":" + " combination not found in library" + self.options().library);
    }
    return data;
  }
  
  
/**
  * given a listId return the unique values in a a list
  * @param {string|number} listId can be a column name, or a column number starting at 1
  * @param {boolean} optSort whether to sort the list
  * return {<Array>.*|null} the unique data values in a list
  */
  
  self.getUniqueListValues = function (listId, optSort, optFilters) {
    var item = self.getListValues(listId,optFilters);
    if (item) {
        var uniqueList =  item.filter ( function ( v , i , t) {
          return t.indexOf (v) === i;
        });
        if(optSort) uniqueList.sort();
        return uniqueList;
    }
    return item; 
  }
  

/**
  * get the existing pContent for a list. First time it will get from scriptDB
  * @param {boolean} optRefresh if true will always refresh pContent first
  * return {object} the data as written to scriptDb
  */
  
  self.content = function (optRefresh) {
    if (optRefresh) { 
      pContent = self.dbContent();
      if(!pContent) { 
        pContent= self.emptyPackage(); 
      }
      else {
        self.sort();
      }
    }

    return pContent;
  }


/**
  * get the existing pContent for a list. First time it will get from scriptDB
  * return {object} the data as written to scriptDb
  */
  
  self.dbContent = function () {
    // get from scriptdb
    var results = pSilo.query ( self.template() );
    return results.hasNext() ? results.next() : null;
  }
  
/**
  * given a range will  replace the content for this blister and write it to scriptDB
  * @param {Range} sourceRange if true will always refresh pContent first
  * @param {boolean} optHeadings if true will use the first row headings as the the listIDs
  * @param {boolean} optIncludeEmpty if false will stop at first completely empty row
  * return {cBlister} the generated blister
  */
    
  self.makeBlisterFromRange = function(sourceRange,optHeadings,optIncludeEmpty) {
    pContent = self.getBlisterFromRange (sourceRange, optHeadings,optIncludeEmpty);
    return self.replace ();
  }

/**
  * given a range will construct a cBlister payload 
  * @param {Range} sourceRange if true will always refresh pContent first
  * @param {boolean} optHeadings if true will use the first row headings as the the listIDs
  * @param {boolean} optIncludeEmpty if false will stop at first completely empty row
  * return {cBlister} the cBlister payload
  */
  
  self.getBlisterFromRange = function (r, optHeadings,optIncludeEmpty) {
    var data = r.getValues(), 
      headings = fixOptional(optHeadings, false),
      maxRow = 0;
    
    var obData = self.emptyPackage();
    
    // srt out headings - can be named from column headings, or numeric
    if (data.length) {
      // keys
      for (var i=0; i < data[0].length; i++) {
        obData.package.keys.push( headings ? data[0][i] : FAKELISTCOLUMN + (i+1) );
       }
       // data
       for (var i=0; i < data[0].length; i++) {
         var d = [];
         for (var j = (headings ? 1 : 0) ; j < data.length; j++) {
          d.push( data[j][i] );
          if (data[j][i]) maxRow = j;
         }
         obData.package.items.push(d);
       }
    }
    // trim trailing blank rows
    if (!optIncludeEmpty && maxRow < data.length -1) {
      for (var i=0;i<obData.package.keys.length;i++) {
        obData.package.items[i] = obData.package.items[i].splice(0,maxRow);
      }
    }
    return obData;
  }
  pContent = self.content(true);
  return self;
}
/* helps to set up a new blister object
 */
function blisterHelper (listName,options , optLocalLibrary ) {

  
  // if a library is specified then we use that
  if ( options.library != "blister") {
     options.db = optLocalLibrary;
     if (!options.db) throw ("unknown library " + options.library);
  }
  
  return new cBlister (listName, options );

}

cBlister support functions

/**delete everything**/
function deleteAll() {
  var p = publicStuffDb() ;
  var results = p.query({});
  while (results.hasNext()) {
    p.remove(results.next());
  }
}
/**
 * throw an exception if not true
 * @param {*} arg given value
 * @param {*} defaultValue value to use if given value IsMissing
 * @return {*} the new value 
 */
function fixOptional (arg, defaultValue) {
  if (isUndefined(arg) ){
    return defaultValue;
  }
  else 
    return arg;
}

// got this here 
// https://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
/**
 * get detailed type of javaScript var
 * @param {*} obj given item
 * @return {string} type
 */
function toType(obj) {
  return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
}
function isObject (x) {
  return typeof x === 'object';
}
/**
 * Check if a value is an array
 * @param {*} arg given item
 * @return {boolean} true if array
 */
function isArray (arg) {
  return toType(arg) === 'array';
}

/**
 * Check if a value is defined
 * @param {*} arg given value
 * @return {boolean} true if undefined
 */
function isUndefined ( arg) {
  return typeof arg === 'undefined';
}
function transposeArray (a) {
  if (!a) return a;
  if (!isArray(a)  ) return a;
  if(!a.length) return [[]];
  if (!isArray(a[0])) a = [a];
  return Object.keys(a[0]).map(function (c) { return a.map(function (r) { return r; }); });
}

/**
 * return the db silo item associated with the given key
 * @param {string} k the key
 * @param {ScriptDbInstance=} db the  scriptDB
 * @return {siloItem} the cScriptDbSiloItem
 */
function siloItem(k,db) {
// find the Silo associated with this key
    var pdb = db;
    var self = this;
    var pSiloId =k;
    
/**
 * remove the silo item with the given key
 * @param {object} optOb the item to remove, default all of them
 * @return {siloItem} the siloItem
 */
    self.remove = function (optOb) {
      var qob = self.template(optOb);
    
  // delete all matches
      var result = pdb.query(qob);
      while (result.hasNext()) {
        pdb.remove(result.next());
      }
      return self;
    }

  
/**
 * create template for this silo
 * @param {object|null=} optOb the item to add to the template, default none
 * @return {object} the cScriptDbSiloItem
 */
    self.template = function (optOb) {
      var qob = fixOptional(optOb,{});
      if(pSiloId)qob.siloId = pSiloId;
      return qob;
    }
    
/**
 * do a query within the silo for this siloItem
 * @param {object|null=} optOb the query by example (default all data)
 * @return {ScriptDBquery} a query by example to silo the data
 */
    self.query = function (optOb) {
      return pdb.query(self.template(optOb));
    }

/**
 * do a save within the silo for this siloItem
 * @param {object} ob the bject to save
 * @return {object} the object that was saved
 */
    self.save = function (ob) {
      return pdb.save(self.template(ob));
    }
    

    return self;
}

function getBlisterPackages (options) {
  // get all blisters in a given db
  var o = options || {};
  var s = new siloItem("", o.db || getDefaultDB("blister"));
  var r = s.query(), t = [];
  while (r.hasNext()) {
    t.push (r.next());
  }
  return t;
}
function getRangeFromItem (item) {
  return SpreadsheetApp.getActiveSpreadsheet().getSheetByName(item.sheet).getRange(item.range);
}

The cValidation object

// use blisters to manage validation
// a blisterrule looks like this
// { blister: [ {listId:'xx', dependent: true , sortId:'optxx', sortDescending: false},{},{} ..] }

function cValidation(b, optBlisterRule) {
  var self = this;

/**
 * check that these rules go with this blister 
 * @return {boolean} good or not.
 */
  self.validate = function () {
    for (var i= 0 ; i < pRules.blister.length ; i++ ) {
      if( pBlister.getKeyItemIndex(pRules.blister[i].listId) < 0 ) return false;
    }
    return true;
  }
  
/**
 * check that this range goes with this validation
 * @return {boolean} good or not.
 */
  self.worksWith = function (range) {
      return (pRules.blister.length === range.getNumColumns() || !pRules.blister.length) && self.validate();
  } 
  
/**
 * make an empty blisterRule
 * @return {boolean} good or not.
 */
  self.emptyBlisterRule = function () {
      return { blister: [] };
  } 
  
/**
 * create a default rule to match this range
 * @param {Range} range the range to apply it to
 * @return {object} a default blisterrule
 */  
 
 self.defaultBlisterRule = function (range) {
   var p = self.emptyBlisterRule(), nc = range.getNumColumns() ;
   for ( var i = 0 ; i < nc ; i++ ) {
     var d = { listId: i +1 , sortId: i +1 , filters: null ,  sortDescending: false};
     // filter on previous columns
     if (i > 0 ) {
       for ( var filters=[],j = 0; j < i ; j++) {
          filters.push ( {listId: j+1, value: null } );
       }
       d.filters = filters;
     }
     p.blister.push (d);
   }
   return p;
 }
  
  
/**
 * apply this validation
 * @param {Range} range the range to apply it to
 * @return {cValidation} the self for chaining
 */  
  
  self.apply = function (range) {
  
    if (!self.worksWith(range)) throw ("attempt to apply mismatched blister to range");
    pRange = range;
    pValidations = [];
   
     // use given rules or set up defaults
    var r = pRules.blister.length ? pRules.blister : self.defaultBlisterRule(range).blister;
    var data = null,nr = pRange.getNumRows(), nc= pRange.getNumColumns() ;

    for ( var i = 0 ; i < r.length ; i ++ ) {
      if (!r[i].filters) {
        // we can do the whole column at once
        var f =  pBlister.getUniqueListValues (r[i].listId, r[i].sortId);
    
        if (f) {
          // bugette patch
          f.push('');
          var p = SpreadsheetApp.newDataValidation()
                .setAllowInvalid(false)
                .requireValueInList(f )
                .build();
          pRange.offset(0,i,nr,1).setDataValidation (p);
        }
      }
      else {
        // we have to do it row by row
        if (!data) data = pRange.getValues();
        for ( var j = 0 ; j < nr ; j++) {
          // filter on the data values
          for (var k=0; k < r[i].filters.length; k++) {
            r[i].filters[k].value = data[j][k];
          }  
          
          // apply dymanic filter to individual cell 
          var f = pBlister.getUniqueListValues  (r[i].listId, r[i].sortId, r[i].filters);
         
          if (f) {
            // bugette patch
            f.push('');
            var p = SpreadsheetApp.newDataValidation()
                  .setAllowInvalid(false)
                  .requireValueInList(f)
                  .build();
            pRange.offset(j,i,1,1).setDataValidation (p);
           
          }
        }
      }

  }
  
  return self;
}
  
/**
 * return validations created by this 
 * @return {<array>.DataValidation} the array of validations
 */
 
  self.getValidations = function () {
    return pValidations;
  }
   
  
/**
 * return latest used range for this validation 
 * @return {Range} the range
 */
 
  self.getRange = function () {
    return pRange;
  }
  
/**
 * return latest used range for this validation 
 * @return {cValidation} itself for chaining
 */
 
  self.setRange = function (range) {
    pRange = range;
    return self;
  }


  var pBlister = b;
  if (optBlisterRule && optBlisterRule.blister) {
    pRules = optBlisterRule;
  }
  else {
    pRules = self.emptyBlisterRule();
  }

  var pRange = null;
  var pValidations = [];
  
  return self;
  
}


function intersect (e, target) {
  // does the range returned by onedit intersect with the one given by onEdit
  return target.getSheet().getName() == e.getSheet().getName() && 
         target.getRow() <= e.range.rowEnd &&
         target.getLastRow() >= e.range.rowStart &&
         target.getColumn() <= e.range.columnEnd  &&
         target.getLastColumn() >= e.range.columnStart ; 
}

function clearKnownValidations(v) {
  
  for (var i=0; i < v.blisters.length; i++) {
    for ( var j =0; j < v.blisters[i].applies.length; j++) {
       getRangeFromItem(v.blisters[i].applies[j]).clearDataValidations();
    }
  }
}

function applyValidations (blisterValidations,assister) {
  
  if (blisterValidations) {
      clearKnownValidations (blisterValidations);
      for (var i=0; i < blisterValidations.blisters.length ; i++ ) {
        var b = assister ( blisterValidations.blisters[i].listName ) ;
        var v = new cValidation (b, blisterValidations.blisters[i] );
        for ( var j = 0 ; j < blisterValidations.blisters[i].applies.length ; j++ ) {
          var r = getRangeFromItem(blisterValidations.blisters[i].applies[j]);
          r.clearDataValidations();
          v.apply(r);
        }
    }
  }
  return blisterValidations;
}

function reApply(e, blisterValidations,assister) {

  
  if (blisterValidations && e.range.getDataValidations()) {
       
    // lets see if this intersects with any known validation
          for (var i=0; i < blisterValidations.blisters.length ; i++ ) {
            var b = null,v;
            
            for ( var j = 0 ; j < blisterValidations.blisters[i].applies.length ; j++ ) {
              var r = getRangeFromItem(blisterValidations.blisters[i].applies[j]);
              
              if ( intersect ( e, r) ) {
                 
                if (!b) {
                    
                    b = assister ( blisterValidations.blisters[i].listName ) ;
                    v = new cValidation (b, blisterValidations.blisters[i] );
                }
               
                var eRow = e.range.offset(0,r.offset(0,0,1,1).getColumn()-e.range.getColumn(),1,r.getNumColumns());
                eRow.clearDataValidations();
                v.apply(eRow);
              }
           }
        }
    }
}

All comments, suggestions, assistance, good lists are welcome as I develop this capability. You can get me on Google plus, Twitter or this forum.