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