merge
[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/delete_system_orms.sql"),
62           function (err, wipeSql) {
63         if (err) {
64           callback(err);
65           return;
66         }
67         extensionSql = wipeSql + extensionSql;
68         callback(null, extensionSql);
69       });
70     } else {
71       callback(null, extensionSql);
72     }
73   };
74
75   var explodeManifest = function (options, manifestCallback) {
76     var manifestFilename = options.manifestFilename;
77     var packageJson;
78     var dbSourceRoot = path.dirname(manifestFilename);
79
80     if (options.extensionPath && fs.existsSync(path.resolve(options.extensionPath, "package.json"))) {
81       packageJson = require(path.resolve(options.extensionPath, "package.json"));
82     }
83     //
84     // Step 2:
85     // Read the manifest files.
86     //
87
88     if (!fs.existsSync(manifestFilename) && packageJson) {
89       console.log("No manifest file " + manifestFilename + ". There is probably no db-side code in the extension.");
90       composeExtensionSql([], packageJson, options, manifestCallback);
91       return;
92
93     } else if (!fs.existsSync(manifestFilename)) {
94       // error condition: no manifest file
95       manifestCallback("Cannot find manifest " + manifestFilename);
96       return;
97     }
98     fs.readFile(manifestFilename, "utf8", function (err, manifestString) {
99       var manifest,
100         databaseScripts,
101         extraManifestPath,
102         defaultSchema,
103         extraManifest,
104         extraManifestScripts,
105         alterPaths = dbSourceRoot.indexOf("foundation-database") < 0;
106
107       try {
108         manifest = JSON.parse(manifestString);
109         databaseScripts = manifest.databaseScripts;
110         defaultSchema = manifest.defaultSchema;
111
112       } catch (error) {
113         // error condition: manifest file is not properly formatted
114         manifestCallback("Manifest is not valid JSON" + manifestFilename);
115         return;
116       }
117
118       //
119       // Step 2b:
120       //
121
122       // supported use cases:
123
124       // 1. add mobilized inventory to quickbooks
125       // need the frozen_manifest, the foundation/manifest, and the mobile manifest
126       // -e ../private-extensions/source/inventory -f
127       // useFrozenScripts, useFoundationScripts
128
129       // 2. add mobilized inventory to masterref (foundation inventory is already there)
130       // need the the foundation/manifest and the mobile manifest
131       // -e ../private-extensions/source/inventory
132       // useFoundationScripts
133
134       // 3. add unmobilized inventory to quickbooks
135       // need the frozen_manifest and the foundation/manifest
136       // -e ../private-extensions/source/inventory/foundation-database -f
137       // useFrozenScripts (useFoundationScripts already taken care of by -e path)
138
139       // 4. upgrade unmobilized inventory
140       // not sure if this is necessary, but it would look like
141       // -e ../private-extensions/source/inventory/foundation-database
142
143       if (options.useFoundationScripts) {
144         extraManifest = JSON.parse(fs.readFileSync(path.join(dbSourceRoot, "../../foundation-database/manifest.js")));
145         defaultSchema = defaultSchema || extraManifest.defaultSchema;
146         extraManifestScripts = extraManifest.databaseScripts;
147         extraManifestScripts = _.map(extraManifestScripts, function (path) {
148           return "../../foundation-database/" + path;
149         });
150         databaseScripts.unshift(extraManifestScripts);
151         databaseScripts = _.flatten(databaseScripts);
152       }
153       if (options.useFrozenScripts) {
154         // Frozen files are not idempotent and should only be run upon first registration
155         extraManifestPath = alterPaths ?
156          path.join(dbSourceRoot, "../../foundation-database/frozen_manifest.js") :
157          path.join(dbSourceRoot, "frozen_manifest.js");
158
159         extraManifest = JSON.parse(fs.readFileSync(extraManifestPath));
160         defaultSchema = defaultSchema || extraManifest.defaultSchema;
161         extraManifestScripts = extraManifest.databaseScripts;
162         if (alterPaths) {
163           extraManifestScripts = _.map(extraManifestScripts, function (path) {
164             return "../../foundation-database/" + path;
165           });
166         }
167         databaseScripts.unshift(extraManifestScripts);
168         databaseScripts = _.flatten(databaseScripts);
169       }
170
171       //
172       // Step 3:
173       // Concatenate together all the files referenced in the manifest.
174       //
175       var getScriptSql = function (filename, scriptCallback) {
176         var fullFilename = path.join(dbSourceRoot, filename);
177         if (!fs.existsSync(fullFilename)) {
178           // error condition: script referenced in manifest.js isn't there
179           scriptCallback(path.join(dbSourceRoot, filename) + " does not exist");
180           return;
181         }
182         fs.readFile(fullFilename, "utf8", function (err, scriptContents) {
183           // error condition: can't read script
184           if (err) {
185             scriptCallback(err);
186             return;
187           }
188           var beforeNoticeSql = "do $$ BEGIN RAISE NOTICE 'Loading file " + path.basename(fullFilename) +
189               "'; END $$ language plpgsql;\n",
190             extname = path.extname(fullFilename).substring(1);
191
192           // convert special files: metasql, uiforms, reports, uijs
193           scriptContents = conversionMap[extname](scriptContents, fullFilename, defaultSchema);
194
195           //
196           // Incorrectly-ended sql files (i.e. no semicolon) make for unhelpful error messages
197           // when we concatenate 100's of them together. Guard against these.
198           //
199           scriptContents = scriptContents.trim();
200           if (scriptContents.charAt(scriptContents.length - 1) !== ';') {
201             // error condition: script is improperly formatted
202             scriptCallback("Error: " + fullFilename + " contents do not end in a semicolon.");
203           }
204
205           scriptCallback(null, '\n' + scriptContents);
206         });
207       };
208       async.mapSeries(databaseScripts || [], getScriptSql, function (err, scriptSql) {
209         var registerSql,
210           dependencies;
211
212         if (err) {
213           manifestCallback(err);
214           return;
215         }
216
217         composeExtensionSql(scriptSql, packageJson || manifest, options, manifestCallback);
218
219       });
220       //
221       // End script installation code
222       //
223     });
224   };
225
226   exports.explodeManifest = explodeManifest;
227 }());