Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat multiple accounts and instances #851

Merged
merged 74 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
d9fa0bb
feat(cli): Multiple accounts in CLI
byawitz May 21, 2024
6eeef88
Merge branch 'refs/heads/feat-cli-whoami-command' into feat-multiple-…
byawitz May 22, 2024
fd78fad
feat(cli): Hooking migration before any command
byawitz May 22, 2024
9387d95
feat(cli): Interactive multiple account changing
byawitz May 22, 2024
d250003
feat(cli): Adding json option to list accounts
byawitz May 22, 2024
144e043
feat(cli): Interactive multiple user logout
byawitz May 22, 2024
9a61aef
fix(cli): Changing migrate to regular function
byawitz May 22, 2024
0cf5edd
fix(cli): Login question
byawitz May 22, 2024
7c07523
fix(cli): Extracting migrate
byawitz May 22, 2024
58df0ee
fix(cli): Renaming the migrate function name
byawitz May 22, 2024
8c77004
fix(cli): Excluding migrate from tests
byawitz May 22, 2024
c1e5c41
fix(cli): Adding migrate to tests
byawitz May 22, 2024
ea494b7
fix(cli): Adapting the `client` command and bug when getting key.
byawitz May 22, 2024
8c7b894
refactor(cli): Multiple account login refactoring
byawitz May 24, 2024
32452ff
refactor(cli): Review fixing
byawitz May 28, 2024
0b7a25a
Merge branch 'feat-cli-g2' into feat-multiple-accounts-and-instances
Meldiron May 29, 2024
ab9e297
Merge branch 'refs/heads/feat-cli-g2' into feat-multiple-accounts-and…
byawitz May 29, 2024
02be0f0
feat(cli): Updating push to include the project
byawitz May 29, 2024
9ac316d
feat(cli): Updating push to include the project
byawitz May 29, 2024
e089e9c
feat(cli): Pull all resources
byawitz May 29, 2024
3d36787
Merge branch 'refs/heads/feat-pull-project-settings' into feat-push-p…
byawitz May 29, 2024
cf2ce7d
Merge branch 'refs/heads/feat-non-destructive-db-actions' into feat-m…
byawitz May 29, 2024
705647d
chore(cli): Adding headless login
byawitz May 29, 2024
2962472
Merge branch 'refs/heads/feat-multiple-accounts-and-instances' into f…
byawitz May 29, 2024
87e88c3
chore(cli): Adapting pull request
byawitz May 29, 2024
063ac9e
refactor(cli): Texts and removing unnecessary options
byawitz May 30, 2024
0135a4f
refactor(cli): removing unneeded async
byawitz May 30, 2024
dcea1df
feat(cli): Skipping unavailable push resources
byawitz May 30, 2024
f78b368
Merge pull request #865 from appwrite/feat-push-pull-all
christyjacob4 May 30, 2024
2e8f560
feat(cli): Showing deployed function URL
byawitz May 30, 2024
b35b4bb
Merge pull request #866 from appwrite/feat-function-deployment-url
christyjacob4 May 30, 2024
7efedaf
refactor(cli): Adding pagination and reorder questions
byawitz May 31, 2024
930685c
feat(cli): headless project setup
byawitz May 31, 2024
5bba9cd
feat(cli): creating project if not exist
byawitz May 31, 2024
3621c3c
refactor(cli): reorder questions
byawitz May 31, 2024
64c364e
Merge branch 'refs/heads/feat-multiple-accounts-and-instances' into f…
byawitz May 31, 2024
d36a623
wip
byawitz Jun 3, 2024
406ef22
feat: Init project only ID
byawitz Jun 7, 2024
fb8e08a
feat: Init to local only & database creation
byawitz Jun 7, 2024
9717830
refactor: Improve command line to use `rawArgs`
byawitz Jun 7, 2024
2fd8f2b
feat: Added list search
byawitz Jun 7, 2024
0b817a7
feat: Added Register
byawitz Jun 7, 2024
acef728
feat: Tip for detailed error
byawitz Jun 7, 2024
990557a
feat: clearer function summary
byawitz Jun 7, 2024
33a5680
feat: Adding error when enable to open with default browser
byawitz Jun 7, 2024
471fc85
feat: Adding endpoint to whoami
byawitz Jun 7, 2024
f24abb6
feat: Adding self-hosted to project init
byawitz Jun 7, 2024
85167e9
feat: Removing unusable accounts
byawitz Jun 7, 2024
069f70c
feat: Ask when deleting, and explanations
byawitz Jun 7, 2024
6d8b6f2
refactor: Make sure `min` and `max` are numbers
byawitz Jun 7, 2024
f64000e
refactor: Asking unanswered questions
byawitz Jun 7, 2024
c393058
Merge pull request #871 from appwrite/feat-search-list
christyjacob4 Jun 10, 2024
06b2a4b
Merge pull request #870 from appwrite/feat-improve-report
christyjacob4 Jun 10, 2024
034d08e
Update templates/cli/lib/questions.js.twig
christyjacob4 Jun 11, 2024
49488a6
Merge pull request #875 from appwrite/feat-collection-push-improvements
christyjacob4 Jun 11, 2024
469594c
Merge branch 'refs/heads/feat-arg-project-init' into feat-general-imp…
byawitz Jun 11, 2024
8420eb0
fix: reviews
byawitz Jun 11, 2024
cc78c01
Merge remote-tracking branch 'origin/feat-general-improvements' into …
byawitz Jun 11, 2024
4ae99d5
fix: reviews
byawitz Jun 11, 2024
b5ca8bf
fix: reviews
byawitz Jun 11, 2024
aabddae
fix: texts
byawitz Jun 11, 2024
dea75b4
refactor: Removing login from init
byawitz Jun 11, 2024
5fe0bd2
refactor: Removing self-hosted from login
byawitz Jun 11, 2024
2db03b5
Merge pull request #874 from appwrite/feat-general-improvements
christyjacob4 Jun 11, 2024
9116fc0
feat: Fetching function templates from API
byawitz Jun 11, 2024
7ed643e
Update templates/cli/lib/commands/init.js.twig
christyjacob4 Jun 11, 2024
57984ba
Merge pull request #877 from appwrite/feat-function-init-improvments
christyjacob4 Jun 11, 2024
0cf2882
refactor: Removing `--open` option
byawitz Jun 11, 2024
fcb2ddc
Merge pull request #868 from appwrite/feat-arg-project-init
christyjacob4 Jun 11, 2024
3b04fe8
Merge branch 'refs/heads/feat-console-flow' into feat-multiple-accoun…
byawitz Jun 11, 2024
3e054b0
Merge remote-tracking branch 'origin/feat-multiple-accounts-and-insta…
byawitz Jun 11, 2024
309d764
refactor: reviews
byawitz Jun 11, 2024
4641f61
refactor: removing redundant exports and questions.
byawitz Jun 11, 2024
0860867
refactor: renaming login to sessions
byawitz Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion templates/cli/base/requests/api.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
if (parseOutput) {
{%~ if methodHaveConsolePreview(method.name,service.name) %}
if(console) {
showConsoleLink('{{service.name}}', '{{ method.name }}',open
showConsoleLink('{{service.name}}', '{{ method.name }}'
{%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%}
);
} else {
Expand Down
6 changes: 6 additions & 0 deletions templates/cli/index.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ const chalk = require("chalk");
const { version } = require("./package.json");
const { commandDescriptions, cliConfig } = require("./lib/parser");
const { client } = require("./lib/commands/generic");
const inquirer = require("inquirer");
{% if sdk.test != "true" %}
const { login, logout, whoami } = require("./lib/commands/generic");
const { init } = require("./lib/commands/init");
const { pull } = require("./lib/commands/pull");
const { push } = require("./lib/commands/push");
{% else %}
const { migrate } = require("./lib/commands/generic");
{% endif %}
{% for service in spec.services %}
const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}");
{% endfor %}

inquirer.registerPrompt('search-list', require('inquirer-search-list'));

program
.description(commandDescriptions['main'])
.configureHelp({
Expand All @@ -29,6 +34,7 @@ program
.version(version, "-v, --version")
.option("--verbose", "Show complete error log")
.option("--json", "Output in JSON format")
.hook('preAction', migrate)
.option("-f,--force", "Flag to confirm all warnings")
.option("-a,--all", "Flag to push all resources")
.option("--id [id...]", "Flag to pass list of ids for a giving action")
Expand Down
11 changes: 10 additions & 1 deletion templates/cli/lib/client.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const { fetch, FormData, Agent } = require("undici");
const JSONbig = require("json-bigint")({ storeAsString: false });
const {{spec.title | caseUcfirst}}Exception = require("./exception.js");
const { globalConfig } = require("./config.js");
const chalk = require("chalk");

class Client {
CHUNK_SIZE = 5*1024*1024; // 5MB

constructor() {
this.endpoint = '{{spec.endpoint}}';
this.headers = {
Expand Down Expand Up @@ -144,6 +145,14 @@ class Client {
} catch (error) {
throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text);
}

if (path !== '/account' && json.code === 401 && json.type === 'user_more_factors_required') {
console.log(`${chalk.cyan.bold("ℹ Info")} ${chalk.cyan("Unusable account found, removing...")}`);

const current = globalConfig.getCurrentSession();
globalConfig.setCurrentSession('');
globalConfig.removeSession(current);
}
throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json);
}

Expand Down
3 changes: 1 addition & 2 deletions templates/cli/lib/commands/command.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({
{%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%}

{%- if method.type == 'location' -%}, destination{%- endif -%}
{% if methodHaveConsolePreview(method.name,service.name) %}, console, open{%- endif -%}
{% if methodHaveConsolePreview(method.name,service.name) %}, console{%- endif -%}
}) => {
{%~ endblock %}
let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} :
Expand All @@ -97,7 +97,6 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({
{% endif %}
{% if methodHaveConsolePreview(method.name,service.name) %}
.option(`--console`, `Get the resource console url`)
.option(`--open`, `Use with '--console' to open the using default browser`)
{% endif %}
{% endautoescape %}
.action(actionRunner({{ service.name | caseLower }}{{ method.name | caseUcfirst }}))
Expand Down
161 changes: 129 additions & 32 deletions templates/cli/lib/commands/generic.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,69 @@ const { Command } = require("commander");
const Client = require("../client");
const { sdkForConsole } = require("../sdks");
const { globalConfig, localConfig } = require("../config");
const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser");
const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser");
const ID = require("../id");
{% if sdk.test != "true" %}
const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions");
const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account");
const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions");
const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account");

const loginCommand = async () => {
const answers = await inquirer.prompt(questionsLogin)
const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1';

let client = await sdkForConsole(false);
const loginCommand = async ({ email, password, endpoint, mfa, code }) => {
const oldCurrent = globalConfig.getCurrentSession();
let configEndpoint = endpoint ?? DEFAULT_ENDPOINT;

await accountCreateEmailPasswordSession({
email: answers.email,
password: answers.password,
parseOutput: false,
sdk: client
})
const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin);

if (answers.method === 'select') {
const accountId = answers.accountId;

if (!globalConfig.getSessionIds().includes(accountId)) {
throw Error('Session ID not found');
}

globalConfig.setCurrentSession(accountId);
success(`Current account is ${accountId}`);

return;
}

const id = ID.unique();

client.setCookie(globalConfig.getCookie());
globalConfig.addSession(id, {});
globalConfig.setCurrentSession(id);
globalConfig.setEndpoint(configEndpoint);
globalConfig.setEmail(answers.email);

let client = await sdkForConsole(false);

let account;

try {
await accountCreateEmailPasswordSession({
email: answers.email,
password: answers.password,
parseOutput: false,
sdk: client
})

client.setCookie(globalConfig.getCookie());

account = await accountGet({
sdk: client,
parseOutput: false
});
} catch(error) {
} catch (error) {
if (error.response === 'user_more_factors_required') {
const { factor } = await inquirer.prompt(questionsListFactors);
const { factor } = mfa ? { factor: mfa } : await inquirer.prompt(questionsListFactors);

const challenge = await accountCreateMfaChallenge({
factor,
parseOutput: false,
sdk: client
});

const { otp } = await inquirer.prompt(questionsMfaChallenge);
const { otp } = code ? { otp: code } : await inquirer.prompt(questionsMfaChallenge);

await accountUpdateMfaChallenge({
challengeId: challenge.$id,
Expand All @@ -53,6 +79,8 @@ const loginCommand = async () => {
parseOutput: false
});
} else {
globalConfig.removeSession(id);
globalConfig.setCurrentSession(oldCurrent);
throw error;
}
}
Expand Down Expand Up @@ -88,7 +116,8 @@ const whoami = new Command("whoami")
'ID': account.$id,
'Name': account.name,
'Email': account.email,
'MFA enabled': account.mfa ? 'Yes' : 'No'
'MFA enabled': account.mfa ? 'Yes' : 'No',
'Endpoint': globalConfig.getEndpoint()
}
];
if (json) {
Expand All @@ -100,20 +129,20 @@ const whoami = new Command("whoami")
drawTable(data)
}));


const login = new Command("login")
.description(commandDescriptions['login'])
.option(`--email [email]`, `User email`)
.option(`--password [password]`, `User password`)
.option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`)
.option(`--mfa [factor]`, `Multi-factor authentication login factor: totp, email, phone or recoveryCode`)
.option(`--code [code]`, `Multi-factor code`)
.configureHelp({
helpWidth: process.stdout.columns || 80
})
.action(actionRunner(loginCommand));

const logout = new Command("logout")
.description(commandDescriptions['logout'])
.configureHelp({
helpWidth: process.stdout.columns || 80
})
.action(actionRunner(async () => {
const deleteSession = async (accountId) => {
try {
let client = await sdkForConsole();

await accountDeleteSession({
Expand All @@ -122,8 +151,51 @@ const logout = new Command("logout")
sdk: client
})

globalConfig.setCookie("");
success()
globalConfig.removeSession(accountId);
} catch (e) {
error('Unable to log out, removing locally saved session information')
}
globalConfig.removeSession(accountId);
}

const logout = new Command("logout")
.description(commandDescriptions['logout'])
.configureHelp({
helpWidth: process.stdout.columns || 80
})
.action(actionRunner(async () => {
const sessions = globalConfig.getSessions();
const current = globalConfig.getCurrentSession();

if (current === '') {
return;
}
if (sessions.length === 1) {
await deleteSession(current);
success();

return;
}

const answers = await inquirer.prompt(questionsLogout);

if (answers.accounts) {
for (let accountId of answers.accounts) {
globalConfig.setCurrentSession(accountId);
await deleteSession(accountId);
}
}

const leftSessions = globalConfig.getSessions();

if (leftSessions.length > 0 && leftSessions.filter(session => session.id === current).length !== 1) {
const accountId = leftSessions[0].id;
globalConfig.setCurrentSession(accountId);

success(`Current account is ${accountId}`);
}

success();
}));
{% endif %}

Expand Down Expand Up @@ -156,6 +228,7 @@ const client = new Command("client")

if (endpoint !== undefined) {
try {
const id = ID.unique();
let url = new URL(endpoint);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error();
Expand All @@ -170,7 +243,8 @@ const client = new Command("client")
if (!response.version) {
throw new Error();
}

globalConfig.setCurrentSession(id);
globalConfig.addSession(id, {});
globalConfig.setEndpoint(endpoint);
} catch (_) {
throw new Error("Invalid endpoint or your Appwrite server is not running as expected.");
Expand All @@ -190,22 +264,45 @@ const client = new Command("client")
}

if (reset !== undefined) {
globalConfig.setEndpoint("");
globalConfig.setKey("");
globalConfig.setCookie("");
globalConfig.setSelfSigned("");
localConfig.setProject("", "");
const sessions = globalConfig.getSessions();

for (let accountId of sessions.map(session => session.id)) {
globalConfig.setCurrentSession(accountId);
await deleteSession(accountId);
}
}

success()
}));

const migrate = async () => {
if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) {
return;
}

const endpoint = globalConfig.get('endpoint');
const cookie = globalConfig.get('cookie');

const id = ID.unique();
const data = {
endpoint,
cookie,
email: 'legacy'
};

globalConfig.addSession(id, data);
globalConfig.setCurrentSession(id);
globalConfig.delete('endpoint');
globalConfig.delete('cookie');

}
module.exports = {
{% if sdk.test != "true" %}
loginCommand,
whoami,
login,
logout,
{% endif %}
migrate,
client
};
Loading
Loading