query-file.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. /*
  2. * Copyright (c) 2015-present, Vitaly Tomilov
  3. *
  4. * See the LICENSE file at the top-level directory of this distribution
  5. * for licensing information.
  6. *
  7. * Removal or modification of this copyright notice is prohibited.
  8. */
  9. const {InnerState} = require('./inner-state');
  10. const {QueryFileError} = require('./errors');
  11. const {assert} = require('./assert');
  12. const {ColorConsole} = require('./utils/color');
  13. const npm = {
  14. fs: require('fs'),
  15. os: require('os'),
  16. path: require('path'),
  17. minify: require('pg-minify'),
  18. utils: require('./utils'),
  19. formatting: require('./formatting')
  20. };
  21. const file$query = Symbol('QueryFile.query');
  22. /**
  23. * @class QueryFile
  24. * @description
  25. *
  26. * Represents an external SQL file. The type is available from the library's root: `pgp.QueryFile`.
  27. *
  28. * Reads a file with SQL and prepares it for execution, also parses and minifies it, if required.
  29. * The SQL can be of any complexity, with both single and multi-line comments.
  30. *
  31. * The type can be used in place of the `query` parameter, with any query method directly, plus as `text` in {@link PreparedStatement}
  32. * and {@link ParameterizedQuery}.
  33. *
  34. * It never throws any error, leaving it for query methods to reject with {@link errors.QueryFileError QueryFileError}.
  35. *
  36. * **IMPORTANT:** You should only create a single reusable object per file, in order to avoid repeated file reads,
  37. * as the IO is a very expensive resource. If you do not follow it, you will be seeing the following warning:
  38. * `Creating a duplicate QueryFile object for the same file`, which signals a bad-use pattern.
  39. *
  40. * @param {string} file
  41. * Path to the SQL file with the query, either absolute or relative to the application's entry point file.
  42. *
  43. * If there is any problem reading the file, it will be reported when executing the query.
  44. *
  45. * @param {QueryFile.Options} [options]
  46. * Set of configuration options, as documented by {@link QueryFile.Options}.
  47. *
  48. * @returns {QueryFile}
  49. *
  50. * @see
  51. * {@link errors.QueryFileError QueryFileError},
  52. * {@link QueryFile#toPostgres toPostgres}
  53. *
  54. * @example
  55. * // File sql.js
  56. *
  57. * // Proper way to organize an sql provider:
  58. * //
  59. * // - have all sql files for Users in ./sql/users
  60. * // - have all sql files for Products in ./sql/products
  61. * // - have your sql provider module as ./sql/index.js
  62. *
  63. * const {QueryFile} = require('pg-promise');
  64. * const {join: joinPath} = require('path');
  65. *
  66. * // Helper for linking to external query files:
  67. * function sql(file) {
  68. * const fullPath = joinPath(__dirname, file); // generating full path;
  69. * return new QueryFile(fullPath, {minify: true});
  70. * }
  71. *
  72. * module.exports = {
  73. * // external queries for Users:
  74. * users: {
  75. * add: sql('users/create.sql'),
  76. * search: sql('users/search.sql'),
  77. * report: sql('users/report.sql'),
  78. * },
  79. * // external queries for Products:
  80. * products: {
  81. * add: sql('products/add.sql'),
  82. * quote: sql('products/quote.sql'),
  83. * search: sql('products/search.sql'),
  84. * }
  85. * };
  86. *
  87. * @example
  88. * // Testing our SQL provider
  89. *
  90. * const db = require('./db'); // our database module;
  91. * const {users: sql} = require('./sql'); // sql for users;
  92. *
  93. * module.exports = {
  94. * addUser: (name, age) => db.none(sql.add, [name, age]),
  95. * findUser: name => db.any(sql.search, name)
  96. * };
  97. *
  98. */
  99. class QueryFile extends InnerState {
  100. constructor(file, options) {
  101. let filePath = file;
  102. options = assert(options, {
  103. debug: npm.utils.isDev(),
  104. minify: (options && options.compress && options.minify === undefined) ? true : undefined,
  105. compress: undefined,
  106. params: undefined,
  107. noWarnings: undefined
  108. });
  109. if (npm.utils.isText(filePath) && !npm.path.isAbsolute(filePath)) {
  110. filePath = npm.path.join(npm.utils.startDir, filePath);
  111. }
  112. const {usedPath} = QueryFile.instance;
  113. // istanbul ignore next:
  114. if (!options.noWarnings) {
  115. if (filePath in usedPath) {
  116. usedPath[filePath]++;
  117. ColorConsole.warn(`WARNING: Creating a duplicate QueryFile object for the same file - \n ${filePath}\n${npm.utils.getLocalStack(2, 3)}\n`);
  118. } else {
  119. usedPath[filePath] = 0;
  120. }
  121. }
  122. const _inner = {
  123. file,
  124. filePath,
  125. options,
  126. sql: undefined,
  127. error: undefined,
  128. ready: undefined,
  129. modTime: undefined
  130. };
  131. super(_inner);
  132. this.prepare();
  133. }
  134. /**
  135. * Global instance of the file-path repository.
  136. *
  137. * @return {{usedPath: {}}}
  138. */
  139. static get instance() {
  140. const s = Symbol.for('pgPromiseQueryFile');
  141. let scope = global[s];
  142. if (!scope) {
  143. scope = {
  144. usedPath: {} // used-path look-up dictionary
  145. };
  146. global[s] = scope;
  147. }
  148. return scope;
  149. }
  150. /**
  151. * @name QueryFile#Symbol(QueryFile.$query)
  152. * @type {string}
  153. * @default undefined
  154. * @readonly
  155. * @private
  156. * @summary Prepared query string.
  157. * @description
  158. * When property {@link QueryFile#error error} is set, the query is `undefined`.
  159. *
  160. * **IMPORTANT:** This property is for internal use by the library only, never use this
  161. * property directly from your code.
  162. */
  163. get [file$query]() {
  164. return this._inner.sql;
  165. }
  166. /**
  167. * @name QueryFile#error
  168. * @type {errors.QueryFileError}
  169. * @default undefined
  170. * @readonly
  171. * @description
  172. * When in an error state, it is set to a {@link errors.QueryFileError QueryFileError} object. Otherwise, it is `undefined`.
  173. */
  174. get error() {
  175. return this._inner.error;
  176. }
  177. /**
  178. * @name QueryFile#file
  179. * @type {string}
  180. * @readonly
  181. * @description
  182. * File name that was passed into the constructor.
  183. *
  184. * This property is primarily for internal use by the library.
  185. */
  186. get file() {
  187. return this._inner.file;
  188. }
  189. /**
  190. * @name QueryFile#options
  191. * @type {QueryFile.Options}
  192. * @readonly
  193. * @description
  194. * Set of options, as configured during the object's construction.
  195. *
  196. * This property is primarily for internal use by the library.
  197. */
  198. get options() {
  199. return this._inner.options;
  200. }
  201. /**
  202. * @summary Prepares the query for execution.
  203. * @description
  204. * If the query hasn't been prepared yet, it will read the file and process the content according
  205. * to the parameters passed into the constructor.
  206. *
  207. * This method is primarily for internal use by the library.
  208. *
  209. * @param {boolean} [throwErrors=false]
  210. * Throw any error encountered.
  211. */
  212. prepare(throwErrors) {
  213. const i = this._inner, options = i.options;
  214. let lastMod;
  215. if (options.debug && i.ready) {
  216. try {
  217. lastMod = npm.fs.statSync(i.filePath).mtime.getTime();
  218. // istanbul ignore if;
  219. if (lastMod === i.modTime) {
  220. return;
  221. }
  222. i.ready = false;
  223. } catch (e) {
  224. i.sql = undefined;
  225. i.ready = false;
  226. i.error = e;
  227. if (throwErrors) {
  228. throw i.error;
  229. }
  230. return;
  231. }
  232. }
  233. if (i.ready) {
  234. return;
  235. }
  236. try {
  237. i.sql = npm.fs.readFileSync(i.filePath, 'utf8');
  238. i.modTime = lastMod || npm.fs.statSync(i.filePath).mtime.getTime();
  239. if (options.minify && options.minify !== 'after') {
  240. i.sql = npm.minify(i.sql, {compress: options.compress});
  241. }
  242. if (options.params !== undefined) {
  243. i.sql = npm.formatting.as.format(i.sql, options.params, {partial: true});
  244. }
  245. if (options.minify && options.minify === 'after') {
  246. i.sql = npm.minify(i.sql, {compress: options.compress});
  247. }
  248. i.ready = true;
  249. i.error = undefined;
  250. } catch (e) {
  251. i.sql = undefined;
  252. i.error = new QueryFileError(e, this);
  253. if (throwErrors) {
  254. throw i.error;
  255. }
  256. }
  257. }
  258. }
  259. // Hiding the query as a symbol within the type,
  260. // to make it even more difficult to misuse it:
  261. QueryFile.$query = file$query;
  262. /**
  263. * @method QueryFile#toPostgres
  264. * @description
  265. * $[Custom Type Formatting], based on $[Symbolic CTF], i.e. the actual method is available only via {@link external:Symbol Symbol}:
  266. *
  267. * ```js
  268. * const ctf = pgp.as.ctf; // Custom Type Formatting symbols namespace
  269. * const query = qf[ctf.toPostgres](); // qf = an object of type QueryFile
  270. * ```
  271. *
  272. * This is a raw formatting type (`rawType = true`), i.e. when used as a query-formatting parameter, type `QueryFile` injects SQL as raw text.
  273. *
  274. * If you need to support type `QueryFile` outside of query methods, this is the only safe way to get the most current SQL.
  275. * And you would want to use this method dynamically, as it reloads the SQL automatically, if option `debug` is set.
  276. * See {@link QueryFile.Options Options}.
  277. *
  278. * @param {QueryFile} [self]
  279. * Optional self-reference, for ES6 arrow functions.
  280. *
  281. * @returns {string}
  282. * SQL string from the file, according to the {@link QueryFile.Options options} specified.
  283. *
  284. */
  285. QueryFile.prototype[npm.formatting.as.ctf.toPostgres] = function (self) {
  286. self = this instanceof QueryFile && this || self;
  287. self.prepare(true);
  288. return self[QueryFile.$query];
  289. };
  290. QueryFile.prototype[npm.formatting.as.ctf.rawType] = true; // use as pre-formatted
  291. /**
  292. * @method QueryFile#toString
  293. * @description
  294. * Creates a well-formatted multi-line string that represents the object's current state.
  295. *
  296. * It is called automatically when writing the object into the console.
  297. *
  298. * @param {number} [level=0]
  299. * Nested output level, to provide visual offset.
  300. *
  301. * @returns {string}
  302. */
  303. QueryFile.prototype.toString = function (level) {
  304. level = level > 0 ? parseInt(level) : 0;
  305. const gap = npm.utils.messageGap(level + 1);
  306. const lines = [
  307. 'QueryFile {'
  308. ];
  309. this.prepare();
  310. lines.push(gap + 'file: "' + this.file + '"');
  311. lines.push(gap + 'options: ' + npm.utils.toJson(this.options));
  312. if (this.error) {
  313. lines.push(gap + 'error: ' + this.error.toString(level + 1));
  314. } else {
  315. lines.push(gap + 'query: "' + this[QueryFile.$query] + '"');
  316. }
  317. lines.push(npm.utils.messageGap(level) + '}');
  318. return lines.join(npm.os.EOL);
  319. };
  320. npm.utils.addInspection(QueryFile, function () {
  321. return this.toString();
  322. });
  323. module.exports = {QueryFile};
  324. /**
  325. * @typedef QueryFile.Options
  326. * @description
  327. * A set of configuration options as passed into the {@link QueryFile} constructor.
  328. *
  329. * @property {boolean} debug
  330. * When in debug mode, the query file is checked for its last modification time on every query request,
  331. * so if it changes, the file is read afresh.
  332. *
  333. * The default for this property is `true` when `NODE_ENV` = `development`,
  334. * or `false` otherwise.
  335. *
  336. * @property {boolean|string} minify=false
  337. * Parses and minifies the SQL using $[pg-minify]:
  338. * - `false` - do not use $[pg-minify]
  339. * - `true` - use $[pg-minify] to parse and minify SQL
  340. * - `'after'` - use $[pg-minify] after applying static formatting parameters
  341. * (option `params`), as opposed to before it (default)
  342. *
  343. * If option `compress` is set, then the default for `minify` is `true`.
  344. *
  345. * Failure to parse SQL will result in $[SQLParsingError].
  346. *
  347. * @property {boolean} compress=false
  348. * Sets option `compress` as supported by $[pg-minify], to uglify the SQL:
  349. * - `false` - no compression to be applied, keep minimum spaces for easier read
  350. * - `true` - remove all unnecessary spaces from SQL
  351. *
  352. * This option has no meaning, if `minify` is explicitly set to `false`. However, if `minify` is not
  353. * specified and `compress` is specified as `true`, then `minify` defaults to `true`.
  354. *
  355. * @property {array|object|value} params
  356. *
  357. * Static formatting parameters to be applied to the SQL, using the same method {@link formatting.format as.format},
  358. * but with option `partial` = `true`.
  359. *
  360. * Most of the time query formatting is fully dynamic, and applied just before executing the query.
  361. * In some cases though you may need to pre-format SQL with static values. Examples of it can be a
  362. * schema name, or a configurable table name.
  363. *
  364. * This option makes two-step SQL formatting easy: you can pre-format the SQL initially, and then
  365. * apply the second-step dynamic formatting when executing the query.
  366. *
  367. * @property {boolean} noWarnings=false
  368. * Suppresses all warnings produced by the class. It is not recommended for general use, only in specific tests
  369. * that may require it.
  370. *
  371. */