Merge pull request #1826 from malfredo32/haxtuple1
[xtuple] / scripts / lib / util / process_manifest.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 _:true */
4
5 (function () {
6   "use strict";
7
8   var _ = require('underscore'),
9     async = require('async'),
10     exec = require('child_process').exec,
11     fs = require('fs'),
12     path = require('path'),
13     conversionMap = require("./convert_specialized").conversionMap,
14     dataSource = require('../../../node-datasource/lib/ext/datasource').dataSource,
15     inspectDatabaseExtensions = require("./inspect_database").inspectDatabaseExtensions;
16
17   // register extension and dependencies
18   var getRegistrationSql = function (options, extensionLocation) {
19     var registerSql = 'do $$ plv8.elog(NOTICE, "About to register extension ' +
20       options.name + '"); $$ language plv8;\n';
21
22     registerSql = registerSql + "select xt.register_extension('%@', '%@', '%@', '', %@);\n"
23       .f(options.name, options.description || options.comment, extensionLocation, options.loadOrder || 9999);
24
25     registerSql = registerSql + "select xt.grant_role_ext('ADMIN', '%@');\n"
26       .f(options.name);
27
28     // TODO: infer dependencies from package.json using peerDependencies
29     var dependencies = options.dependencies || [];
30     _.each(dependencies, function (dependency) {
31       var dependencySql = "select xt.register_extension_dependency('%@', '%@');\n"
32           .f(options.name, dependency),
33         grantDependToAdmin = "select xt.grant_role_ext('ADMIN', '%@');\n"
34           .f(dependency);
35
36       registerSql = registerSql + dependencySql + grantDependToAdmin;
37     });
38     return registerSql;
39   };
40
41   var composeExtensionSql = function (scriptSql, packageFile, options, callback) {
42     // each String of the scriptContents is the concatenated SQL for the script.
43     // join these all together into a single string for the whole extension.
44     var extensionSql = _.reduce(scriptSql, function (memo, script) {
45       return memo + script;
46     }, "");
47
48     if (options.registerExtension) {
49       extensionSql = getRegistrationSql(packageFile, options.extensionLocation) +
50         extensionSql;
51     }
52     if (options.runJsInit) {
53       // unless it it hasn't yet been defined (ie. lib/orm),
54       // running xt.js_init() is probably a good idea.
55       extensionSql = "select xt.js_init();" + extensionSql;
56     }
57
58     if (options.wipeViews) {
59       // If we want to pre-emptively wipe out the views, the best place to do it
60       // is at the start of the core application code
61       fs.readFile(path.join(__dirname, "../../../enyo-client/database/source/wipe_views.sql"),
62           function (err, wipeSql) {
63         if (err) {
64           callback(err);
65           return;
66         }
67         extensionSql = wipeSql + extensionSql;
68         callback(null, extensionSql);
69       });
70
71     } else if (options.wipeOrms) {
72       // If we want to pre-emptively wipe out the views, the best place to do it
73       // is at the start of the core application code
74       fs.readFile(path.join(__dirname, "../../../enyo-client/database/source/delete_system_orms.sql"),
75           function (err, wipeSql) {
76         if (err) {
77           callback(err);
78           return;
79         }
80         extensionSql = wipeSql + extensionSql;
81         callback(null, extensionSql);
82       });
83     } else {
84       callback(null, extensionSql);
85     }
86   };
87
88   var explodeManifest = function (options, manifestCallback) {
89     var manifestFilename = options.manifestFilename;
90     var packageJson;
91     var dbSourceRoot = path.dirname(manifestFilename);
92
93     if (options.extensionPath && fs.existsSync(path.resolve(options.extensionPath, "package.json"))) {
94       packageJson = require(path.resolve(options.extensionPath, "package.json"));
95     }
96     //
97     // Step 2:
98     // Read the manifest files.
99     //
100
101     if (!fs.existsSync(manifestFilename) && packageJson) {
102       console.log("No manifest file " + manifestFilename + ". There is probably no db-side code in the extension.");
103       composeExtensionSql([], packageJson, options, manifestCallback);
104       return;
105
106     } else if (!fs.existsSync(manifestFilename)) {
107       // error condition: no manifest file
108       manifestCallback("Cannot find manifest " + manifestFilename);
109       return;
110     }
111     fs.readFile(manifestFilename, "utf8", function (err, manifestString) {
112       var manifest,
113         databaseScripts,
114         extraManifestPath,
115         defaultSchema,
116         extraManifest,
117         extraManifestScripts,
118         alterPaths = dbSourceRoot.indexOf("foundation-database") < 0;
119
120       try {
121         manifest = JSON.parse(manifestString);
122         databaseScripts = manifest.databaseScripts;
123         defaultSchema = manifest.defaultSchema;
124
125       } catch (error) {
126         // error condition: manifest file is not properly formatted
127         manifestCallback("Manifest is not valid JSON" + manifestFilename);
128         return;
129       }
130
131       //
132       // Step 2b:
133       //
134
135       // supported use cases:
136
137       // 1. add mobilized inventory to quickbooks
138       // need the frozen_manifest, the foundation/manifest, and the mobile manifest
139       // -e ../private-extensions/source/inventory -f
140       // useFrozenScripts, useFoundationScripts
141
142       // 2. add mobilized inventory to masterref (foundation inventory is already there)
143       // need the the foundation/manifest and the mobile manifest
144       // -e ../private-extensions/source/inventory
145       // useFoundationScripts
146
147       // 3. add unmobilized inventory to quickbooks
148       // need the frozen_manifest and the foundation/manifest
149       // -e ../private-extensions/source/inventory/foundation-database -f
150       // useFrozenScripts (useFoundationScripts already taken care of by -e path)
151
152       // 4. upgrade unmobilized inventory
153       // not sure if this is necessary, but it would look like
154       // -e ../private-extensions/source/inventory/foundation-database
155
156       if (options.useFoundationScripts) {
157         extraManifest = JSON.parse(fs.readFileSync(path.join(dbSourceRoot, "../../foundation-database/manifest.js")));
158         defaultSchema = defaultSchema || extraManifest.defaultSchema;
159         extraManifestScripts = extraManifest.databaseScripts;
160         extraManifestScripts = _.map(extraManifestScripts, function (path) {
161           return "../../foundation-database/" + path;
162         });
163         databaseScripts.unshift(extraManifestScripts);
164         databaseScripts = _.flatten(databaseScripts);
165       }
166       if (options.useFrozenScripts) {
167         // Frozen files are not idempotent and should only be run upon first registration
168         extraManifestPath = alterPaths ?
169          path.join(dbSourceRoot, "../../foundation-database/frozen_manifest.js") :
170          path.join(dbSourceRoot, "frozen_manifest.js");
171
172         extraManifest = JSON.parse(fs.readFileSync(extraManifestPath));
173         defaultSchema = defaultSchema || extraManifest.defaultSchema;
174         extraManifestScripts = extraManifest.databaseScripts;
175         if (alterPaths) {
176           extraManifestScripts = _.map(extraManifestScripts, function (path) {
177             return "../../foundation-database/" + path;
178           });
179         }
180         databaseScripts.unshift(extraManifestScripts);
181         databaseScripts = _.flatten(databaseScripts);
182       }
183
184       //
185       // Step 3:
186       // Concatenate together all the files referenced in the manifest.
187       //
188       var getScriptSql = function (filename, scriptCallback) {
189         var fullFilename = path.join(dbSourceRoot, filename);
190         if (!fs.existsSync(fullFilename)) {
191           // error condition: script referenced in manifest.js isn't there
192           scriptCallback(path.join(dbSourceRoot, filename) + " does not exist");
193           return;
194         }
195         fs.readFile(fullFilename, "utf8", function (err, scriptContents) {
196           // error condition: can't read script
197           if (err) {
198             scriptCallback(err);
199             return;
200           }
201           var beforeNoticeSql = "do $$ BEGIN RAISE NOTICE 'Loading file " + path.basename(fullFilename) +
202               "'; END $$ language plpgsql;\n",
203             extname = path.extname(fullFilename).substring(1);
204
205           // convert special files: metasql, uiforms, reports, uijs
206           scriptContents = conversionMap[extname](scriptContents, fullFilename, defaultSchema);
207
208           //
209           // Incorrectly-ended sql files (i.e. no semicolon) make for unhelpful error messages
210           // when we concatenate 100's of them together. Guard against these.
211           //
212           scriptContents = scriptContents.trim();
213           if (scriptContents.charAt(scriptContents.length - 1) !== ';') {
214             // error condition: script is improperly formatted
215             scriptCallback("Error: " + fullFilename + " contents do not end in a semicolon.");
216           }
217
218           scriptCallback(null, '\n' + scriptContents);
219         });
220       };
221       async.mapSeries(databaseScripts || [], getScriptSql, function (err, scriptSql) {
222         var registerSql,
223           dependencies;
224
225         if (err) {
226           manifestCallback(err);
227           return;
228         }
229
230         composeExtensionSql(scriptSql, packageJson || manifest, options, manifestCallback);
231
232       });
233       //
234       // End script installation code
235       //
236     });
237   };
238
239   exports.explodeManifest = explodeManifest;
240 }());