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