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