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