pg-promise
Version:
PostgreSQL via promises
306 lines (290 loc) • 9.85 kB
JavaScript
;
var $npm = {
fs: require('fs'),
os: require('os'),
minify: require('pg-minify'),
utils: require('./utils'),
format: require('./formatting').as.format,
QueryFileError: require('./errors/queryFile')
};
/**
* @constructor QueryFile
* @description
*
* Represents an external SQL file. The type is available from the library's root: `pgp.QueryFile`.
*
* Reads a file with SQL and prepares it for execution, also parses and minifies it, if required.
* The SQL can be of any complexity, with both single and multi-line comments.
*
* The type can be used in place of the `query` parameter, with any query method directly, plus as `text` in Prepared Statements.
* It never throws any error, leaving it for query methods to reject with {@link QueryFileError}.
*
* For any given SQL file you should only create a single instance of this class throughout the
* application.
*
* @param {String} file
* Name/path of the SQL file with the query. If there is any problem reading the file, it will be
* reported when executing the query.
*
* @param {Object} [options]
* A set of configuration options.
*
* @param {Boolean} [options.debug]
* When in debug mode, the query file is checked for its last modification time on every query request,
* so if it changes, the file is read afresh.
*
* The default for this property is `true` when `NODE_ENV` = `development`,
* or `false` otherwise.
*
* @param {Boolean|String} [options.minify=false]
* Parses and minifies the SQL using $[pg-minify]:
* - `false` - do not use $[pg-minify]
* - `true` - use $[pg-minify] to parse and minify SQL
* - `'after'` - use $[pg-minify] after applying static formatting parameters
* (option `params`), as opposed to before it (default)
*
* If option `compress` is set, then the default for `minify` is `true`.
*
* Failure to parse SQL will result in $[SQLParsingError].
*
* @param {Boolean} [options.compress=false]
* Sets option `compress` as supported by $[pg-minify], to uglify the SQL:
* - `false` - no compression to be applied, keep minimum spaces for easier read
* - `true` - remove all unnecessary spaces from SQL
*
* This option has no meaning, if `minify` is explicitly set to `false`. However, if `minify` is not
* specified and `compress` is specified as `true`, then `minify` defaults to `true`.
*
* @param {Array|Object|value} [options.params]
* Static formatting parameters to be applied to the SQL, using the same method {@link formatting.format as.format},
* but with option `partial` = `true`.
*
* Most of the time query formatting is fully dynamic, and applied just before executing the query.
* In some cases though you may need to pre-format SQL with static values. Examples of it can be a
* schema name, or a configurable table name.
*
* This option makes two-step SQL formatting easy: you can pre-format the SQL initially, and then
* apply the second-step dynamic formatting when executing the query.
*
* @returns {QueryFile}
*
* @see QueryFileError
*
* @example
* // File sql.js
*
* // Proper way to organize an sql provider:
* //
* // - have all sql files for Users in ./sql/users
* // - have all sql files for Products in ./sql/products
* // - have your sql provider module as ./sql/index.js
*
* var QueryFile = require('pg-promise').QueryFile;
*
* // Helper for linking to external query files:
* function sql(file) {
* var relativePath = './db/sql/';
* return new QueryFile(relativePath + file, {minify: true});
* }
*
* var sqlProvider = {
* // external queries for Users:
* users: {
* add: sql('users/create.sql'),
* search: sql('users/search.sql'),
* report: sql('users/report.sql'),
* },
* // external queries for Products:
* products: {
* add: sql('products/add.sql'),
* quote: sql('products/quote.sql'),
* search: sql('products/search.sql'),
* }
* };
*
* module.exports = sqlProvider;
*
* @example
* // Testing our SQL provider
*
* var db = require('./db'); // our database module;
* var sql = require('./sql').users; // our sql for users;
*
* module.exports = {
* addUser: function (name, age) {
* return db.none(sql.add, [name, age]);
* },
* findUser: function (name) {
* return db.any(sql.search, name);
* }
* };
*
*/
function QueryFile(file, options) {
if (!(this instanceof QueryFile)) {
return new QueryFile(file, options);
}
var sql, error, ready, modTime, after, opt = {
debug: process.env.NODE_ENV === 'development',
minify: false,
compress: false
};
if (options && typeof options === 'object') {
if (options.debug !== undefined) {
opt.debug = !!options.debug;
}
if (options.minify !== undefined) {
after = options.minify === 'after';
opt.minify = after ? 'after' : !!options.minify;
}
if (options.compress !== undefined) {
opt.compress = !!options.compress;
}
if (opt.compress && options.minify === undefined) {
opt.minify = true;
}
if (options.params !== undefined) {
opt.params = options.params;
}
}
Object.freeze(opt);
/**
* @method QueryFile.prepare
* @summary Prepares the query for execution.
* @description
* If the the query hasn't been prepared yet, it will read the file
* and process the contents according to the parameters passed into
* the constructor.
*
* This method is meant primarily for internal use by the library.
*/
this.prepare = function () {
var lastMod;
if (opt.debug && ready) {
try {
lastMod = $npm.fs.statSync(file).mtime.getTime();
if (lastMod !== modTime) {
ready = false;
}
} catch (e) {
sql = undefined;
ready = false;
error = e;
return;
}
}
if (!ready) {
try {
sql = $npm.fs.readFileSync(file, 'utf8');
modTime = lastMod || $npm.fs.statSync(file).mtime.getTime();
if (opt.minify && !after) {
sql = $npm.minify(sql, {compress: opt.compress});
}
if (opt.params !== undefined) {
sql = $npm.format(sql, opt.params, {partial: true});
}
if (opt.minify && after) {
sql = $npm.minify(sql, {compress: opt.compress});
}
ready = true;
error = undefined;
} catch (e) {
sql = undefined;
error = new $npm.QueryFileError(e, this);
}
}
};
/**
* @name QueryFile#query
* @type String
* @default undefined
* @readonly
* @summary Prepared query string.
* @description
* When property {@link QueryFile#error error} is set, the query is `undefined`.
*
* This property is meant primarily for internal use by the library.
*/
Object.defineProperty(this, 'query', {
get: function () {
return sql;
}
});
/**
* @name QueryFile#error
* @type {QueryFileError}
* @default undefined
* @readonly
* @description
* When in an error state, it is set to a {@link QueryFileError} object. Otherwise, it is `undefined`.
*
* This property is meant primarily for internal use by the library.
*/
Object.defineProperty(this, 'error', {
get: function () {
return error;
}
});
/**
* @name QueryFile#file
* @type String
* @readonly
* @description
* File name that was passed into the constructor.
*
* This property is meant primarily for internal use by the library.
*/
Object.defineProperty(this, 'file', {
get: function () {
return file;
}
});
/**
* @name QueryFile#options
* @type Object
* @readonly
* @description
* Set of options, as configured during the object's construction.
*
* This property is meant primarily for internal use by the library.
*/
Object.defineProperty(this, 'options', {
get: function () {
return opt;
}
});
this.prepare();
}
/**
* @method QueryFile.toString
* @description
* Creates a well-formatted multi-line string that represents the object's current state.
*
* It is called automatically when writing the object into the console.
*
* @param {Number} [level=0]
* Nested output level, to provide visual offset.
*
* @returns {string}
*/
QueryFile.prototype.toString = function (level) {
level = level > 0 ? parseInt(level) : 0;
var gap = $npm.utils.messageGap(level + 1);
var lines = [
'QueryFile {'
];
this.prepare();
lines.push(gap + 'file: "' + this.file + '"');
lines.push(gap + 'options: ' + JSON.stringify(this.options));
if (this.error) {
lines.push(gap + 'error: ' + this.error.toString(level + 1));
} else {
lines.push(gap + 'query: "' + this.query + '"');
}
lines.push($npm.utils.messageGap(level) + '}');
return lines.join($npm.os.EOL);
};
QueryFile.prototype.inspect = function () {
return this.toString();
};
module.exports = QueryFile;