Merge branch '4_7_x' of https://github.com/xtuple/xtuple into i24559_xtlocks
[xtuple] / scripts / lib / build_database.js
1 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, strict:true, trailing:true, white:true */
3 /*global X:true, Backbone:true, _:true, XM:true, XT:true*/
4
5 _ = require('underscore');
6
7 var  async = require('async'),
8   dataSource = require('../../node-datasource/lib/ext/datasource').dataSource,
9   exec = require('child_process').exec,
10   explodeManifest = require("./util/process_manifest").explodeManifest,
11   fs = require('fs'),
12   ormInstaller = require('./orm'),
13   dictionaryBuilder = require('./build_dictionary'),
14   clientBuilder = require('./build_client'),
15   path = require('path'),
16   pg = require('pg'),
17   os = require('os'),
18   sendToDatabase = require("./util/send_to_database").sendToDatabase,
19   winston = require('winston');
20
21 (function () {
22   "use strict";
23
24   /**
25     @param {Object} specs Specification for the build process, in the form:
26       [ { extensions:
27            [ '/home/user/git/xtuple/enyo-client',
28              '/home/user/git/xtuple/enyo-client/extensions/source/crm',
29              '/home/user/git/xtuple/enyo-client/extensions/source/sales',
30              '/home/user/git/private-extensions/source/incident_plus' ],
31           database: 'dev',
32           orms: [] },
33         { extensions:
34            [ '/home/user/git/xtuple/enyo-client',
35              '/home/user/git/xtuple/enyo-client/extensions/source/sales',
36              '/home/user/git/xtuple/enyo-client/extensions/source/project' ],
37           database: 'dev2',
38           orms: [] }]
39
40     @param {Object} creds Database credentials, in the form:
41       { hostname: 'localhost',
42         port: 5432,
43         user: 'admin',
44         password: 'admin',
45         host: 'localhost' }
46   */
47   var buildDatabase = exports.buildDatabase = function (specs, creds, masterCallback) {
48     //
49     // The function to generate all the scripts for a database
50     //
51     var installDatabase = function (spec, databaseCallback) {
52       var extensions = spec.extensions,
53         databaseName = spec.database;
54
55       //
56       // The function to install all the scripts for an extension
57       //
58       var getExtensionSql = function (extension, extensionCallback) {
59         if (spec.clientOnly) {
60           extensionCallback(null, "");
61           return;
62         }
63         // deal with directory structure quirks
64         var baseName = path.basename(extension),
65           isFoundation = extension.indexOf("foundation-database") >= 0,
66           isFoundationExtension = extension.indexOf("inventory/foundation-database") >= 0 ||
67             extension.indexOf("manufacturing/foundation-database") >= 0 ||
68             extension.indexOf("distribution/foundation-database") >= 0,
69           isLibOrm = extension.indexOf("lib/orm") >= 0,
70           isApplicationCore = extension.indexOf("enyo-client") >= 0 &&
71             extension.indexOf("extension") < 0,
72           isCoreExtension = extension.indexOf("enyo-client") >= 0 &&
73             extension.indexOf("extension") >= 0,
74           isPublicExtension = extension.indexOf("xtuple-extensions") >= 0,
75           isPrivateExtension = extension.indexOf("private-extensions") >= 0,
76           isNpmExtension = baseName.indexOf("xtuple-") >= 0,
77           isExtension = !isFoundation && !isLibOrm && !isApplicationCore,
78           dbSourceRoot = (isFoundation || isFoundationExtension) ? extension :
79             isLibOrm ? path.join(extension, "source") :
80             path.join(extension, "database/source"),
81           manifestOptions = {
82             manifestFilename: path.resolve(dbSourceRoot, "manifest.js"),
83             extensionPath: isExtension ?
84               path.resolve(dbSourceRoot, "../../") :
85               undefined,
86             useFrozenScripts: spec.frozen,
87             useFoundationScripts: baseName.indexOf('inventory') >= 0 ||
88               baseName.indexOf('manufacturing') >= 0 ||
89               baseName.indexOf('distribution') >= 0,
90             registerExtension: isExtension,
91             runJsInit: !isFoundation && !isLibOrm,
92             wipeViews: isFoundation && spec.wipeViews,
93             wipeOrms: isApplicationCore && spec.wipeViews,
94             extensionLocation: isCoreExtension ? "/core-extensions" :
95               isPublicExtension ? "/xtuple-extensions" :
96               isPrivateExtension ? "/private-extensions" :
97               isNpmExtension ? "npm" : "not-applicable"
98           };
99
100         explodeManifest(manifestOptions, extensionCallback);
101       };
102
103       // We also need to get the sql that represents the queries to generate
104       // the XM views from the ORMs. We use the old ORM installer for this,
105       // which has been retooled to return the queryString instead of running
106       // it itself.
107       var getOrmSql = function (extension, callback) {
108         if (spec.clientOnly) {
109           callback(null, "");
110           return;
111         }
112         var ormDir = path.join(extension, "database/orm");
113
114         if (fs.existsSync(ormDir)) {
115           var updateSpecs = function (err, res) {
116             if (err) {
117               callback(err);
118             }
119             // if the orm installer has added any new orms we want to know about them
120             // so we can inform the next call to the installer.
121             spec.orms = _.unique(_.union(spec.orms, res.orms), function (orm) {
122               return orm.namespace + orm.type;
123             });
124             callback(err, res.query);
125           };
126           ormInstaller.run(ormDir, spec, updateSpecs);
127         } else {
128           // No ORM dir? No problem! Nothing to install.
129           callback(null, "");
130         }
131       };
132
133       // We also need to get the sql that represents the queries to put the
134       // client source in the database.
135       var getClientSql = function (extension, callback) {
136         if (spec.databaseOnly) {
137           callback(null, "");
138           return;
139         }
140         clientBuilder.getClientSql(extension, callback);
141       };
142
143       /**
144         The sql for each extension comprises the sql in the the source directory
145         with the orm sql tacked on to the end. Note that an alternate methodology
146         dictates that *all* source for all extensions should be run before *any*
147         orm queries for any extensions, but that is not the way it works here.
148        */
149       var getAllSql = function (extension, masterCallback) {
150
151         async.series([
152           function (callback) {
153             getExtensionSql(extension, callback);
154           },
155           function (callback) {
156             if (spec.clientOnly) {
157               callback(null, "");
158               return;
159             }
160             dictionaryBuilder.getDictionarySql(extension, callback);
161           },
162           function (callback) {
163             getOrmSql(extension, callback);
164           },
165           function (callback) {
166             getClientSql(extension, callback);
167           }
168         ], function (err, results) {
169           masterCallback(err, _.reduce(results, function (memo, sql) {
170             return memo + sql;
171           }, ""));
172         });
173       };
174
175
176       //
177       // Asyncronously run all the functions to all the extension sql for the database,
178       // in series, and execute the query when they all have come back.
179       //
180       async.mapSeries(extensions, getAllSql, function (err, extensionSql) {
181         var allSql,
182           credsClone = JSON.parse(JSON.stringify(creds));
183
184         if (err) {
185           databaseCallback(err);
186           return;
187         }
188         // each String of the scriptContents is the concatenated SQL for the extension.
189         // join these all together into a single string for the whole database.
190         allSql = _.reduce(extensionSql, function (memo, script) {
191           return memo + script;
192         }, "");
193
194         // Without this, psql runs all input and returns success even if errors occurred
195         allSql = "\\set ON_ERROR_STOP TRUE\n" + allSql;
196
197         winston.info("Applying build to database " + spec.database);
198         credsClone.database = spec.database;
199         sendToDatabase(allSql, credsClone, spec, function (err, res) {
200           if (spec.populateData && creds.encryptionKeyFile) {
201             var populateSql = "DO $$ XT.disableLocks = true; $$ language plv8;";
202             var encryptionKey = fs.readFileSync(path.resolve(__dirname, "../../node-datasource", creds.encryptionKeyFile), "utf8");
203             var patches = require(path.join(__dirname, "../../enyo-client/database/source/populate_data")).patches;
204             _.each(patches, function (patch) {
205               patch.encryptionKey = encryptionKey;
206               patch.username = creds.username;
207               populateSql += "select xt.patch(\'" + JSON.stringify(patch) + "\');";
208             });
209             populateSql += "DO $$ XT.disableLocks = undefined; $$ language plv8;";
210             dataSource.query(populateSql, credsClone, databaseCallback);
211           } else {
212             databaseCallback(err, res);
213           }
214         });
215       });
216     };
217
218     //
219     // Step 1:
220     // Okay, before we install the database there is ONE thing we need to check,
221     // which is the pre-installed ORMs. Check that now.
222     //
223     var preInstallDatabase = function (spec, callback) {
224       var existsSql = "select relname from pg_class where relname = 'orm'",
225         credsClone = JSON.parse(JSON.stringify(creds)),
226         ormTestSql = "select orm_namespace as namespace, " +
227           " orm_type as type " +
228           "from xt.orm " +
229           "where not orm_ext;";
230
231       credsClone.database = spec.database;
232
233       dataSource.query(existsSql, credsClone, function (err, res) {
234         if (err) {
235           callback(err);
236         }
237         if (spec.wipeViews || res.rowCount === 0) {
238           // xt.orm doesn't exist, because this is probably a brand-new DB.
239           // No problem! That just means that there are no pre-existing ORMs.
240           spec.orms = [];
241           installDatabase(spec, callback);
242         } else {
243           dataSource.query(ormTestSql, credsClone, function (err, res) {
244             if (err) {
245               callback(err);
246             }
247             spec.orms = res.rows;
248             installDatabase(spec, callback);
249           });
250         }
251       });
252     };
253
254     //
255     // Install all the databases
256     //
257     async.map(specs, preInstallDatabase, function (err, res) {
258       if (err) {
259         winston.error(err.message, err.stack, err);
260         if (masterCallback) {
261           masterCallback(err);
262         }
263         return;
264       }
265       winston.info("Success installing all scripts.");
266       winston.info("Cleaning up.");
267       clientBuilder.cleanup(specs, function (err) {
268         if (masterCallback) {
269           masterCallback(err, res);
270         }
271       });
272     });
273   };
274
275 }());