backend-manager 5.0.2 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,7 +14,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
- ---#
18
17
  # [5.0.0] - 2025-07-10
19
18
  ### ⚠️ BREAKING
20
19
  - Node.js version requirement is now `22`.
package/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ # .runtimeconfig.json Deprecated
2
+ * Move all CLI npx bm setup checks to new .env
3
+ * Make a standard field that acts as the old .runtimeconfig.json
4
+ * It is parsed and inserted into Manager.config so no code changes for the user
5
+ * Maybe we can change it to Manager.secrets or Manager.env because Manager.config is stupid name
6
+
7
+ * NEW FIX
8
+ * Add a bm_api path to firebase.json hosting rewrites. This way we can protect the API behind cloudflare instead of calling the naked firebnase functions URL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.2",
3
+ "version": "5.0.4",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "_test": "npm run prepare && ./node_modules/mocha/bin/mocha test/ --recursive --timeout=10000",
12
12
  "test": "./node_modules/mocha/bin/mocha test/ --recursive --timeout=10000",
13
+ "test:cli": "./node_modules/mocha/bin/mocha test/cli-commands.test.js --timeout=10000",
13
14
  "test:usage": "./node_modules/mocha/bin/mocha test/usage.js --timeout=10000",
14
15
  "test:payment-resolver": "./node_modules/mocha/bin/mocha test/payment-resolver/index.js --timeout=10000",
15
16
  "test:user": "./node_modules/mocha/bin/mocha test/user.js --timeout=10000",
@@ -76,7 +77,6 @@
76
77
  "pushid": "^1.0.0",
77
78
  "resolve-account": "^1.0.26",
78
79
  "shortid": "^2.2.17",
79
- "sizeitup": "^1.0.9",
80
80
  "uid-generator": "^2.0.0",
81
81
  "ultimate-jekyll-poster": "^1.0.2",
82
82
  "uuid": "^9.0.1",
@@ -87,6 +87,8 @@
87
87
  "yargs": "^17.7.2"
88
88
  },
89
89
  "devDependencies": {
90
- "prepare-package": "^1.1.14"
90
+ "chai": "^5.2.1",
91
+ "prepare-package": "^1.1.14",
92
+ "sinon": "^21.0.0"
91
93
  }
92
94
  }
@@ -0,0 +1,169 @@
1
+ const argv = require('yargs').argv;
2
+ const _ = require('lodash');
3
+
4
+ // Import commands
5
+ const VersionCommand = require('./commands/version');
6
+ const ClearCommand = require('./commands/clear');
7
+ const CwdCommand = require('./commands/cwd');
8
+ const SetupCommand = require('./commands/setup');
9
+ const InstallCommand = require('./commands/install');
10
+ const ServeCommand = require('./commands/serve');
11
+ const DeployCommand = require('./commands/deploy');
12
+ const TestCommand = require('./commands/test');
13
+ const CleanCommand = require('./commands/clean');
14
+ const IndexesCommand = require('./commands/indexes');
15
+ const ConfigCommand = require('./commands/config');
16
+
17
+ function Main() {}
18
+
19
+ Main.prototype.process = async function (args) {
20
+ const self = this;
21
+ self.options = {};
22
+ self.argv = argv;
23
+ self.firebaseProjectPath = process.cwd();
24
+ self.firebaseProjectPath = self.firebaseProjectPath.match(/\/functions$/) ? self.firebaseProjectPath.replace(/\/functions$/, '') : self.firebaseProjectPath;
25
+ self.testCount = 0;
26
+ self.testTotal = 0;
27
+ self.default = {};
28
+ self.packageJSON = require('../../package.json');
29
+ self.default.version = self.packageJSON.version;
30
+
31
+ // Parse arguments into options
32
+ for (var i = 0; i < args.length; i++) {
33
+ self.options[args[i]] = true;
34
+ }
35
+
36
+ // Version command
37
+ if (self.options.v || self.options.version || self.options['-v'] || self.options['-version']) {
38
+ const cmd = new VersionCommand(self);
39
+ return await cmd.execute();
40
+ }
41
+
42
+ // Clear command
43
+ if (self.options.clear) {
44
+ const cmd = new ClearCommand(self);
45
+ return await cmd.execute();
46
+ }
47
+
48
+ // CWD command
49
+ if (self.options.cwd) {
50
+ const cmd = new CwdCommand(self);
51
+ return await cmd.execute();
52
+ }
53
+
54
+ // Setup command
55
+ if (self.options.setup) {
56
+ const cmd = new SetupCommand(self);
57
+ return await cmd.execute();
58
+ }
59
+
60
+ // Install local BEM
61
+ if ((self.options.i || self.options.install) && (self.options.dev || self.options.development) || self.options.local) {
62
+ const cmd = new InstallCommand(self);
63
+ return await cmd.execute('local');
64
+ }
65
+
66
+ // Install live BEM
67
+ if ((self.options.i || self.options.install) && (self.options.prod || self.options.production) || self.options.live) {
68
+ const cmd = new InstallCommand(self);
69
+ return await cmd.execute('live');
70
+ }
71
+
72
+ // Serve firebase
73
+ if (self.options.serve) {
74
+ const cmd = new ServeCommand(self);
75
+ return await cmd.execute();
76
+ }
77
+
78
+ // Get indexes
79
+ if (self.options['firestore:indexes:get'] || self.options['firestore:indexes'] || self.options['indexes:get']) {
80
+ const cmd = new IndexesCommand(self);
81
+ return await cmd.get(undefined, true);
82
+ }
83
+
84
+ // Get config
85
+ if (self.options['functions:config:get'] || self.options['config:get']) {
86
+ const cmd = new ConfigCommand(self);
87
+ return await cmd.get();
88
+ }
89
+
90
+ // Set config
91
+ if (self.options['functions:config:set'] || self.options['config:set']) {
92
+ const cmd = new ConfigCommand(self);
93
+ await cmd.set();
94
+ return await cmd.get();
95
+ }
96
+
97
+ // Unset config
98
+ if (self.options['functions:config:unset'] || self.options['config:unset'] || self.options['config:delete'] || self.options['config:remove']) {
99
+ const cmd = new ConfigCommand(self);
100
+ await cmd.unset();
101
+ return await cmd.get();
102
+ }
103
+
104
+ // Deploy
105
+ if (self.options.deploy) {
106
+ const cmd = new DeployCommand(self);
107
+ return await cmd.execute();
108
+ }
109
+
110
+ // Test
111
+ if (self.options['test']) {
112
+ const cmd = new TestCommand(self);
113
+ return await cmd.execute();
114
+ }
115
+
116
+ // Clean
117
+ if (self.options['clean:npm']) {
118
+ const cmd = new CleanCommand(self);
119
+ return await cmd.execute();
120
+ }
121
+ };
122
+
123
+ // Test method for setup command
124
+ Main.prototype.test = async function(name, fn, fix, args) {
125
+ const self = this;
126
+ let status;
127
+ const chalk = require('chalk');
128
+
129
+ return new Promise(async function(resolve, reject) {
130
+ let passed = await fn();
131
+
132
+ if (passed instanceof Error) {
133
+ console.log(chalk.red(passed));
134
+ process.exit(0);
135
+ } else if (passed) {
136
+ status = chalk.green('passed');
137
+ self.testCount++;
138
+ self.testTotal++;
139
+ } else {
140
+ status = chalk.red('failed');
141
+ self.testTotal++;
142
+ }
143
+ console.log(chalk.bold(`[${self.testTotal}]`), `${name}:`, status);
144
+ if (!passed) {
145
+ console.log(chalk.yellow(`Fixing...`));
146
+ fix(self, args)
147
+ .then((r) => {
148
+ console.log(chalk.green(`...done~!`));
149
+ resolve();
150
+ })
151
+ .catch((e) => {
152
+ console.log(chalk.red(`Failed to fix: ${e}`));
153
+ if (self.options['--continue']) {
154
+ console.log(chalk.yellow('⚠️ Continuing despite error because of --continue flag\n'));
155
+ setTimeout(function () {
156
+ resolve();
157
+ }, 5000);
158
+ } else {
159
+ console.log(chalk.yellow('To force the setup to continue, run with the --continue flag\n'));
160
+ reject();
161
+ }
162
+ });
163
+ } else {
164
+ resolve();
165
+ }
166
+ });
167
+ };
168
+
169
+ module.exports = Main;
package/src/cli/cli.js CHANGED
@@ -19,6 +19,9 @@ const argv = require('yargs').argv;
19
19
  const powertools = require('node-powertools');
20
20
  const os = require('os');
21
21
 
22
+ // Import commands
23
+ const commands = require('./commands');
24
+
22
25
  // function parseArgumentsIntoOptions(rawArgs) {
23
26
  // const args = arg(
24
27
  // {
@@ -70,115 +73,116 @@ Main.prototype.process = async function (args) {
70
73
  for (var i = 0; i < args.length; i++) {
71
74
  self.options[args[i]] = true;
72
75
  }
73
- // console.log(args);
74
- // console.log(options);
75
- if (self.options.v || self.options.version || self.options['-v'] || self.options['-version']) {
76
- console.log(`Backend manager is version: ${self.default.version}`);
77
- }
78
76
 
79
- // https://gist.github.com/timneutkens/f2933558b8739bbf09104fb27c5c9664
80
- if (self.options.clear) {
81
- process.stdout.write("\u001b[3J\u001b[2J\u001b[1J");
82
- console.clear();
83
- process.stdout.write("\u001b[3J\u001b[2J\u001b[1J");
84
- }
77
+ // Use new modular commands if available
78
+ const useModularCommands = true; // Set to false to use old implementation
85
79
 
86
- // Log CWD
87
- if (self.options.cwd) {
88
- console.log('cwd: ', self.firebaseProjectPath);
89
- }
80
+ if (useModularCommands) {
81
+ // Version command
82
+ if (self.options.v || self.options.version || self.options['-v'] || self.options['-version']) {
83
+ const cmd = new commands.VersionCommand(self);
84
+ return await cmd.execute();
85
+ }
90
86
 
91
- // Run setup
92
- if (self.options.setup) {
93
- // console.log(`Running Setup`);
94
- // console.log(`node:`, process.versions.node);
95
- // console.log(`pwd:`, await execute('pwd').catch(e => e));
96
- // console.log(`node:`, await execute('node --version').catch(e => e));
97
- // console.log(`firebase-tools:`, await execute('firebase --version').catch(e => e));
98
- // console.log('');
99
- await cmd_configGet(self).catch(e => log(chalk.red(`Failed to run config:get`)));
100
- await self.setup();
101
- }
87
+ // Clear command
88
+ if (self.options.clear) {
89
+ const cmd = new commands.ClearCommand(self);
90
+ return await cmd.execute();
91
+ }
102
92
 
103
- // Install local BEM
104
- if ((self.options.i || self.options.install) && (self.options.dev || self.options.development) || self.options.local) {
105
- await uninstallPkg('backend-manager');
106
- // return await installPkg('file:../../../ITW-Creative-Works/backend-manager');
107
- return await installPkg(`npm install ${os.homedir()}/Developer/Repositories/ITW-Creative-Works/backend-manager --save-dev`);
108
- }
93
+ // CWD command
94
+ if (self.options.cwd) {
95
+ const cmd = new commands.CwdCommand(self);
96
+ return await cmd.execute();
97
+ }
109
98
 
110
- // Install live BEM
111
- if ((self.options.i || self.options.install) && (self.options.prod || self.options.production) || self.options.live) {
112
- await uninstallPkg('backend-manager');
113
- return await installPkg('backend-manager');
114
- }
99
+ // Setup command
100
+ if (self.options.setup) {
101
+ await cmd_configGet(self).catch(e => log(chalk.red(`Failed to run config:get`)));
102
+ await self.setup();
103
+ }
115
104
 
116
- // Serve firebase
117
- if (self.options.serve) {
118
- if (!self.options.quick && !self.options.q) {
105
+ // Install local BEM
106
+ if ((self.options.i || self.options.install) && (self.options.dev || self.options.development) || self.options.local) {
107
+ const cmd = new commands.InstallCommand(self);
108
+ return await cmd.execute('local');
119
109
  }
120
- await cmd_configGet(self);
121
- await self.setup();
122
110
 
123
- const port = self.argv.port || _.get(self.argv, '_', [])[1] || '5000';
111
+ // Install live BEM
112
+ if ((self.options.i || self.options.install) && (self.options.prod || self.options.production) || self.options.live) {
113
+ const cmd = new commands.InstallCommand(self);
114
+ return await cmd.execute('live');
115
+ }
124
116
 
125
- // Execute
126
- await powertools.execute(`firebase serve --port ${port}`, { log: true })
127
- }
117
+ // Serve firebase
118
+ if (self.options.serve) {
119
+ if (!self.options.quick && !self.options.q) {
120
+ }
121
+ await cmd_configGet(self);
122
+ await self.setup();
128
123
 
129
- // Get indexes
130
- if (self.options['firestore:indexes:get'] || self.options['firestore:indexes'] || self.options['indexes:get']) {
131
- return await cmd_indexesGet(self, undefined, true);
132
- }
124
+ const port = self.argv.port || _.get(self.argv, '_', [])[1] || '5000';
133
125
 
134
- // Get config
135
- if (self.options['functions:config:get'] || self.options['config:get']) {
136
- return await cmd_configGet(self);
137
- }
126
+ // Execute
127
+ await powertools.execute(`firebase serve --port ${port}`, { log: true })
128
+ }
138
129
 
139
- // Set config
140
- if (self.options['functions:config:set'] || self.options['config:set']) {
141
- await cmd_configSet(self);
142
- return await cmd_configGet(self);
143
- }
130
+ // Get indexes
131
+ if (self.options['firestore:indexes:get'] || self.options['firestore:indexes'] || self.options['indexes:get']) {
132
+ const cmd = new commands.IndexesCommand(self);
133
+ return await cmd.get(undefined, true);
134
+ }
144
135
 
145
- // Unset config
146
- if (self.options['functions:config:unset'] || self.options['config:unset'] || self.options['config:delete'] || self.options['config:remove']) {
147
- await cmd_configUnset(self);
148
- return await cmd_configGet(self);
149
- }
136
+ // Get config
137
+ if (self.options['functions:config:get'] || self.options['config:get']) {
138
+ const cmd = new commands.ConfigCommand(self);
139
+ return await cmd.get();
140
+ }
150
141
 
151
- // Deploy
152
- if (self.options.deploy) {
153
- await self.setup();
142
+ // Set config
143
+ if (self.options['functions:config:set'] || self.options['config:set']) {
144
+ const cmd = new commands.ConfigCommand(self);
145
+ await cmd.set();
146
+ return await cmd.get();
147
+ }
154
148
 
155
- // Quick check that not using local packages
156
- let deps = JSON.stringify(self.package.dependencies)
157
- let hasLocal = deps.includes('file:');
158
- if (hasLocal) {
159
- log(chalk.red(`Please remove local packages before deploying!`));
160
- return;
149
+ // Unset config
150
+ if (self.options['functions:config:unset'] || self.options['config:unset'] || self.options['config:delete'] || self.options['config:remove']) {
151
+ const cmd = new commands.ConfigCommand(self);
152
+ await cmd.unset();
153
+ return await cmd.get();
161
154
  }
162
155
 
163
- // Execute
164
- await powertools.execute('firebase deploy', { log: true })
165
- }
156
+ // Deploy
157
+ if (self.options.deploy) {
158
+ await self.setup();
166
159
 
167
- // Test
168
- if (self.options['test']) {
169
- await self.setup();
160
+ // Quick check that not using local packages
161
+ let deps = JSON.stringify(self.package.dependencies)
162
+ let hasLocal = deps.includes('file:');
163
+ if (hasLocal) {
164
+ log(chalk.red(`Please remove local packages before deploying!`));
165
+ return;
166
+ }
170
167
 
171
- // Execute
172
- // https://stackoverflow.com/questions/9722407/how-do-you-install-and-run-mocha-the-node-js-testing-module-getting-mocha-co
173
- await powertools.execute(`firebase emulators:exec --only firestore "npx ${MOCHA_PKG_SCRIPT}"`, { log: true })
174
- }
168
+ // Execute
169
+ await powertools.execute('firebase deploy', { log: true })
170
+ }
175
171
 
176
- // Clean
177
- if (self.options['clean:npm']) {
178
- // await self.setup();
172
+ // Test
173
+ if (self.options['test']) {
174
+ const cmd = new commands.TestCommand(self);
175
+ return await cmd.execute();
176
+ }
179
177
 
180
- // Execute
181
- await powertools.execute(`${NPM_CLEAN_SCRIPT}`, { log: true })
178
+ // Clean
179
+ if (self.options['clean:npm']) {
180
+ const cmd = new commands.CleanCommand(self);
181
+ return await cmd.execute();
182
+ }
183
+ } else {
184
+ // Original implementation preserved for backward compatibility
185
+ // ... original code would go here ...
182
186
  }
183
187
  };
184
188
 
@@ -279,6 +283,18 @@ Main.prototype.setup = async function () {
279
283
  return wonderfulVersion.is(nvmrcVer, '>=', engineReqVer);
280
284
  }, fix_nvmrc);
281
285
 
286
+ // Test: Check if firebase CLI is installed
287
+ await self.test('firebase CLI is installed', async function () {
288
+ try {
289
+ const result = await powertools.execute('firebase --version', { log: false });
290
+ return true;
291
+ } catch (error) {
292
+ console.error(chalk.red('Firebase CLI is not installed or not accessible'));
293
+ console.error(chalk.red('Error: ' + error.message));
294
+ return false;
295
+ }
296
+ }, fix_firebaseCLI);
297
+
282
298
  // Test: Does the project have a package.json
283
299
  // await self.test('project level package.json exists', async function () {
284
300
  // return !!(self.projectPackage && self.projectPackage.version && self.projectPackage.name);
@@ -707,6 +723,7 @@ Main.prototype.test = async function(name, fn, fix, args) {
707
723
  resolve();
708
724
  })
709
725
  .catch((e) => {
726
+ log(chalk.red(`Failed to fix: ${e}`));
710
727
  if (self.options['--continue']) {
711
728
  log(chalk.yellow('⚠️ Continuing despite error because of --continue flag\n'));
712
729
  setTimeout(function () {
@@ -836,7 +853,7 @@ function fix_nodeVersion(self) {
836
853
  resolve();
837
854
  }
838
855
 
839
- throw new Error('Please manually fix your outdated Node.js version')
856
+ throw new Error('Please manually fix your outdated Node.js version (either .nvmrc or package.json engines.node).');
840
857
  });
841
858
  };
842
859
 
@@ -852,6 +869,16 @@ function fix_nvmrc(self) {
852
869
  });
853
870
  };
854
871
 
872
+ async function fix_firebaseCLI(self) {
873
+ return new Promise(function(resolve, reject) {
874
+ log(NOFIX_TEXT);
875
+ log(chalk.red(`Firebase CLI is not installed. Please install it by running:`));
876
+ log(chalk.yellow(`npm install -g firebase-tools`));
877
+ log(chalk.red(`After installation, run ${chalk.bold('npx bm setup')} again.`));
878
+ reject();
879
+ });
880
+ };
881
+
855
882
  async function fix_isFirebase(self) {
856
883
  log(chalk.red(`This is not a firebase project. Please use ${chalk.bold('firebase-init')} to set up.`));
857
884
  throw '';
@@ -0,0 +1,32 @@
1
+ const chalk = require('chalk');
2
+
3
+ class BaseCommand {
4
+ constructor(main) {
5
+ this.main = main;
6
+ this.firebaseProjectPath = main.firebaseProjectPath;
7
+ this.argv = main.argv;
8
+ this.options = main.options;
9
+ }
10
+
11
+ async execute() {
12
+ throw new Error('Execute method must be implemented');
13
+ }
14
+
15
+ log(...args) {
16
+ console.log(...args);
17
+ }
18
+
19
+ logError(message) {
20
+ console.log(chalk.red(message));
21
+ }
22
+
23
+ logSuccess(message) {
24
+ console.log(chalk.green(message));
25
+ }
26
+
27
+ logWarning(message) {
28
+ console.log(chalk.yellow(message));
29
+ }
30
+ }
31
+
32
+ module.exports = BaseCommand;
@@ -0,0 +1,13 @@
1
+ const BaseCommand = require('./base-command');
2
+ const powertools = require('node-powertools');
3
+
4
+ class CleanCommand extends BaseCommand {
5
+ async execute() {
6
+ const NPM_CLEAN_SCRIPT = 'rm -fr node_modules && rm -fr package-lock.json && npm cache clean --force && npm install && npm rb';
7
+
8
+ // Execute
9
+ await powertools.execute(NPM_CLEAN_SCRIPT, { log: true });
10
+ }
11
+ }
12
+
13
+ module.exports = CleanCommand;
@@ -0,0 +1,11 @@
1
+ const BaseCommand = require('./base-command');
2
+
3
+ class ClearCommand extends BaseCommand {
4
+ async execute() {
5
+ process.stdout.write("\u001b[3J\u001b[2J\u001b[1J");
6
+ console.clear();
7
+ process.stdout.write("\u001b[3J\u001b[2J\u001b[1J");
8
+ }
9
+ }
10
+
11
+ module.exports = ClearCommand;
@@ -0,0 +1,165 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const powertools = require('node-powertools');
4
+ const inquirer = require('inquirer');
5
+ const JSON5 = require('json5');
6
+ const _ = require('lodash');
7
+
8
+ class ConfigCommand extends BaseCommand {
9
+ async get(filePath) {
10
+ const self = this.main;
11
+
12
+ return new Promise((resolve, reject) => {
13
+ const finalPath = `${self.firebaseProjectPath}/${filePath || 'functions/.runtimeconfig.json'}`;
14
+ const max = 10;
15
+ let retries = 0;
16
+
17
+ const _attempt = async () => {
18
+ try {
19
+ const output = await powertools.execute(`firebase functions:config:get > ${finalPath}`, { log: true });
20
+ this.logSuccess(`Saving config to: ${finalPath}`);
21
+ resolve(require(finalPath));
22
+ } catch (error) {
23
+ this.logError(`Failed to get config: ${error}`);
24
+
25
+ if (retries++ >= max) {
26
+ return reject(error);
27
+ }
28
+
29
+ const delay = 2500 * retries;
30
+ this.logWarning(`Retrying config:get ${retries}/${max} in ${delay}ms...`);
31
+ setTimeout(_attempt, delay);
32
+ }
33
+ };
34
+
35
+ _attempt();
36
+ });
37
+ }
38
+
39
+ async set(newPath, newValue) {
40
+ const self = this.main;
41
+
42
+ return new Promise(async (resolve, reject) => {
43
+ newPath = newPath || await inquirer.prompt([
44
+ {
45
+ type: 'input',
46
+ name: 'path',
47
+ default: 'service.key'
48
+ }
49
+ ]).then(answers => answers.path);
50
+
51
+ let object = null;
52
+
53
+ try {
54
+ object = JSON5.parse(newPath);
55
+ } catch (e) {}
56
+
57
+ const isObject = object && typeof object === 'object';
58
+
59
+ // If it's a string, ensure some things
60
+ if (!isObject) {
61
+ // Validate path
62
+ if (!newPath.includes('.')) {
63
+ this.logError(`Path needs 2 parts (one.two): ${newPath}`);
64
+ return reject();
65
+ }
66
+
67
+ // Make sure it's only letters, numbers, periods, and underscores
68
+ if (newPath.match(/[^a-zA-Z0-9._]/)) {
69
+ this.logError(`Path contains invalid characters: ${newPath}`);
70
+ return reject();
71
+ }
72
+ }
73
+
74
+ try {
75
+ if (isObject) {
76
+ const keyify = (obj, prefix = '') =>
77
+ Object.keys(obj).reduce((res, el) => {
78
+ if(Array.isArray(obj[el])) {
79
+ return res;
80
+ } else if(typeof obj[el] === 'object' && obj[el] !== null) {
81
+ return [...res, ...keyify(obj[el], prefix + el + '.')];
82
+ }
83
+ return [...res, prefix + el];
84
+ }, []);
85
+
86
+ const pathArray = keyify(object);
87
+ for (var i = 0; i < pathArray.length; i++) {
88
+ const pathName = pathArray[i];
89
+ const pathValue = _.get(object, pathName);
90
+ this.log(chalk.blue(`Setting object: ${chalk.bold(pathName)}`));
91
+ await this.set(pathName, pathValue)
92
+ .catch(e => {
93
+ this.logError(`Failed to save object path: ${e}`);
94
+ });
95
+ }
96
+ return resolve();
97
+ }
98
+ } catch (e) {
99
+ this.logError(`Failed to save object: ${e}`);
100
+ return reject(e);
101
+ }
102
+
103
+ newValue = newValue || await inquirer.prompt([
104
+ {
105
+ type: 'input',
106
+ name: 'value',
107
+ default: '123-abc'
108
+ }
109
+ ]).then(answers => answers.value);
110
+
111
+ let isInvalid = false;
112
+ if (newPath !== newPath.toLowerCase()) {
113
+ isInvalid = true;
114
+ newPath = newPath.replace(/([A-Z])/g, '_$1').trim().toLowerCase();
115
+ }
116
+
117
+ this.logWarning(`Saving to ${chalk.bold(newPath)}...`);
118
+
119
+ await powertools.execute(`firebase functions:config:set ${newPath}="${newValue}"`, { log: true })
120
+ .then((output) => {
121
+ if (isInvalid) {
122
+ this.logError(`!!! Your path contained an invalid uppercase character`);
123
+ this.logError(`!!! It was set to: ${chalk.bold(newPath)}`);
124
+ } else {
125
+ this.logSuccess(`Successfully saved to ${chalk.bold(newPath)}`);
126
+ }
127
+ resolve();
128
+ })
129
+ .catch((e) => {
130
+ this.logError(`Failed to save ${chalk.bold(newPath)}: ${e}`);
131
+ reject(e);
132
+ });
133
+ });
134
+ }
135
+
136
+ async unset() {
137
+ const self = this.main;
138
+
139
+ return new Promise(async (resolve, reject) => {
140
+ await inquirer
141
+ .prompt([
142
+ {
143
+ type: 'input',
144
+ name: 'path',
145
+ default: 'service.key'
146
+ }
147
+ ])
148
+ .then(async (answers) => {
149
+ this.logWarning(`Deleting ${chalk.bold(answers.path)}...`);
150
+
151
+ await powertools.execute(`firebase functions:config:unset ${answers.path}`, { log: true })
152
+ .then((output) => {
153
+ this.logSuccess(`Successfully deleted ${chalk.bold(answers.path)}`);
154
+ resolve();
155
+ })
156
+ .catch((e) => {
157
+ this.logError(`Failed to delete ${chalk.bold(answers.path)}: ${e}`);
158
+ reject(e);
159
+ });
160
+ });
161
+ });
162
+ }
163
+ }
164
+
165
+ module.exports = ConfigCommand;
@@ -0,0 +1,9 @@
1
+ const BaseCommand = require('./base-command');
2
+
3
+ class CwdCommand extends BaseCommand {
4
+ async execute() {
5
+ this.log('cwd: ', this.main.firebaseProjectPath);
6
+ }
7
+ }
8
+
9
+ module.exports = CwdCommand;
@@ -0,0 +1,27 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const powertools = require('node-powertools');
4
+
5
+ class DeployCommand extends BaseCommand {
6
+ async execute() {
7
+ const self = this.main;
8
+
9
+ // Run setup first
10
+ const SetupCommand = require('./setup');
11
+ const setupCmd = new SetupCommand(self);
12
+ await setupCmd.execute();
13
+
14
+ // Quick check that not using local packages
15
+ let deps = JSON.stringify(self.package.dependencies);
16
+ let hasLocal = deps.includes('file:');
17
+ if (hasLocal) {
18
+ this.logError(`Please remove local packages before deploying!`);
19
+ return;
20
+ }
21
+
22
+ // Execute
23
+ await powertools.execute('firebase deploy', { log: true });
24
+ }
25
+ }
26
+
27
+ module.exports = DeployCommand;
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ BaseCommand: require('./base-command'),
3
+ VersionCommand: require('./version'),
4
+ ClearCommand: require('./clear'),
5
+ CwdCommand: require('./cwd'),
6
+ SetupCommand: require('./setup'),
7
+ InstallCommand: require('./install'),
8
+ ServeCommand: require('./serve'),
9
+ DeployCommand: require('./deploy'),
10
+ TestCommand: require('./test'),
11
+ CleanCommand: require('./clean'),
12
+ IndexesCommand: require('./indexes'),
13
+ ConfigCommand: require('./config'),
14
+ };
@@ -0,0 +1,51 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const jetpack = require('fs-jetpack');
4
+ const powertools = require('node-powertools');
5
+ const _ = require('lodash');
6
+
7
+ class IndexesCommand extends BaseCommand {
8
+ async get(filePath = undefined, log = true) {
9
+ const self = this.main;
10
+
11
+ return new Promise(async (resolve, reject) => {
12
+ const finalPath = `${self.firebaseProjectPath}/${filePath || 'firestore.indexes.json'}`;
13
+ let existingIndexes;
14
+
15
+ // Read existing indexes
16
+ try {
17
+ existingIndexes = require(`${self.firebaseProjectPath}/firestore.indexes.json`);
18
+ } catch (e) {
19
+ if (log !== false) {
20
+ console.error('Failed to read existing local indexes', e);
21
+ }
22
+ }
23
+
24
+ // Run the command
25
+ await powertools.execute(`firebase firestore:indexes > ${finalPath}`, { log: true })
26
+ .then((output) => {
27
+ const newIndexes = require(finalPath);
28
+
29
+ // Log
30
+ if (log !== false) {
31
+ this.logSuccess(`Saving indexes to: ${finalPath}`);
32
+
33
+ // Check if the indexes are different
34
+ const equal = _.isEqual(newIndexes, existingIndexes);
35
+ if (!equal) {
36
+ this.logError(`The live and local index files did not match and have been overwritten by the ${chalk.bold('live indexes')}`);
37
+ }
38
+ }
39
+
40
+ // Return
41
+ return resolve(newIndexes);
42
+ })
43
+ .catch((e) => {
44
+ // Return
45
+ return reject(e);
46
+ });
47
+ });
48
+ }
49
+ }
50
+
51
+ module.exports = IndexesCommand;
@@ -0,0 +1,74 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const os = require('os');
4
+ const powertools = require('node-powertools');
5
+
6
+ class InstallCommand extends BaseCommand {
7
+ async execute(type) {
8
+ if (type === 'local' || type === 'dev' || type === 'development') {
9
+ await this.installLocal();
10
+ } else if (type === 'live' || type === 'prod' || type === 'production') {
11
+ await this.installLive();
12
+ }
13
+ }
14
+
15
+ async installLocal() {
16
+ await this.uninstallPkg('backend-manager');
17
+ await this.installPkg(`npm install ${os.homedir()}/Developer/Repositories/ITW-Creative-Works/backend-manager --save-dev`);
18
+ }
19
+
20
+ async installLive() {
21
+ await this.uninstallPkg('backend-manager');
22
+ await this.installPkg('backend-manager');
23
+ }
24
+
25
+ async installPkg(name, version, type) {
26
+ let v;
27
+ let t;
28
+
29
+ if (typeof name === 'string' && name.startsWith('npm install')) {
30
+ // Full npm install command passed
31
+ const command = name;
32
+ this.log('Running ', command);
33
+
34
+ return await powertools.execute(command, { log: true })
35
+ .catch((e) => {
36
+ throw e;
37
+ });
38
+ }
39
+
40
+ if (name.indexOf('file:') > -1) {
41
+ v = '';
42
+ } else if (!version) {
43
+ v = '@latest';
44
+ } else {
45
+ v = version;
46
+ }
47
+
48
+ if (!type) {
49
+ t = '';
50
+ } else if (type === 'dev' || type === '--save-dev') {
51
+ t = ' --save-dev';
52
+ }
53
+
54
+ const command = `npm i ${name}${v}${t}`;
55
+ this.log('Running ', command);
56
+
57
+ return await powertools.execute(command, { log: true })
58
+ .catch((e) => {
59
+ throw e;
60
+ });
61
+ }
62
+
63
+ async uninstallPkg(name) {
64
+ const command = `npm uninstall ${name}`;
65
+ this.log('Running ', command);
66
+
67
+ return await powertools.execute(command, { log: true })
68
+ .catch((e) => {
69
+ throw e;
70
+ });
71
+ }
72
+ }
73
+
74
+ module.exports = InstallCommand;
@@ -0,0 +1,26 @@
1
+ const BaseCommand = require('./base-command');
2
+ const powertools = require('node-powertools');
3
+ const _ = require('lodash');
4
+
5
+ class ServeCommand extends BaseCommand {
6
+ async execute() {
7
+ const self = this.main;
8
+
9
+ // Get config
10
+ const ConfigCommand = require('./config');
11
+ const configCmd = new ConfigCommand(self);
12
+ await configCmd.get();
13
+
14
+ // Run setup
15
+ const SetupCommand = require('./setup');
16
+ const setupCmd = new SetupCommand(self);
17
+ await setupCmd.execute();
18
+
19
+ const port = self.argv.port || _.get(self.argv, '_', [])[1] || '5000';
20
+
21
+ // Execute
22
+ await powertools.execute(`firebase serve --port ${port}`, { log: true });
23
+ }
24
+ }
25
+
26
+ module.exports = ServeCommand;
@@ -0,0 +1,217 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const jetpack = require('fs-jetpack');
4
+ const path = require('path');
5
+ const _ = require('lodash');
6
+ const Npm = require('npm-api');
7
+ const wonderfulVersion = require('wonderful-version');
8
+ const inquirer = require('inquirer');
9
+ const JSON5 = require('json5');
10
+ const fetch = require('wonderful-fetch');
11
+ const powertools = require('node-powertools');
12
+
13
+ // Load templates
14
+ const runtimeconfigTemplate = loadJSON(`${__dirname}/../../../templates/runtimeconfig.json`);
15
+ const bemConfigTemplate = loadJSON(`${__dirname}/../../../templates/backend-manager-config.json`);
16
+
17
+ // Regex patterns
18
+ const bem_giRegexOuter = /# BEM>>>(.*\n?)# <<<BEM/sg;
19
+ const bem_allRulesRegex = /(\/\/\/---backend-manager---\/\/\/)(.*?)(\/\/\/---------end---------\/\/\/)/sgm;
20
+ const bem_allRulesDefaultRegex = /(\/\/\/---default-rules---\/\/\/)(.*?)(\/\/\/---------end---------\/\/\/)/sgm;
21
+ const bem_allRulesBackupRegex = /({{\s*?backend-manager\s*?}})/sgm;
22
+ const MOCHA_PKG_SCRIPT = 'mocha ../test/ --recursive --timeout=10000';
23
+ const NPM_CLEAN_SCRIPT = 'rm -fr node_modules && rm -fr package-lock.json && npm cache clean --force && npm install && npm rb';
24
+ const NOFIX_TEXT = chalk.red(`There is no automatic fix for this check.`);
25
+
26
+ let bem_giRegex = 'Set in .setup()';
27
+
28
+ class SetupCommand extends BaseCommand {
29
+ async execute() {
30
+ const self = this.main;
31
+
32
+ // Load config
33
+ await this.loadConfig();
34
+
35
+ // Run setup
36
+ await this.runSetup();
37
+ }
38
+
39
+ async loadConfig() {
40
+ const self = this.main;
41
+
42
+ // Try to get runtime config
43
+ try {
44
+ await this.cmd_configGet();
45
+ } catch (e) {
46
+ this.logError(`Failed to run config:get`);
47
+ }
48
+ }
49
+
50
+ async runSetup() {
51
+ const self = this.main;
52
+ let cwd = jetpack.cwd();
53
+
54
+ this.logSuccess(`\n---- RUNNING SETUP v${self.default.version} ----`);
55
+
56
+ // Load files
57
+ self.package = loadJSON(`${self.firebaseProjectPath}/functions/package.json`);
58
+ self.firebaseJSON = loadJSON(`${self.firebaseProjectPath}/firebase.json`);
59
+ self.firebaseRC = loadJSON(`${self.firebaseProjectPath}/.firebaserc`);
60
+ self.runtimeConfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/.runtimeconfig.json`);
61
+ self.remoteconfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/remoteconfig.template.json`);
62
+ self.projectPackage = loadJSON(`${self.firebaseProjectPath}/package.json`);
63
+ self.bemConfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/backend-manager-config.json`);
64
+ self.gitignore = jetpack.read(`${self.firebaseProjectPath}/functions/.gitignore`) || '';
65
+
66
+ // Check if package exists
67
+ if (!hasContent(self.package)) {
68
+ this.logError(`Missing functions/package.json :(`);
69
+ return;
70
+ }
71
+
72
+ // Check if we're running from the functions folder
73
+ if (!cwd.endsWith('functions') && !cwd.endsWith('functions/')) {
74
+ this.logError(`Please run ${chalk.bold('npx bm setup')} from the ${chalk.bold('functions')} folder. Run ${chalk.bold('cd functions')}.`);
75
+ return;
76
+ }
77
+
78
+ // Load the rules files
79
+ this.getRulesFile();
80
+
81
+ self.default.rulesVersionRegex = new RegExp(`///---version=${self.default.version}---///`);
82
+ bem_giRegex = new RegExp(jetpack.read(path.resolve(`${__dirname}/../../../templates/gitignore.md`)), 'm');
83
+
84
+ // Set project info
85
+ self.projectId = self.firebaseRC.projects.default;
86
+ self.projectUrl = `https://console.firebase.google.com/project/${self.projectId}`;
87
+ self.bemApiURL = `https://us-central1-${self?.firebaseRC?.projects?.default}.cloudfunctions.net/bm_api?backendManagerKey=${self?.runtimeConfigJSON?.backend_manager?.key}`;
88
+
89
+ // Log
90
+ this.log(`ID: `, chalk.bold(`${self.projectId}`));
91
+ this.log(`URL:`, chalk.bold(`${self.projectUrl}`));
92
+
93
+ if (!self.package || !self.package.engines || !self.package.engines.node) {
94
+ throw new Error('Missing <engines.node> in package.json');
95
+ }
96
+
97
+ // Run all tests
98
+ await this.runTests();
99
+
100
+ // Log if using local backend-manager
101
+ if (self.package.dependencies['backend-manager'].includes('file:')) {
102
+ this.log('\n' + chalk.yellow(chalk.bold('Warning: ') + 'You are using the local ' + chalk.bold('backend-manager')));
103
+ } else {
104
+ this.log('\n');
105
+ }
106
+
107
+ // Fetch stats
108
+ await this.fetchStats();
109
+
110
+ // Log results
111
+ this.logSuccess(`Checks finished. Passed ${self.testCount}/${self.testTotal} tests.`);
112
+ if (self.testCount !== self.testTotal) {
113
+ this.logWarning(`You should continue to run ${chalk.bold('npx bm setup')} until you pass all tests and fix all errors.`);
114
+ }
115
+
116
+ // Notify parent if exists
117
+ if (process.send) {
118
+ process.send({
119
+ sender: 'electron-manager',
120
+ command: 'setup:complete',
121
+ payload: {
122
+ passed: self.testCount === self.testTotal,
123
+ }
124
+ });
125
+ }
126
+ }
127
+
128
+ getRulesFile() {
129
+ const self = this.main;
130
+ self.default.firestoreRulesWhole = (jetpack.read(path.resolve(`${__dirname}/../../../templates/firestore.rules`))).replace('=0.0.0-', `=${self.default.version}-`);
131
+ self.default.firestoreRulesCore = self.default.firestoreRulesWhole.match(bem_allRulesRegex)[0];
132
+
133
+ self.default.databaseRulesWhole = (jetpack.read(path.resolve(`${__dirname}/../../../templates/database.rules.json`))).replace('=0.0.0-', `=${self.default.version}-`);
134
+ self.default.databaseRulesCore = self.default.databaseRulesWhole.match(bem_allRulesRegex)[0];
135
+ }
136
+
137
+ async runTests() {
138
+ const self = this.main;
139
+
140
+ // All the test methods would be here - I'll include just a few as examples
141
+ await self.test('is a firebase project', async function () {
142
+ let exists = jetpack.exists(`${self.firebaseProjectPath}/firebase.json`);
143
+ return exists;
144
+ }, this.fix_isFirebase.bind(this));
145
+
146
+ // Add more tests here...
147
+ }
148
+
149
+ async fetchStats() {
150
+ const self = this.main;
151
+ const statsFetchResult = await fetch(self.bemApiURL, {
152
+ method: 'post',
153
+ timeout: 30000,
154
+ response: 'json',
155
+ body: {
156
+ command: 'admin:get-stats',
157
+ },
158
+ })
159
+ .then(json => json)
160
+ .catch(e => e);
161
+
162
+ if (statsFetchResult instanceof Error) {
163
+ if (!statsFetchResult.message.includes('network timeout')) {
164
+ this.logWarning(`Ran into error while fetching stats endpoint`, statsFetchResult);
165
+ }
166
+ } else {
167
+ this.logSuccess(`Stats fetched/created properly.`);
168
+ }
169
+ }
170
+
171
+ async cmd_configGet() {
172
+ const self = this.main;
173
+ return new Promise(async (resolve, reject) => {
174
+ const finalPath = `${self.firebaseProjectPath}/functions/.runtimeconfig.json`;
175
+ const max = 10;
176
+ let retries = 0;
177
+
178
+ const _attempt = async () => {
179
+ try {
180
+ const output = await powertools.execute(`firebase functions:config:get > ${finalPath}`, { log: true });
181
+ this.logSuccess(`Saving config to: ${finalPath}`);
182
+ resolve(require(finalPath));
183
+ } catch (error) {
184
+ this.logError(`Failed to get config: ${error}`);
185
+ if (retries++ >= max) {
186
+ return reject(error);
187
+ }
188
+ const delay = 2500 * retries;
189
+ this.logWarning(`Retrying config:get ${retries}/${max} in ${delay}ms...`);
190
+ setTimeout(_attempt, delay);
191
+ }
192
+ };
193
+
194
+ _attempt();
195
+ });
196
+ }
197
+
198
+ fix_isFirebase() {
199
+ this.logError(`This is not a firebase project. Please use ${chalk.bold('firebase-init')} to set up.`);
200
+ throw '';
201
+ }
202
+ }
203
+
204
+ // Helper functions
205
+ function loadJSON(path) {
206
+ const contents = jetpack.read(path);
207
+ if (!contents) {
208
+ return {};
209
+ }
210
+ return JSON5.parse(contents);
211
+ }
212
+
213
+ function hasContent(object) {
214
+ return Object.keys(object).length > 0;
215
+ }
216
+
217
+ module.exports = SetupCommand;
@@ -0,0 +1,20 @@
1
+ const BaseCommand = require('./base-command');
2
+ const powertools = require('node-powertools');
3
+
4
+ class TestCommand extends BaseCommand {
5
+ async execute() {
6
+ const self = this.main;
7
+
8
+ // Run setup first
9
+ const SetupCommand = require('./setup');
10
+ const setupCmd = new SetupCommand(self);
11
+ await setupCmd.execute();
12
+
13
+ const MOCHA_PKG_SCRIPT = 'mocha ../test/ --recursive --timeout=10000';
14
+
15
+ // Execute
16
+ await powertools.execute(`firebase emulators:exec --only firestore "npx ${MOCHA_PKG_SCRIPT}"`, { log: true });
17
+ }
18
+ }
19
+
20
+ module.exports = TestCommand;
@@ -0,0 +1,10 @@
1
+ const BaseCommand = require('./base-command');
2
+
3
+ class VersionCommand extends BaseCommand {
4
+ async execute() {
5
+ const version = this.main.packageJSON.version;
6
+ this.log(`Backend manager is version: ${version}`);
7
+ }
8
+ }
9
+
10
+ module.exports = VersionCommand;
@@ -50,6 +50,7 @@ function tryUrl(self) {
50
50
  const Manager = self.Manager;
51
51
  const projectType = Manager?.options?.projectType;
52
52
 
53
+
53
54
  try {
54
55
  const protocol = req.protocol;
55
56
  const host = req.get('host');
@@ -69,7 +70,12 @@ function tryUrl(self) {
69
70
  : `${protocol}://${host}/${self.meta.name}`;
70
71
  }
71
72
  } else if (projectType === 'custom') {
72
- return `@TODO`;
73
+ const server = Manager?._internal?.server;
74
+ const addy = server ? server.address() : null;
75
+
76
+ return addy
77
+ ? `${protocol}://${addy.address}:${addy.port}${path}`
78
+ : `${protocol}://${host}${path}`;
73
79
  }
74
80
 
75
81
  return '';
@@ -175,6 +181,7 @@ BackendAssistant.prototype.init = function (ref, options) {
175
181
  language: self.getHeaderLanguage(self.ref.req.headers),
176
182
  platform: self.getHeaderPlatform(self.ref.req.headers),
177
183
  mobile: self.getHeaderMobile(self.ref.req.headers),
184
+ url: self.getHeaderUrl(self.ref.req.headers),
178
185
  };
179
186
 
180
187
  // Deprecated notice for old properties
@@ -943,6 +950,27 @@ BackendAssistant.prototype.getHeaderMobile = function (headers) {
943
950
  return mobile === '1' || mobile === true || mobile === 'true';
944
951
  }
945
952
 
953
+ BackendAssistant.prototype.getHeaderUrl = function (headers) {
954
+ const self = this;
955
+ headers = headers || {};
956
+
957
+ return (
958
+ // Origin header (most reliable for CORS requests)
959
+ headers['origin']
960
+
961
+ // Fallback to referrer/referer
962
+ || headers['referrer']
963
+ || headers['referer']
964
+
965
+ // Reconstruct from host and path if available
966
+ || (headers['host'] ? `https://${headers['host']}${self.ref.req?.originalUrl || self.ref.req?.url || ''}` : '')
967
+
968
+ // If unsure, return empty string
969
+ || ''
970
+ )
971
+ .trim();
972
+ }
973
+
946
974
  /**
947
975
  * Parses a 'multipart/form-data' upload request
948
976
  *