You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
13 KiB
398 lines
13 KiB
/*
|
|
* Copyright (c) 2015-present, Vitaly Tomilov
|
|
*
|
|
* See the LICENSE file at the top-level directory of this distribution
|
|
* for licensing information.
|
|
*
|
|
* Removal or modification of this copyright notice is prohibited.
|
|
*/
|
|
|
|
const {InnerState} = require(`./inner-state`);
|
|
const {QueryFileError} = require(`./errors`);
|
|
const {assertOptions} = require(`assert-options`);
|
|
const {ColorConsole} = require(`./utils/color`);
|
|
|
|
const npm = {
|
|
fs: require(`fs`),
|
|
os: require(`os`),
|
|
path: require(`path`),
|
|
minify: require(`pg-minify`),
|
|
utils: require(`./utils`),
|
|
formatting: require(`./formatting`)
|
|
};
|
|
|
|
const file$query = Symbol(`QueryFile.query`);
|
|
|
|
/**
|
|
* @class 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 {@link PreparedStatement}
|
|
* and {@link ParameterizedQuery}.
|
|
*
|
|
* It never throws any error, leaving it for query methods to reject with {@link errors.QueryFileError QueryFileError}.
|
|
*
|
|
* **IMPORTANT:** You should only create a single reusable object per file, in order to avoid repeated file reads,
|
|
* as the IO is a very expensive resource. If you do not follow it, you will be seeing the following warning:
|
|
* `Creating a duplicate QueryFile object for the same file`, which signals a bad-use pattern.
|
|
*
|
|
* @param {string} file
|
|
* Path to the SQL file with the query, either absolute or relative to the application's entry point file.
|
|
*
|
|
* If there is any problem reading the file, it will be reported when executing the query.
|
|
*
|
|
* @param {QueryFile.Options} [options]
|
|
* Set of configuration options, as documented by {@link QueryFile.Options}.
|
|
*
|
|
* @returns {QueryFile}
|
|
*
|
|
* @see
|
|
* {@link errors.QueryFileError QueryFileError},
|
|
* {@link QueryFile#toPostgres toPostgres}
|
|
*
|
|
* @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
|
|
*
|
|
* const {QueryFile} = require('pg-promise');
|
|
* const {join: joinPath} = require('path');
|
|
*
|
|
* // Helper for linking to external query files:
|
|
* function sql(file) {
|
|
* const fullPath = joinPath(__dirname, file); // generating full path;
|
|
* return new QueryFile(fullPath, {minify: true});
|
|
* }
|
|
*
|
|
* module.exports = {
|
|
* // 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'),
|
|
* }
|
|
* };
|
|
*
|
|
* @example
|
|
* // Testing our SQL provider
|
|
*
|
|
* const db = require('./db'); // our database module;
|
|
* const {users: sql} = require('./sql'); // sql for users;
|
|
*
|
|
* module.exports = {
|
|
* addUser: (name, age) => db.none(sql.add, [name, age]),
|
|
* findUser: name => db.any(sql.search, name)
|
|
* };
|
|
*
|
|
*/
|
|
class QueryFile extends InnerState {
|
|
|
|
constructor(file, options) {
|
|
|
|
let filePath = file;
|
|
|
|
options = assertOptions(options, {
|
|
debug: npm.utils.isDev(),
|
|
minify: (options && options.compress && options.minify === undefined) ? true : undefined,
|
|
compress: undefined,
|
|
params: undefined,
|
|
noWarnings: undefined
|
|
});
|
|
|
|
if (npm.utils.isText(filePath) && !npm.path.isAbsolute(filePath)) {
|
|
filePath = npm.path.join(npm.utils.startDir, filePath);
|
|
}
|
|
|
|
const {usedPath} = QueryFile.instance;
|
|
|
|
// istanbul ignore next:
|
|
if (!options.noWarnings) {
|
|
if (filePath in usedPath) {
|
|
usedPath[filePath]++;
|
|
ColorConsole.warn(`WARNING: Creating a duplicate QueryFile object for the same file - \n ${filePath}\n${npm.utils.getLocalStack(2, 3)}\n`);
|
|
} else {
|
|
usedPath[filePath] = 0;
|
|
}
|
|
}
|
|
|
|
const _inner = {
|
|
file,
|
|
filePath,
|
|
options,
|
|
sql: undefined,
|
|
error: undefined,
|
|
ready: undefined,
|
|
modTime: undefined
|
|
};
|
|
|
|
super(_inner);
|
|
|
|
this.prepare();
|
|
}
|
|
|
|
/**
|
|
* Global instance of the file-path repository.
|
|
*
|
|
* @return {{usedPath: {}}}
|
|
*/
|
|
static get instance() {
|
|
const s = Symbol.for(`pgPromiseQueryFile`);
|
|
let scope = global[s];
|
|
if (!scope) {
|
|
scope = {
|
|
usedPath: {} // used-path look-up dictionary
|
|
};
|
|
global[s] = scope;
|
|
}
|
|
return scope;
|
|
}
|
|
|
|
/**
|
|
* @name QueryFile#Symbol(QueryFile.$query)
|
|
* @type {string}
|
|
* @default undefined
|
|
* @readonly
|
|
* @private
|
|
* @summary Prepared query string.
|
|
* @description
|
|
* When property {@link QueryFile#error error} is set, the query is `undefined`.
|
|
*
|
|
* **IMPORTANT:** This property is for internal use by the library only, never use this
|
|
* property directly from your code.
|
|
*/
|
|
get [file$query]() {
|
|
return this._inner.sql;
|
|
}
|
|
|
|
/**
|
|
* @name QueryFile#error
|
|
* @type {errors.QueryFileError}
|
|
* @default undefined
|
|
* @readonly
|
|
* @description
|
|
* When in an error state, it is set to a {@link errors.QueryFileError QueryFileError} object. Otherwise, it is `undefined`.
|
|
*/
|
|
get error() {
|
|
return this._inner.error;
|
|
}
|
|
|
|
/**
|
|
* @name QueryFile#file
|
|
* @type {string}
|
|
* @readonly
|
|
* @description
|
|
* File name that was passed into the constructor.
|
|
*
|
|
* This property is primarily for internal use by the library.
|
|
*/
|
|
get file() {
|
|
return this._inner.file;
|
|
}
|
|
|
|
/**
|
|
* @name QueryFile#options
|
|
* @type {QueryFile.Options}
|
|
* @readonly
|
|
* @description
|
|
* Set of options, as configured during the object's construction.
|
|
*
|
|
* This property is primarily for internal use by the library.
|
|
*/
|
|
get options() {
|
|
return this._inner.options;
|
|
}
|
|
|
|
/**
|
|
* @summary Prepares the query for execution.
|
|
* @description
|
|
* If the query hasn't been prepared yet, it will read the file and process the content according
|
|
* to the parameters passed into the constructor.
|
|
*
|
|
* This method is primarily for internal use by the library.
|
|
*
|
|
* @param {boolean} [throwErrors=false]
|
|
* Throw any error encountered.
|
|
*/
|
|
prepare(throwErrors) {
|
|
const i = this._inner, options = i.options;
|
|
let lastMod;
|
|
if (options.debug && i.ready) {
|
|
try {
|
|
lastMod = npm.fs.statSync(i.filePath).mtime.getTime();
|
|
// istanbul ignore if;
|
|
if (lastMod === i.modTime) {
|
|
return;
|
|
}
|
|
i.ready = false;
|
|
} catch (e) {
|
|
i.sql = undefined;
|
|
i.ready = false;
|
|
i.error = e;
|
|
if (throwErrors) {
|
|
throw i.error;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (i.ready) {
|
|
return;
|
|
}
|
|
try {
|
|
i.sql = npm.fs.readFileSync(i.filePath, `utf8`);
|
|
i.modTime = lastMod || npm.fs.statSync(i.filePath).mtime.getTime();
|
|
if (options.minify && options.minify !== `after`) {
|
|
i.sql = npm.minify(i.sql, {compress: options.compress});
|
|
}
|
|
if (options.params !== undefined) {
|
|
i.sql = npm.formatting.as.format(i.sql, options.params, {partial: true});
|
|
}
|
|
if (options.minify && options.minify === `after`) {
|
|
i.sql = npm.minify(i.sql, {compress: options.compress});
|
|
}
|
|
i.ready = true;
|
|
i.error = undefined;
|
|
} catch (e) {
|
|
i.sql = undefined;
|
|
i.error = new QueryFileError(e, this);
|
|
if (throwErrors) {
|
|
throw i.error;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Hiding the query as a symbol within the type,
|
|
// to make it even more difficult to misuse it:
|
|
QueryFile.$query = file$query;
|
|
|
|
/**
|
|
* @method QueryFile#toPostgres
|
|
* @description
|
|
* $[Custom Type Formatting], based on $[Symbolic CTF], i.e. the actual method is available only via {@link external:Symbol Symbol}:
|
|
*
|
|
* ```js
|
|
* const ctf = pgp.as.ctf; // Custom Type Formatting symbols namespace
|
|
* const query = qf[ctf.toPostgres](); // qf = an object of type QueryFile
|
|
* ```
|
|
*
|
|
* This is a raw formatting type (`rawType = true`), i.e. when used as a query-formatting parameter, type `QueryFile` injects SQL as raw text.
|
|
*
|
|
* If you need to support type `QueryFile` outside of query methods, this is the only safe way to get the most current SQL.
|
|
* And you would want to use this method dynamically, as it reloads the SQL automatically, if option `debug` is set.
|
|
* See {@link QueryFile.Options Options}.
|
|
*
|
|
* @param {QueryFile} [self]
|
|
* Optional self-reference, for ES6 arrow functions.
|
|
*
|
|
* @returns {string}
|
|
* SQL string from the file, according to the {@link QueryFile.Options options} specified.
|
|
*
|
|
*/
|
|
QueryFile.prototype[npm.formatting.as.ctf.toPostgres] = function (self) {
|
|
self = this instanceof QueryFile && this || self;
|
|
self.prepare(true);
|
|
return self[QueryFile.$query];
|
|
};
|
|
|
|
QueryFile.prototype[npm.formatting.as.ctf.rawType] = true; // use as pre-formatted
|
|
|
|
/**
|
|
* @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;
|
|
const gap = npm.utils.messageGap(level + 1);
|
|
const lines = [
|
|
`QueryFile {`
|
|
];
|
|
this.prepare();
|
|
lines.push(gap + `file: "` + this.file + `"`);
|
|
lines.push(gap + `options: ` + npm.utils.toJson(this.options));
|
|
if (this.error) {
|
|
lines.push(gap + `error: ` + this.error.toString(level + 1));
|
|
} else {
|
|
lines.push(gap + `query: "` + this[QueryFile.$query] + `"`);
|
|
}
|
|
lines.push(npm.utils.messageGap(level) + `}`);
|
|
return lines.join(npm.os.EOL);
|
|
};
|
|
|
|
npm.utils.addInspection(QueryFile, function () {
|
|
return this.toString();
|
|
});
|
|
|
|
module.exports = {QueryFile};
|
|
|
|
/**
|
|
* @typedef QueryFile.Options
|
|
* @description
|
|
* A set of configuration options as passed into the {@link QueryFile} constructor.
|
|
*
|
|
* @property {boolean} 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.
|
|
*
|
|
* @property {boolean|string} 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].
|
|
*
|
|
* @property {boolean} 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`.
|
|
*
|
|
* @property {array|object|value} 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.
|
|
*
|
|
* @property {boolean} noWarnings=false
|
|
* Suppresses all warnings produced by the class. It is not recommended for general use, only in specific tests
|
|
* that may require it.
|
|
*
|
|
*/
|
|
|