Merge pull request #1580 from gilmoskowitz/23796
[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         //winston.info("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           dbSourceRoot = (isFoundation || isFoundationExtension) ? extension :
100             isLibOrm ? path.join(extension, "source") :
101             path.join(extension, "database/source"),
102           manifestOptions = {
103             useFrozenScripts: spec.frozen,
104             useFoundationScripts: baseName.indexOf('inventory') >= 0 ||
105               baseName.indexOf('manufacturing') >= 0 ||
106               baseName.indexOf('distribution') >= 0,
107             registerExtension: !isFoundation && !isLibOrm && !isApplicationCore,
108             runJsInit: !isFoundation && !isLibOrm,
109             wipeViews: isApplicationCore && spec.wipeViews,
110             extensionLocation: isCoreExtension ? "/core-extensions" :
111               isPublicExtension ? "/xtuple-extensions" :
112               isPrivateExtension ? "/private-extensions" : "not-applicable"
113           };
114
115         buildDatabaseUtil.explodeManifest(path.join(dbSourceRoot, "manifest.js"),
116           manifestOptions, extensionCallback);
117       };
118
119       // We also need to get the sql that represents the queries to generate
120       // the XM views from the ORMs. We use the old ORM installer for this,
121       // which has been retooled to return the queryString instead of running
122       // it itself.
123       var getOrmSql = function (extension, callback) {
124         if (spec.clientOnly) {
125           callback(null, "");
126           return;
127         }
128         var ormDir = path.join(extension, "database/orm");
129
130         if (fs.existsSync(ormDir)) {
131           var updateSpecs = function (err, res) {
132             if (err) {
133               callback(err);
134             }
135             // if the orm installer has added any new orms we want to know about them
136             // so we can inform the next call to the installer.
137             spec.orms = _.unique(_.union(spec.orms, res.orms), function (orm) {
138               return orm.namespace + orm.type;
139             });
140             callback(err, res.query);
141           };
142           ormInstaller.run(ormDir, spec, updateSpecs);
143         } else {
144           // No ORM dir? No problem! Nothing to install.
145           callback(null, "");
146         }
147       };
148
149       // We also need to get the sql that represents the queries to put the
150       // client source in the database.
151       var getClientSql = function (extension, callback) {
152         if (spec.databaseOnly) {
153           callback(null, "");
154           return;
155         }
156         clientBuilder.getClientSql(extension, callback);
157       };
158
159       /**
160         The sql for each extension comprises the sql in the the source directory
161         with the orm sql tacked on to the end. Note that an alternate methodology
162         dictates that *all* source for all extensions should be run before *any*
163         orm queries for any extensions, but that is not the way it works here.
164        */
165       var getAllSql = function (extension, masterCallback) {
166
167         async.series([
168           function (callback) {
169             getExtensionSql(extension, callback);
170           },
171           function (callback) {
172             if (spec.clientOnly) {
173               callback(null, "");
174               return;
175             }
176             dictionaryBuilder.getDictionarySql(extension, callback);
177           },
178           function (callback) {
179             getOrmSql(extension, callback);
180           },
181           function (callback) {
182             getClientSql(extension, callback);
183           }
184         ], function (err, results) {
185           masterCallback(err, _.reduce(results, function (memo, sql) {
186             return memo + sql;
187           }, ""));
188         });
189       };
190
191
192       //
193       // Asyncronously run all the functions to all the extension sql for the database,
194       // in series, and execute the query when they all have come back.
195       //
196       async.mapSeries(extensions, getAllSql, function (err, extensionSql) {
197         var allSql,
198           credsClone = JSON.parse(JSON.stringify(creds));
199
200         if (err) {
201           databaseCallback(err);
202           return;
203         }
204         // each String of the scriptContents is the concatenated SQL for the extension.
205         // join these all together into a single string for the whole database.
206         allSql = _.reduce(extensionSql, function (memo, script) {
207           return memo + script;
208         }, "");
209
210         // Without this, when we delegate to exec psql the err var will not be set even
211         // on the case of error.
212         allSql = "\\set ON_ERROR_STOP TRUE;\n" + allSql;
213
214         if (spec.wasInitialized && !_.isEqual(extensions, ["foundation-database"])) {
215           // give the admin user every extension by default
216           allSql = allSql + "insert into xt.usrext (usrext_usr_username, usrext_ext_id) " +
217             "select '" + creds.username +
218             "', ext_id from xt.ext where ext_location = '/core-extensions' and ext_name NOT LIKE 'oauth2';";
219         }
220
221         winston.info("Applying build to database " + spec.database);
222         credsClone.database = spec.database;
223         buildDatabaseUtil.sendToDatabase(allSql, credsClone, spec, function (err, res) {
224           databaseCallback(err, res);
225         });
226       });
227     };
228
229     //
230     // Step 1:
231     // Okay, before we install the database there is ONE thing we need to check,
232     // which is the pre-installed ORMs. Check that now.
233     //
234     var preInstallDatabase = function (spec, callback) {
235       var existsSql = "select relname from pg_class where relname = 'orm'",
236         credsClone = JSON.parse(JSON.stringify(creds)),
237         ormTestSql = "select orm_namespace as namespace, " +
238           " orm_type as type " +
239           "from xt.orm " +
240           "where not orm_ext;";
241
242       credsClone.database = spec.database;
243
244       dataSource.query(existsSql, credsClone, function (err, res) {
245         if (err) {
246           callback(err);
247         }
248         if (spec.wipeViews || res.rowCount === 0) {
249           // xt.orm doesn't exist, because this is probably a brand-new DB.
250           // No problem! That just means that there are no pre-existing ORMs.
251           spec.orms = [];
252           installDatabase(spec, callback);
253         } else {
254           dataSource.query(ormTestSql, credsClone, function (err, res) {
255             if (err) {
256               callback(err);
257             }
258             spec.orms = res.rows;
259             installDatabase(spec, callback);
260           });
261         }
262       });
263     };
264
265     //
266     // Install all the databases
267     //
268     async.map(specs, preInstallDatabase, function (err, res) {
269       if (err) {
270         winston.error(err.message, err.stack, err);
271         if (masterCallback) {
272           masterCallback(err);
273         }
274         return;
275       }
276       winston.info("Success installing all scripts.");
277       winston.info("Cleaning up.");
278       clientBuilder.cleanup(specs, function (err) {
279         if (masterCallback) {
280           masterCallback(err, res);
281         }
282       });
283     });
284   };
285
286 }());