Skip to content
This repository has been archived by the owner on Mar 4, 2019. It is now read-only.

Commit

Permalink
Merge pull request #108 from dmfay/views
Browse files Browse the repository at this point in the history
View support
  • Loading branch information
robconery committed Aug 16, 2015
2 parents efb3c2e + 26b9aa4 commit 8b86609
Show file tree
Hide file tree
Showing 8 changed files with 831 additions and 566 deletions.
113 changes: 73 additions & 40 deletions index.js
@@ -1,6 +1,7 @@
var Runner = require("./lib/runner");
var _ = require("underscore")._;
var fs = require("fs");
var Queryable = require("./lib/queryable");
var Table = require("./lib/table");
var util = require("util");
var assert = require("assert");
Expand All @@ -19,6 +20,7 @@ var Massive = function(args){
_.extend(this,runner);

this.tables = [];
this.views = [];
this.queryFiles = [];
this.schemas = [];
this.functions = [];
Expand All @@ -33,8 +35,8 @@ var Massive = function(args){
// any "truthy" value passed will cause functions to be excluded. No param
// will be a "falsy" value, and functions will be included...
this.excludeFunctions = args.excludeFunctions;
this.functionBlacklist = this.getTableFilter(args.functionBlacklist)
}
this.functionBlacklist = this.getTableFilter(args.functionBlacklist);
};

Massive.prototype.getSchemaFilter = function(allowedSchemas) {
// an empty string will cause all schema to be loaded by default:
Expand Down Expand Up @@ -81,42 +83,63 @@ Massive.prototype.getTableFilter = function(filter) {
Massive.prototype.run = function(){
var args = ArgTypes.queryArgs(arguments);
this.query(args);
}
};
Massive.prototype.runSync = DA(Massive.prototype.run);

Massive.prototype.loadQueries = function() {
walkSqlFiles(this,this.scriptsDir);
};


Massive.prototype.loadTables = function(next){
Massive.prototype.loadTables = function(next) {
var tableSql = __dirname + "/lib/scripts/tables.sql";
var parameters = [this.allowedSchemas, this.blacklist, this.exceptions];
var self = this;

// ONLY allow whitelisted items:
if(this.whitelist) {
tableSql = __dirname + "/lib/scripts/whitelist.sql";
var parameters = [this.whitelist]
parameters = [this.whitelist];
}
this.executeSqlFile({file : tableSql, params: parameters}, function(err,tables){
if(err){
next(err,null);
}else{
_.each(tables, function(table){
var _table = new Table({
schema : table.schema,
name : table.name,
pk : table.pk,
db : self
});
// This refactoring appears to work well:
MapTableToNamespace(_table);

this.executeSqlFile({file : tableSql, params: parameters}, function(err,tables) {
if (err) { return next(err, null); }

_.each(tables, function(table){
var _table = new Table({
schema : table.schema,
name : table.name,
pk : table.pk,
db : self
});
next(null,self);
}

MapToNamespace(_table);
});

next(null,self);
});
};

Massive.prototype.loadViews = function(next) {
var viewSql = __dirname + "/lib/scripts/views.sql";
var parameters = [this.allowedSchemas, this.blacklist, this.exceptions];
var self = this;

this.executeSqlFile({file : viewSql, params: parameters}, function(err, views){
if (err) { return next(err, null); }

_.each(views, function(view) {
var _view = new Queryable({
schema : view.schema,
name : view.name,
db : self
});

MapToNamespace(_view, "views");
});

next(null, self);
});
}
};

Massive.prototype.saveDoc = function(collection, doc, next){
var self = this;
Expand Down Expand Up @@ -153,7 +176,7 @@ Massive.prototype.saveDoc = function(collection, doc, next){
if(err){
next(err,null);
} else {
MapTableToNamespace(_table);
MapToNamespace(_table);
// recurse
self.saveDoc(collection,doc,next);
}
Expand All @@ -162,23 +185,26 @@ Massive.prototype.saveDoc = function(collection, doc, next){
};
Massive.prototype.saveDocSync = DA(Massive.prototype.saveDoc);

var MapTableToNamespace = function(table) {
var db = table.db;
if(table.schema !== "public") {
schemaName = table.schema;
var MapToNamespace = function(queryable, collection) {
collection = collection || "tables";

var db = queryable.db;

if (queryable.schema !== "public") {
schemaName = queryable.schema;
// is this schema already attached?
if(!db[schemaName]) {
// if not, then bolt it on:
db[schemaName] = {};
}
// attach the table to the schema:
db[schemaName][table.name] = table;
db.tables.push(table);
// attach the queryable to the schema:
db[schemaName][queryable.name] = queryable;
} else {
//it's public - just pin table to the root to namespace
db[table.name] = table;
db.tables.push(table);
db[queryable.name] = queryable;
}

db[collection].push(queryable);
};

Massive.prototype.documentTableSql = function(tableName){
Expand Down Expand Up @@ -261,7 +287,7 @@ Massive.prototype.loadFunctions = function(next){
params.push("$" + i);
}

var newFn, pushOnTo
var newFn, pushOnTo;
if(schema !== "public"){
self[schema] || (self[schema] = {});
newFn = assignScriptAsFunction(self[schema], fn.name);
Expand Down Expand Up @@ -330,19 +356,26 @@ exports.connect = function(args, next){
assert((args.connectionString || args.db), "Need a connectionString or db (name of database on localhost) at the very least.");

//override if there's a db name passed in
if(args.db){
if (args.db) {
args.connectionString = "postgres://localhost/"+args.db;
}
var massive = new Massive(args);

var massive = new Massive(args);

//load up the tables, queries, and commands
massive.loadTables(function(err,db){
massive.loadTables(function(err, db) {
assert(!err, err);
self = db;
massive.loadFunctions(function(err,db){

massive.loadViews(function(err, db) {
assert(!err, err);
//synchronous
db.loadQueries();
next(null,db);

massive.loadFunctions(function(err, db) {
assert(!err, err);
//synchronous
db.loadQueries();
next(null,db);
});
});
});
};
Expand Down
165 changes: 165 additions & 0 deletions lib/queryable.js
@@ -0,0 +1,165 @@
var _ = require("underscore")._;
var assert = require("assert");
var util = require('util');
var Where = require("./where");
var ArgTypes = require("./arg_types");
var DA = require("deasync");

/**
* Represents a queryable database entity (table or view).
* @param {[type]} args [description]
*/
var Queryable = function(args) {
this.schema = args.schema;
this.name = args.name;
this.db = args.db;

// create delimited names now instead of at query time
this.delimitedName = "\"" + this.name + "\"";
this.delimitedSchema = "\"" + this.schema + "\"";

// handle naming when schema is other than public:
if(this.schema !== "public") {
this.fullname = this.schema + "." + this.name;
this.delimitedFullName = this.delimitedSchema + "." + this.delimitedName;
} else {
this.fullname = this.name;
this.delimitedFullName = this.delimitedName;
}
};

//a simple alias for returning a single record
Queryable.prototype.findOne = function(args, next){
if(_.isFunction(args)){
next = args;
args = {};
}

this.find(args, function(err,results){
if(err){
next(err,null);
}else{
var result;

if (_.isArray(results)) {
if (results.length > 0) { result = results[0]; }
} else {
result = results;
}

next(null,result);
}
});
};
Queryable.prototype.findOneSync = DA(Queryable.prototype.findOne);

/**
* Counts rows and calls back with any error and the total. There are two ways to use this method:
*
* 1. find() style: db.mytable.count({field: value}, callback);
* 2. where() style: db.mytable.count("field=$1", [value], callback);
*/
Queryable.prototype.count = function() {
var args;
var where;

if (_.isObject(arguments[0])) {
args = ArgTypes.findArgs(arguments);
where = _.isEmpty(args.conditions) ? {where : " "} : Where.forTable(args.conditions);
} else {
args = ArgTypes.whereArgs(arguments);
where = {where: " where " + args.where};
}

var sql = "select COUNT(1) from " + this.delimitedFullName + where.where;

this.db.query(sql, where.params || args.params, {single : true}, function(err, res) {
if (err) args.next(err, null);
else args.next(null, res.count);
});
};
Queryable.prototype.countSync = DA(Queryable.prototype.count);

//a simple way to just run something
//just pass in "id=$1" and the criteria
Queryable.prototype.where = function(){
var args = ArgTypes.whereArgs(arguments);

var sql = "select * from " + this.delimitedFullName + " where " + args.where;
this.db.query(sql, args.params, args.next);
};
Queryable.prototype.whereSync = DA(Queryable.prototype.where);

Queryable.prototype.find = function(){
var args = ArgTypes.findArgs(arguments);

//set default options
//if our inheriting object defines a primary key use that as the default order
args.options.order = args.options.order || (this.hasOwnProperty("pk") ? util.format('"%s"', this.pk) : "1");
args.options.limit = args.options.limit || "1000";
args.options.offset = args.options.offset || "0";
args.options.columns = args.options.columns || "*";

if(_.isFunction(args.conditions)){
//this is our callback as the only argument, caught by Args.ANY
args.next = args.conditions;
}

var returnSingle = false;
var where, order, limit, cols="*", offset;

if(args.options.columns){
if(_.isArray(args.options.columns)){
cols = args.options.columns.join(',');
}else{
cols = args.options.columns;
}
}
order = " order by " + args.options.order;
limit = " limit " + args.options.limit;
offset = " offset " + args.options.offset;

if(_.isNumber(args.conditions)){
//a primary key search
var newArgs = {};
newArgs[this.primaryKeyName()] = args.conditions;
args.conditions = newArgs;
returnSingle = true;
}

where = _.isEmpty(args.conditions) ? {where : " "} : Where.forTable(args.conditions);

var sql = "select " + cols + " from " + this.delimitedFullName + where.where + order + limit + offset;

if (args.options.stream) {
this.db.stream(sql, where.params, null, args.next);
} else {
this.db.query(sql, where.params, {single : returnSingle}, args.next);
}
};
Queryable.prototype.findSync = DA(Queryable.prototype.find);

Queryable.prototype.search = function(args, next){
//search expects a columns array and the term
assert(args.columns && args.term, "Need columns as an array and a term string");

if(!_.isArray(args.columns)){
args.columns = [args.columns];
}

var tsv;
var vectorFormat = 'to_tsvector("%s")';
if(args.columns.length === 1){
tsv = util.format("%s", args.columns[0]);
}else{
vectorFormat = 'to_tsvector(%s)';
tsv= util.format("concat('%s')", args.columns.join(", ', '"));
}
var sql = "select * from " + this.delimitedFullName + " where " + util.format(vectorFormat, tsv);
sql+= " @@ to_tsquery($1);";

this.db.query(sql, [args.term],next);
};
Queryable.prototype.searchSync = DA(Queryable.prototype.search);

module.exports = Queryable;
20 changes: 20 additions & 0 deletions lib/scripts/views.sql
@@ -0,0 +1,20 @@
-- REQUIRES THREE ARGUMENTS:
-- $1, $2, $2 all must be empty string, or comma-delimited string, or array of string:
select v.table_schema as schema, v.table_name as name
from information_schema.views v
where v.table_schema <> 'pg_catalog' and v.table_schema <> 'information_schema' and (
(case -- allow specific schemas (none or '' assumes all):
when $1 ='' then 1=1
else v.table_schema = any(string_to_array(replace($1, ' ', ''), ',')) end)
and
(case -- blacklist tables using LIKE by fully-qualified name (no schema assumes public):
when $2 = '' then 1=1
else replace((v.table_schema || '.'|| v.table_name), 'public.', '') not like all(string_to_array(replace($2, ' ', ''), ',')) end)
) or (
case -- make exceptions for specific tables, with fully-qualified name or wildcard pattern (no schema assumes public).
when $3 = '' then 1=0
-- Below can use '%' as wildcard. Change 'like' to '=' to require exact names:
else replace((v.table_schema || '.'|| v.table_name), 'public.', '') like any(string_to_array(replace($3, ' ', ''), ',')) end
)
order by v.table_schema,
v.table_name;

0 comments on commit 8b86609

Please sign in to comment.