123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- /*
- * 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 {assert} = require('./assert');
- 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 = assert(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.
- *
- */
|