Skip to content
This repository was archived by the owner on Sep 26, 2025. It is now read-only.

Commit 44f072f

Browse files
author
Johan Eliasson
authored
Merge pull request #72 from nhost/feat/link-env
Feat/link env
2 parents 4d74f8a + cb33cd3 commit 44f072f

9 files changed

Lines changed: 282 additions & 4554 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ npm install -g nhost
2828
To quickly get started, run the following commands:
2929

3030
```
31-
nhost login # Login
32-
cd <PROJECT> # Change directory to your project
33-
nhost init # Only run once to initiate project locally
34-
nhost dev # Start local Nhost backend
31+
nhost login # Login
32+
cd <PROJECT> # Change directory to your project
33+
nhost init # Only run once to initiate project locally
34+
nhost dev # Start local Nhost backend
35+
nhost link # Link with existing Nhost project
36+
nhost env [ls, pull] # List or pull environment variables from Nhost
3537
```
3638

3739
## Dependencies

package-lock.json

Lines changed: 0 additions & 4531 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
"@oclif/plugin-help": "^2.2.3",
1414
"@oclif/plugin-warn-if-update-available": "^1.7.0",
1515
"chalk": "^4.1.0",
16+
"cli-ux": "^5.5.1",
1617
"detect-port": "^1.3.0",
1718
"email-prompt": "^0.3.2",
1819
"email-validator": "^2.0.4",
1920
"inquirer": "^7.3.2",
2021
"js-yaml": "^3.13.1",
21-
"load-json-file": "^6.2.0",
2222
"node-fetch": "^2.6.0",
2323
"nunjucks": "^3.2.1",
2424
"ora": "^4.0.4",

src/commands/env/ls.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const { Command } = require("@oclif/command");
2+
const yaml = require("js-yaml");
3+
const util = require("util");
4+
const fs = require("fs");
5+
const { cli } = require("cli-ux");
6+
const readFile = util.promisify(fs.readFile);
7+
const { validateAuth } = require("../../util/login");
8+
9+
class DownCommand extends Command {
10+
async run() {
11+
const projectConfig = yaml.safeLoad(
12+
await readFile(`.nhost/nhost.yaml`, { encoding: "utf8" })
13+
);
14+
const projectId = projectConfig.project_id;
15+
16+
// get env vars from remote
17+
const userData = await validateAuth();
18+
19+
const projects = [
20+
...userData.user.projects,
21+
...userData.user.teams.flatMap(({ team }) => team.projects),
22+
];
23+
24+
const project = projects.find((project) => project.id === projectId);
25+
console.log(`Environment variables for ${project.name}.\n`);
26+
27+
cli.table(
28+
project.project_env_vars,
29+
{
30+
name: {
31+
header: "name",
32+
minWidth: 20,
33+
},
34+
dev_value: {
35+
header: "value (dev)",
36+
},
37+
},
38+
{
39+
printLine: this.log,
40+
}
41+
);
42+
}
43+
async catch(error) {
44+
console.log("");
45+
console.log(error);
46+
}
47+
}
48+
49+
DownCommand.description = `List environment variables`;
50+
51+
module.exports = DownCommand;

src/commands/env/pull.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const { Command } = require("@oclif/command");
2+
const chalk = require("chalk");
3+
const yaml = require("js-yaml");
4+
const util = require("util");
5+
const fs = require("fs");
6+
7+
const { validateAuth } = require("../../util/login");
8+
9+
class DownCommand extends Command {
10+
async run() {
11+
this.log(`Overwriting existing .env.development file`);
12+
const projectConfig = yaml.safeLoad(
13+
fs.readFileSync(`.nhost/nhost.yaml`, { encoding: "utf8" })
14+
);
15+
const projectId = projectConfig.project_id;
16+
17+
// get env vars from remote
18+
const userData = await validateAuth();
19+
20+
const projects = [
21+
...userData.user.projects,
22+
...userData.user.teams.flatMap(({ team }) => team.projects),
23+
];
24+
25+
const project = projects.find((project) => project.id === projectId);
26+
this.log(
27+
`Downloading development environment variables for project: ${project.name}`
28+
);
29+
30+
const envFile = `.env.development`;
31+
32+
var envFileContent = fs.readFileSync(envFile, { encoding: "utf8" });
33+
34+
const existingEnvVars = envFileContent
35+
.split("\n")
36+
.filter((row) => {
37+
return row;
38+
})
39+
.map((row) => {
40+
const [name, value] = row.split("=");
41+
return {
42+
name,
43+
value,
44+
};
45+
});
46+
47+
const remoteEnvVars = project.project_env_vars.map((v) => {
48+
return { name: v.name, value: v.dev_value };
49+
});
50+
51+
remoteEnvVars.push({
52+
name: "REGISTRATION_CUSTOM_FIELDS",
53+
value: project.hbp_REGISTRATION_CUSTOM_FIELDS,
54+
});
55+
56+
remoteEnvVars.push({
57+
name: "JWT_CUSTOM_FIELDS",
58+
value: project.backend_user_fields,
59+
});
60+
61+
remoteEnvVars.push({
62+
name: "DEFAULT_ALLOWED_USER_ROLES",
63+
value: project.hbp_DEFAULT_ALLOWED_USER_ROLES,
64+
});
65+
66+
remoteEnvVars.push({
67+
name: "ALLOWED_USER_ROLES",
68+
value: project.hbp_allowed_user_roles,
69+
});
70+
71+
const updatedProjectEnvVarIndexs = [];
72+
73+
// update env vars already in .env.development
74+
const envVars = existingEnvVars.map((existingEnvVar) => {
75+
const i = remoteEnvVars.findIndex(
76+
(pEnvVar) => pEnvVar.name == existingEnvVar.name
77+
);
78+
if (i === -1) {
79+
return existingEnvVar;
80+
}
81+
82+
const tmpEnvVar = remoteEnvVars[i];
83+
updatedProjectEnvVarIndexs.push(i);
84+
85+
const res = {
86+
name: existingEnvVar.name,
87+
value: tmpEnvVar.value,
88+
};
89+
return res;
90+
});
91+
92+
// add env vars not already in .env.development
93+
remoteEnvVars
94+
.filter((_, i) => {
95+
// filter if the env var was alrady updated
96+
return !updatedProjectEnvVarIndexs.includes(i);
97+
})
98+
.forEach((envVar) => {
99+
// add new env var
100+
envVars.push({
101+
name: envVar.name,
102+
value: envVar.value,
103+
});
104+
});
105+
106+
fs.writeFileSync(
107+
envFile,
108+
envVars.map((envVar) => `${envVar.name}=${envVar.value}`).join("\n"),
109+
{ flag: "w" }
110+
);
111+
112+
this.log(`${chalk.white("✅ .env.development file updated")}`);
113+
}
114+
}
115+
116+
DownCommand.description = `Sync remote environment variables to your local environment`;
117+
118+
module.exports = DownCommand;

src/commands/init.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ class InitCommand extends Command {
151151
(project) => project.id === selectedProjectId
152152
);
153153

154+
const remoteHasuraVersion = project.hasura_gqe_version;
155+
const dockerImage = `nhost/hasura-cli-docker:${remoteHasuraVersion}`;
156+
154157
// create root nhost folder
155158
await mkdir(nhostDir);
156159
// .nhost is used for nhost specific configuration
@@ -198,7 +201,7 @@ class InitCommand extends Command {
198201

199202
try {
200203
// clear current migration information from remote
201-
const qres = await fetch(`${hasuraEndpoint}/v1/query`, {
204+
await fetch(`${hasuraEndpoint}/v1/query`, {
202205
method: "POST",
203206
headers: {
204207
"Content-Type": "application/json",
@@ -216,20 +219,20 @@ class InitCommand extends Command {
216219

217220
// create migrations from remote
218221
spinner.text = "Create migrations";
219-
let command = `hasura migrate create "init" --from-server --schema "public" --schema "auth" ${commonOptions}`;
222+
let command = `docker run --rm -v $(pwd):/hasuracli ${dockerImage} migrate create "init" --from-server --schema "public" --schema "auth" ${commonOptions}`;
220223
await exec(command, { cwd: nhostDir });
221224

222-
// mark this migration as applied (--skip-execution) on the remote server
223-
// so that it doesn't get run again when promoting local
224-
// changes to that environment
225+
// // mark this migration as applied (--skip-execution) on the remote server
226+
// // so that it doesn't get run again when promoting local
227+
// // changes to that environment
225228
const initMigration = fs.readdirSync(migrationDirectory)[0];
226229
const version = initMigration.match(/^\d+/)[0];
227-
command = `hasura migrate apply --version "${version}" --skip-execution ${commonOptions}`;
230+
command = `docker run --rm -v $(pwd):/hasuracli nhost/hasura-cli-docker migrate apply --version "${version}" --skip-execution ${commonOptions}`;
228231
await exec(command, { cwd: nhostDir });
229232

230233
// create metadata from remote
231234
spinner.text = "Create Hasura metadata";
232-
command = `hasura metadata export ${commonOptions}`;
235+
command = `docker run --rm -v $(pwd):/hasuracli ${dockerImage} metadata export ${commonOptions}`;
233236
await exec(command, { cwd: nhostDir });
234237

235238
// auth.roles and auth.providers plus any enum compatible tables that might exist
@@ -255,7 +258,7 @@ class InitCommand extends Command {
255258
""
256259
);
257260
if (fromTables) {
258-
command = `hasura seeds create enum ${fromTables} ${commonOptions}`;
261+
command = `docker run --rm -v $(pwd):/hasuracli ${dockerImage} seeds create roles_and_providers ${fromTables} ${commonOptions}`;
259262
await exec(command, { cwd: nhostDir });
260263
}
261264

@@ -291,22 +294,23 @@ class InitCommand extends Command {
291294

292295
// write ENV variables to .env.development
293296
spinner.text = "Adding env vars to .env.development";
294-
await this._writeToFileSync(
297+
await writeFile(
295298
envFile,
296299
project.project_env_vars
297300
.map((envVar) => `${envVar.name}=${envVar.dev_value}`)
298301
.join("\n")
299302
);
300303

301-
await this._writeToFileSync(
304+
await writeFile(
302305
envFile,
303306
`\nREGISTRATION_CUSTOM_FIELDS=${project.hbp_REGISTRATION_CUSTOM_FIELDS}\n`
304307
);
305308

306309
if (project.backend_user_fields) {
307310
await this._writeToFileSync(
308311
envFile,
309-
`\JWT_CUSTOM_FIELDS=${project.backend_user_fields}\n`
312+
`JWT_CUSTOM_FIELDS=${project.backend_user_fields}\n`,
313+
{ flag: "a" }
310314
);
311315
}
312316

src/commands/link.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
const { Command } = require("@oclif/command");
2+
const fs = require("fs");
3+
const chalk = require("chalk");
4+
const util = require("util");
5+
const mkdir = util.promisify(fs.mkdir);
6+
const exists = util.promisify(fs.exists);
7+
const writeFile = util.promisify(fs.writeFile);
8+
9+
const selectProject = require("../util/projects");
10+
const { readAuthFile, getCustomApiEndpoint } = require("../util/config");
11+
const { validateAuth } = require("../util/login");
12+
13+
class LinkCommand extends Command {
14+
async run() {
15+
const workingDir = ".";
16+
const dotNhost = `${workingDir}/.nhost`;
17+
const apiUrl = getCustomApiEndpoint();
18+
const auth = readAuthFile();
19+
let userData;
20+
try {
21+
userData = await validateAuth(apiUrl, auth);
22+
} catch (err) {
23+
return this.log(`${chalk.red("Error!")} ${err.message}`);
24+
}
25+
26+
// personal and team projects
27+
const projects = [
28+
...userData.user.projects,
29+
...userData.user.teams.flatMap(({ team }) => team.projects),
30+
];
31+
32+
if (projects.length === 0) {
33+
return this.log(
34+
`\nWe couldn't find any projects related to this account, go to ${chalk.bold.underline(
35+
"https://console.nhost.io/new"
36+
)} and create one`
37+
);
38+
}
39+
40+
let selectedProjectId;
41+
try {
42+
selectedProjectId = await selectProject(projects);
43+
} catch (err) {
44+
return this.log(`${chalk.red("Error!")} ${err.message}`);
45+
}
46+
47+
const project = projects.find(
48+
(project) => project.id === selectedProjectId
49+
);
50+
51+
// .nhost is used for nhost specific configuration
52+
if (!(await exists(dotNhost))) {
53+
await mkdir(dotNhost);
54+
}
55+
await writeFile(
56+
`${dotNhost}/nhost.yaml`,
57+
`project_id: ${selectedProjectId}`
58+
);
59+
60+
console.log(`Project linked: ${project.name}`);
61+
}
62+
}
63+
64+
LinkCommand.description = `Link Nhost Project 2
65+
...
66+
Link Nhost project
67+
`;
68+
69+
module.exports = LinkCommand;

src/util/config.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
const fs = require("fs");
2+
const chalk = require("chalk");
23
const util = require("util");
34
const exists = util.promisify(fs.exists);
45
const unlink = util.promisify(fs.unlink);
56
const path = require("path");
67
const { homedir } = require("os");
78
const writeJSON = require("write-json-file");
8-
const loadJSON = require("load-json-file");
99

1010
const NHOST_DIR = path.join(homedir(), ".nhost");
1111
const authPath = path.join(NHOST_DIR, "auth.json");
@@ -66,6 +66,8 @@ async function writeAuthFile(data) {
6666
mode: 0o600,
6767
});
6868
} catch (err) {
69+
console.log(chalk.bold.red("Error!"));
70+
console.log("Could not read auth file. Run `nhost login` first.");
6971
throw err;
7072
}
7173
}
@@ -79,7 +81,13 @@ async function deleteAuthFile() {
7981
}
8082

8183
function readAuthFile() {
82-
return loadJSON.sync(authPath);
84+
try {
85+
return require(authPath);
86+
} catch (error) {
87+
console.log(chalk.bold.red("Error!"));
88+
console.log("Could not read auth file. Run `nhost login` first.");
89+
throw error;
90+
}
8391
}
8492

8593
async function authFileExists() {

0 commit comments

Comments
 (0)