6f646955c89231808766d830f1c1a83ab81f5bab
[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, when we delegate to exec psql the err var will not be set even
194         // on the case of error.
195         allSql = "\\set ON_ERROR_STOP TRUE;\n" + allSql;
196
197         winston.info("Applying build to database " + spec.database);
198         credsClone.database = spec.database;
199         sendToDatabase(allSql, credsClone, spec, function (err, res) {
200           if (spec.populateData && creds.encryptionKeyFile) {
201             var populateSql = "DO $$ XT.disableLocks = true; $$ language plv8;";
202             var encryptionKey = fs.readFileSync(path.resolve(__dirname, "../../node-datasource", creds.encryptionKeyFile), "utf8");
203             var patches = require(path.join(__dirname, "../../enyo-client/database/source/populate_data")).patches;
204             _.each(patches, function (patch) {
205               patch.encryptionKey = encryptionKey;
206               patch.username = creds.username;
207               populateSql += "select xt.patch(\'" + JSON.stringify(patch) + "\');";
208             });
209             populateSql += "DO $$ XT.disableLocks = undefined; $$ language plv8;";
210             dataSource.query(populateSql, credsClone, databaseCallback);
211           } else {
212             databaseCallback(err, res);
213           }
214         });
215       });
216     };
217
218     //
219     // Step 1:
220     // Okay, before we install the database there is ONE thing we need to check,
221     // which is the pre-installed ORMs. Check that now.
222     //
223     var preInstallDatabase = function (spec, callback) {
224       var existsSql = "select relname from pg_class where relname = 'orm'",
225         credsClone = JSON.parse(JSON.stringify(creds)),
226         ormTestSql = "select orm_namespace as namespace, " +
227           " orm_type as type " +
228           "from xt.orm " +
229           "where not orm_ext;";
230
231       credsClone.database = spec.database;
232
233       dataSource.query(existsSql, credsClone, function (err, res) {
234         if (err) {
235           callback(err);
236         }
237         if (spec.wipeViews || res.rowCount === 0) {
238           // xt.orm doesn't exist, because this is probably a brand-new DB.
239           // No problem! That just means that there are no pre-existing ORMs.
240           spec.orms = [];
241           installDatabase(spec, callback);
242         } else {
243           dataSource.query(ormTestSql, credsClone, function (err, res) {
244             if (err) {
245               callback(err);
246             }
247             spec.orms = res.rows;
248             installDatabase(spec, callback);
249           });
250         }
251       });
252     };
253
254     //
255     // Install all the databases
256     //
257     async.map(specs, preInstallDatabase, function (err, res) {
258       if (err) {
259         winston.error(err.message, err.stack, err);
260         if (masterCallback) {
261           masterCallback(err);
262         }
263         return;
264       }
265       winston.info("Success installing all scripts.");
266       winston.info("Cleaning up.");
267       clientBuilder.cleanup(specs, function (err) {
268         if (masterCallback) {
269           masterCallback(err, res);
270         }
271       });
272     });
273   };
274
275 }());