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