From ce31dba53e4bc0dccf319e74560de6e87d96f5c9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 25 Jun 2026 16:09:26 +0530 Subject: [PATCH] chore: update Command Line SDK to 22.3.0 --- CHANGELOG.md | 10 + README.md | 4 +- install.ps1 | 4 +- install.sh | 2 +- lib/auth/login.ts | 14 +- lib/commands/config.ts | 6 +- lib/commands/init.ts | 15 +- lib/commands/push.ts | 415 +++++++++++++++++++++++++++---------- lib/config.ts | 8 +- lib/constants.ts | 2 +- lib/parser.ts | 36 +++- lib/sdks.ts | 7 +- lib/utils.ts | 54 ++++- package-lock.json | 4 +- package.json | 2 +- scoop/appwrite.config.json | 6 +- 16 files changed, 449 insertions(+), 140 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5443bd21..0164e8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 22.3.0 + +* Added: Regional cloud endpoints are now derived from your configured endpoint in `init` and `push` +* Added: `push` creates a default function proxy rule when an existing function is missing one +* Added: `push` aborts and asks you to re-run when tablesDB deletions change the config +* Fixed: Unauthorized sessions now retry an OAuth token refresh before logging out +* Fixed: Auth security policies `limit`, `sessionsLimit`, and `passwordHistory` now accept null for unlimited +* Updated: `push` applies service, protocol, auth method, and policy updates concurrently +* Updated: Verbose error logs now include `code`, `type`, and `response` details + ## 22.2.2 * Fixed: Release binaries now embed `@napi-rs/keyring` native bindings for all targets diff --git a/README.md b/README.md index 67905eb9..e19277a7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using ```sh $ appwrite -v -22.2.2 +22.3.0 ``` ### Install using prebuilt binaries @@ -83,7 +83,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc Once the installation completes, you can verify your install using ``` $ appwrite -v -22.2.2 +22.3.0 ``` ## Getting Started diff --git a/install.ps1 b/install.ps1 index fe8a6f41..3f79106d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,8 +13,8 @@ # You can use "View source" of this page to see the full script. # REPO -$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.2/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.2/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.3.0/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.3.0/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index 7f535eb6..ad5d1bf9 100644 --- a/install.sh +++ b/install.sh @@ -120,7 +120,7 @@ verifyMacOSCodeSignature() { downloadBinary() { echo "[2/5] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="22.2.2" + GITHUB_LATEST_VERSION="22.3.0" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" diff --git a/lib/auth/login.ts b/lib/auth/login.ts index f54604e7..621bbcb0 100644 --- a/lib/auth/login.ts +++ b/lib/auth/login.ts @@ -1,6 +1,6 @@ import inquirer from "inquirer"; import { Account, type Models } from "@appwrite.io/console"; -import { sdkForConsole } from "../sdks.js"; +import { getValidAccessToken, sdkForConsole } from "../sdks.js"; import { globalConfig, normalizeCloudConsoleEndpoint } from "../config.js"; import { EXECUTABLE_NAME, @@ -138,6 +138,18 @@ export const getCurrentAccount = async (): Promise => { return account; } catch (err) { if (isGuestUnauthorizedError(err)) { + try { + await getValidAccessToken(globalConfig.getEndpoint(), { + forceRefresh: true, + }); + const refreshedClient = await sdkForConsole(); + const refreshedAccount = await new Account(refreshedClient).get(); + globalConfig.setEmail(refreshedAccount.email); + return refreshedAccount; + } catch (_refreshError) { + // Fall through to local cleanup only after refresh recovery fails. + } + removeCurrentSession(); } return null; diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 2b4133ca..8b657e90 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -120,9 +120,9 @@ const SettingsSchema = z security: z .object({ duration: z.union([z.number(), z.bigint()]).optional(), - limit: z.union([z.number(), z.bigint()]).optional(), - sessionsLimit: z.union([z.number(), z.bigint()]).optional(), - passwordHistory: z.union([z.number(), z.bigint()]).optional(), + limit: z.union([z.number(), z.bigint()]).nullable().optional(), + sessionsLimit: z.union([z.number(), z.bigint()]).nullable().optional(), + passwordHistory: z.union([z.number(), z.bigint()]).nullable().optional(), passwordDictionary: z.boolean().optional(), personalDataCheck: z.boolean().optional(), sessionAlerts: z.boolean().optional(), diff --git a/lib/commands/init.ts b/lib/commands/init.ts index 5c174b1e..2c5ce0aa 100644 --- a/lib/commands/init.ts +++ b/lib/commands/init.ts @@ -123,6 +123,12 @@ const getExistingProjectSummary = async ( }; }; +const getRegionalCloudEndpoint = (region: string): string => { + const url = new URL(globalConfig.getEndpoint() || DEFAULT_ENDPOINT); + url.hostname = `${region}.${url.hostname}`; + return url.toString().replace(/\/$/, ""); +}; + const printInitProjectSuccess = (message: string): void => { console.log(`${chalk.green.bold("✓")} ${chalk.green(message)}`); }; @@ -359,7 +365,6 @@ const initProject = async ({ } localConfig.clear(); // Clear the config to avoid any conflicts - const url = new URL(DEFAULT_ENDPOINT); if (answers.start === "new") { let projectIdToCreate; @@ -394,9 +399,7 @@ const initProject = async ({ localConfig.setProject(response["$id"], response.name ?? ""); localConfig.setOrganizationId(answers.organization); if (answers.region) { - localConfig.setEndpoint( - `https://${answers.region}.${url.host}${url.pathname}`, - ); + localConfig.setEndpoint(getRegionalCloudEndpoint(answers.region)); } } else { let selectedProject; @@ -417,9 +420,7 @@ const initProject = async ({ localConfig.setOrganizationId(answers.organization); if (isCloud() && selectedProject.region) { - localConfig.setEndpoint( - `https://${selectedProject.region}.${url.host}${url.pathname}`, - ); + localConfig.setEndpoint(getRegionalCloudEndpoint(selectedProject.region)); } } diff --git a/lib/commands/push.ts b/lib/commands/push.ts index 36dec604..1821ec70 100644 --- a/lib/commands/push.ts +++ b/lib/commands/push.ts @@ -34,6 +34,7 @@ import { arrayEqualsUnordered, getFunctionDeploymentConsoleUrl, getSiteDeploymentConsoleUrl, + getCloudEndpointRegion, isCloudHostname, siteRequiresBuildCommand, } from "../utils.js"; @@ -72,6 +73,7 @@ import { commandDescriptions, drawTable, parseBool, + formatErrorForLog, } from "../parser.js"; import { getProxyService, @@ -679,6 +681,73 @@ interface PushCollectionState extends PushCollectionInput { isNewlyCreated?: boolean; } +const getPushErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + + if ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ) { + return error.message; + } + + return String(error); +}; + +const getPushResultErrors = (result: unknown): string[] => { + if (typeof result !== "object" || result === null) { + return []; + } + + if ( + "errors" in result && + Array.isArray(result.errors) && + result.errors.length > 0 + ) { + return result.errors + .map(getPushErrorMessage) + .filter((message) => message.trim() !== ""); + } + + if ( + "error" in result && + typeof result.error === "string" && + result.error.trim() !== "" + ) { + return [result.error]; + } + + return []; +}; + +const formatPushResourceErrors = (results: Record): string => { + const failed = Object.entries(results) + .map(([resourceGroup, result]) => ({ + resourceGroup, + messages: getPushResultErrors(result), + })) + .filter(({ messages }) => messages.length > 0); + + if (failed.length === 0) { + return "Failed to push resource groups."; + } + + return [ + `Failed to push ${failed.length} resource group${failed.length === 1 ? "" : "s"}:`, + ...failed.flatMap(({ resourceGroup, messages }) => [ + `- ${resourceGroup}:`, + ...messages.map((message) => ` - ${message}`), + ]), + ].join("\n"); +}; + +const nullablePolicyTotal = (value: number | bigint | null): number | null => + value === null || Number(value) === 0 ? null : Number(value); + const normalizeIgnoreRules = (value: unknown): string[] => { if (Array.isArray(value)) { return value.filter( @@ -735,6 +804,67 @@ export class Push { } } + private getFunctionRuleDomain(domain: string): string { + const region = getCloudEndpointRegion(this.projectClient.config.endpoint); + + if (!region) { + return domain; + } + + const parts = domain.split("."); + if (parts.length < 3) { + return domain; + } + + // TODO: Replace this fallback with a server-provided regional functions domain. + parts[0] = region; + return parts.join("."); + } + + private async createDefaultFunctionRule(functionId: string): Promise { + let domain = ""; + try { + const consoleService = await getConsoleService(this.consoleClient); + const variables = await consoleService.variables(); + const domains = variables["_APP_DOMAIN_FUNCTIONS"] + .split(",") + .map((value) => value.trim()) + .filter((value) => value.length > 0); + + if (domains.length === 0) { + throw new Error("_APP_DOMAIN_FUNCTIONS is not configured."); + } + + domain = ID.unique() + "." + this.getFunctionRuleDomain(domains[0]); + } catch (err) { + this.error("Error fetching console variables."); + throw err; + } + + try { + const proxyService = await getProxyService(this.projectClient); + this.log(`Creating function proxy rule for ${domain} ...`); + await proxyService.createFunctionRule(domain, functionId); + } catch (err) { + this.error("Error creating function rule."); + throw err; + } + } + + private async hasDefaultFunctionRule(functionId: string): Promise { + const proxyService = await getProxyService(this.projectClient); + const { rules } = await proxyService.listRules({ + queries: [ + Query.limit(1), + Query.equal("deploymentResourceType", "function"), + Query.equal("deploymentResourceId", functionId), + Query.equal("trigger", "manual"), + ], + }); + + return rules.length > 0; + } + /** * Log an error message (respects silent mode) */ @@ -818,7 +948,11 @@ export class Push { results.settings = { success: true }; } catch (e: any) { allErrors.push(e); - results.settings = { success: false, error: e.message }; + results.settings = { + success: false, + error: getPushErrorMessage(e), + errors: [e], + }; } } @@ -937,10 +1071,36 @@ export class Push { config.sites.length > 0 ) { try { + let siteOptions = options.siteOptions; + if (siteOptions?.code === undefined) { + let allowSitesCodePush: boolean | null = + cliConfig.force === true ? true : null; + if (allowSitesCodePush === null) { + const codeAnswer = await inquirer.prompt(questionsPushSitesCode); + allowSitesCodePush = codeAnswer.override; + } + + siteOptions = { + ...siteOptions, + code: allowSitesCodePush === true, + }; + } + + if (siteOptions.code === true && siteOptions.activate === undefined) { + siteOptions = { + ...siteOptions, + activate: + cliConfig.force === true + ? true + : (await inquirer.prompt(questionsPushSitesActivate)) + .activate, + }; + } + this.log("Pushing sites ..."); const result = await this.pushSites( config.sites, - options.siteOptions, + siteOptions, ); this.success( `Successfully pushed ${chalk.bold(result.successfullyPushed)} sites.`, @@ -965,6 +1125,13 @@ export class Push { config.tables.length > 0 ) { try { + const { resyncNeeded } = await checkAndApplyTablesDBChanges(); + if (resyncNeeded) { + throw new Error( + "Configuration changed after applying tablesDB deletions. Re-run the push command to continue.", + ); + } + this.log("Pushing tables ..."); const result = await this.pushTables(config.tables, { attempts: options.tableOptions?.attempts, @@ -1052,48 +1219,83 @@ export class Push { if (settings.services) { this.log("Applying service statuses ..."); - for (const [service, status] of Object.entries(settings.services)) { - await projectService.updateService({ - serviceId: service as ProjectServiceId, - enabled: status, - }); - } + await Promise.all( + Object.entries(settings.services).map(([service, status]) => + projectService.updateService({ + serviceId: service as ProjectServiceId, + enabled: status, + }), + ), + ); } if (settings.protocols) { this.log("Applying protocol statuses ..."); - for (const [protocol, status] of Object.entries(settings.protocols)) { - await projectService.updateProtocol({ - protocolId: protocol as ProjectProtocolId, - enabled: status, - }); - } + await Promise.all( + Object.entries(settings.protocols).map(([protocol, status]) => + projectService.updateProtocol({ + protocolId: protocol as ProjectProtocolId, + enabled: status, + }), + ), + ); } if (settings.auth) { if (settings.auth.security) { this.log("Applying auth security settings ..."); - await projectService.updateSessionDurationPolicy({ - duration: Number(settings.auth.security.duration), - }); - await projectService.updateUserLimitPolicy({ - total: Number(settings.auth.security.limit), - }); - await projectService.updateSessionLimitPolicy({ - total: Number(settings.auth.security.sessionsLimit), - }); - await projectService.updatePasswordDictionaryPolicy({ - enabled: settings.auth.security.passwordDictionary, - }); - await projectService.updatePasswordHistoryPolicy({ - total: Number(settings.auth.security.passwordHistory), - }); - await projectService.updatePasswordPersonalDataPolicy({ - enabled: settings.auth.security.personalDataCheck, - }); - await projectService.updateSessionAlertPolicy({ - enabled: settings.auth.security.sessionAlerts, - }); + const securityUpdates: Promise[] = []; + if (settings.auth.security.duration !== undefined) { + securityUpdates.push( + projectService.updateSessionDurationPolicy({ + duration: Number(settings.auth.security.duration), + }), + ); + } + if (settings.auth.security.limit !== undefined) { + securityUpdates.push( + projectService.updateUserLimitPolicy({ + total: nullablePolicyTotal(settings.auth.security.limit), + }), + ); + } + if (settings.auth.security.sessionsLimit !== undefined) { + securityUpdates.push( + projectService.updateSessionLimitPolicy({ + total: nullablePolicyTotal(settings.auth.security.sessionsLimit), + }), + ); + } + if (settings.auth.security.passwordDictionary !== undefined) { + securityUpdates.push( + projectService.updatePasswordDictionaryPolicy({ + enabled: settings.auth.security.passwordDictionary, + }), + ); + } + if (settings.auth.security.passwordHistory !== undefined) { + securityUpdates.push( + projectService.updatePasswordHistoryPolicy({ + total: nullablePolicyTotal(settings.auth.security.passwordHistory), + }), + ); + } + if (settings.auth.security.personalDataCheck !== undefined) { + securityUpdates.push( + projectService.updatePasswordPersonalDataPolicy({ + enabled: settings.auth.security.personalDataCheck, + }), + ); + } + if (settings.auth.security.sessionAlerts !== undefined) { + securityUpdates.push( + projectService.updateSessionAlertPolicy({ + enabled: settings.auth.security.sessionAlerts, + }), + ); + } + await Promise.all(securityUpdates); + if (settings.auth.security.mockNumbers !== undefined) { const remoteMockNumbers: Array<{ number: string; otp: string }> = []; const limit = 100; @@ -1120,6 +1322,7 @@ export class Push { mockNumber.otp, ]), ); + const mockNumberUpdates: Promise[] = []; for (const remoteMockNumber of remoteMockNumbers) { const desiredOtp = desiredMockNumbersByPhone.get( @@ -1127,39 +1330,48 @@ export class Push { ); if (desiredOtp === undefined) { - await projectService.deleteMockPhone({ - number: remoteMockNumber.number, - }); + mockNumberUpdates.push( + projectService.deleteMockPhone({ + number: remoteMockNumber.number, + }), + ); continue; } if (remoteMockNumber.otp !== desiredOtp) { - await projectService.updateMockPhone({ - number: remoteMockNumber.number, - otp: desiredOtp, - }); + mockNumberUpdates.push( + projectService.updateMockPhone({ + number: remoteMockNumber.number, + otp: desiredOtp, + }), + ); } desiredMockNumbersByPhone.delete(remoteMockNumber.number); } for (const [phone, otp] of desiredMockNumbersByPhone) { - await projectService.createMockPhone({ - number: phone, - otp, - }); + mockNumberUpdates.push( + projectService.createMockPhone({ + number: phone, + otp, + }), + ); } + await Promise.all(mockNumberUpdates); } } if (settings.auth.methods) { this.log("Applying auth methods statuses ..."); - for (const [method, status] of Object.entries(settings.auth.methods)) { - await projectService.updateAuthMethod({ - methodId: method as ProjectAuthMethodId, - enabled: status, - }); - } + await Promise.all( + Object.entries(settings.auth.methods).map(([method, status]) => + projectService.updateAuthMethod({ + methodId: method as ProjectAuthMethodId, + enabled: status, + }), + ), + ); } } } @@ -1524,26 +1736,7 @@ export class Push { deploymentRetention: func.deploymentRetention, }); - let domain = ""; - try { - const consoleService = await getConsoleService( - this.consoleClient, - ); - const variables = await consoleService.variables(); - domain = ID.unique() + "." + variables["_APP_DOMAIN_FUNCTIONS"]; - } catch (err) { - this.error("Error fetching console variables."); - throw err; - } - - try { - const proxyService = await getProxyService(this.projectClient); - await proxyService.createFunctionRule(domain, func.$id); - } catch (err) { - this.error("Error creating function rule."); - throw err; - } - + await this.createDefaultFunctionRule(func.$id); updaterRow.update({ status: "Created" }); } catch (e: any) { errors.push(e); @@ -1553,6 +1746,19 @@ export class Push { }); return; } + } else { + try { + if (!(await this.hasDefaultFunctionRule(func.$id))) { + await this.createDefaultFunctionRule(func.$id); + } + } catch (e: any) { + errors.push(e); + updaterRow.fail({ + errorMessage: + e.message ?? "General error occurs please try again", + }); + return; + } } if (withVariables) { @@ -2877,15 +3083,20 @@ export class Push { } async function createPushInstance( - options: { silent?: boolean; requiresConsoleAuth?: boolean } = { + options: { + silent?: boolean; + requiresConsoleAuth?: boolean; + organizationId?: string; + } = { silent: false, requiresConsoleAuth: false, }, ): Promise { - const { silent, requiresConsoleAuth } = options; + const { silent, requiresConsoleAuth, organizationId } = options; const projectClient = await sdkForProject(); const consoleClient = await sdkForConsole({ requiresAuth: requiresConsoleAuth, + organizationId, }); return new Push(projectClient, consoleClient, silent); @@ -2932,27 +3143,12 @@ const pushResources = async ({ } const sites = localConfig.getSites(); - let allowSitesCodePush: boolean | null = - cliConfig.force === true ? true : null; - let activateSitesDeployment: boolean | undefined = - cliConfig.force === true ? true : undefined; - if (sites.length > 0 && allowSitesCodePush === null) { - const codeAnswer = await inquirer.prompt(questionsPushSitesCode); - allowSitesCodePush = codeAnswer.override; - } - if ( - sites.length > 0 && - allowSitesCodePush === true && - activateSitesDeployment === undefined - ) { - const activateAnswer = await inquirer.prompt(questionsPushSitesActivate); - activateSitesDeployment = activateAnswer.activate; - } + const project = localConfig.getProject(); const pushInstance = await createPushInstance({ requiresConsoleAuth: true, + organizationId: project.organizationId, }); - const project = localConfig.getProject(); const config: ConfigType = { organizationId: project.organizationId, projectId: project.projectId ?? "", @@ -2977,7 +3173,7 @@ const pushResources = async ({ config, ); - await pushInstance.pushResources(config, { + const result = await pushInstance.pushResources(config, { all: cliConfig.all, skipDeprecated, functionOptions: { @@ -2987,12 +3183,14 @@ const pushResources = async ({ logs: functionOptions?.logs, }, siteOptions: { - code: allowSitesCodePush === true, - activate: activateSitesDeployment ?? true, withVariables: false, logs: siteOptions?.logs, }, }); + + if (result.errors.length > 0) { + throw new Error(formatPushResourceErrors(result.results)); + } } else { const actions: Record Promise> = { settings: pushSettings, @@ -3082,10 +3280,11 @@ const pushSettings = async (): Promise => { try { log("Pushing project settings ..."); + const config = localConfig.getProject(); const pushInstance = await createPushInstance({ requiresConsoleAuth: true, + organizationId: config.organizationId ?? resolvedOrganizationId, }); - const config = localConfig.getProject(); await pushInstance.pushSettings({ projectId: config.projectId, @@ -3194,7 +3393,10 @@ const pushSite = async ({ log("Pushing sites ..."); const pushStartTime = Date.now(); - const pushInstance = await createPushInstance(); + const pushInstance = await createPushInstance({ + requiresConsoleAuth: true, + organizationId: localConfig.getProject().organizationId, + }); const result = await pushInstance.pushSites( withResolvedResourcePaths("sites", sites), { @@ -3255,7 +3457,7 @@ const pushSite = async ({ if (cliConfig.verbose) { errors.forEach((e) => { - console.error(e); + console.error(formatErrorForLog(e)); }); } }; @@ -3355,7 +3557,10 @@ const pushFunction = async ({ log("Pushing functions ..."); const pushStartTime = Date.now(); - const pushInstance = await createPushInstance(); + const pushInstance = await createPushInstance({ + requiresConsoleAuth: true, + organizationId: localConfig.getProject().organizationId, + }); const result = await pushInstance.pushFunctions( withResolvedResourcePaths("functions", functions), { @@ -3416,7 +3621,7 @@ const pushFunction = async ({ if (cliConfig.verbose) { errors.forEach((e) => { - console.error(e); + console.error(formatErrorForLog(e)); }); } }; @@ -3586,7 +3791,7 @@ const pushTable = async ({ } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; @@ -3663,7 +3868,7 @@ const pushCollection = async (): Promise => { } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; @@ -3727,7 +3932,7 @@ const pushBucket = async (): Promise => { } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; @@ -3791,7 +3996,7 @@ const pushTeam = async (): Promise => { } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; @@ -3855,7 +4060,7 @@ const pushWebhook = async (): Promise => { } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; @@ -3919,7 +4124,7 @@ const pushMessagingTopic = async (): Promise => { } if (cliConfig.verbose) { - errors.forEach((e) => console.error(e)); + errors.forEach((e) => console.error(formatErrorForLog(e))); } }; diff --git a/lib/config.ts b/lib/config.ts index 51af9274..41669380 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -596,11 +596,9 @@ class Config { ): void { props = whitelistKeys(props, keysSet, nestedKeys); - if (!this.has(entityType)) { - (this.set as (key: string, value: Entity[]) => void)(entityType, []); - } - - const entities = this.get(entityType) as Entity[]; + const entities = this.has(entityType) + ? ([...((this.get(entityType) as Entity[]) ?? [])] as Entity[]) + : []; for (let i = 0; i < entities.length; i++) { if (entities[i]["$id"] == props["$id"]) { entities[i] = props; diff --git a/lib/constants.ts b/lib/constants.ts index 3a2833f5..0c8702ac 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,7 +1,7 @@ // SDK export const SDK_TITLE = 'Appwrite'; export const SDK_TITLE_LOWER = 'appwrite'; -export const SDK_VERSION = '22.2.2'; +export const SDK_VERSION = '22.3.0'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; diff --git a/lib/parser.ts b/lib/parser.ts index 063a0634..2a56f368 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -663,6 +663,38 @@ const printQueryErrorHint = (err: Error): void => { ); }; +const ERROR_DETAIL_KEYS = ["code", "type", "response"] as const; + +export const formatErrorForLog = (err: Error): string => { + const stack = err.stack || `${err.name}: ${err.message}`; + const detailLines = ERROR_DETAIL_KEYS.flatMap((key) => { + if (!Object.prototype.hasOwnProperty.call(err, key)) { + return []; + } + + const value = (err as unknown as Record)[key]; + let detail = "undefined"; + try { + detail = + typeof value === "string" + ? JSON.stringify(value) + : JSON.stringify(value) ?? String(value); + } catch { + detail = String(value); + } + + return [`${key}: ${detail}`]; + }); + + if (detailLines.length === 0) { + return stack; + } + + const [summary, ...frames] = stack.split("\n"); + + return [summary, ...detailLines, ...frames].join("\n"); +}; + export const parseError = (err: Error): void => { if (cliConfig.report) { void (async () => { @@ -712,12 +744,12 @@ export const parseError = (err: Error): void => { printQueryErrorHint(err); error("\n Stack Trace: \n"); - console.error(err); + console.error(formatErrorForLog(err)); process.exit(1); })(); } else { if (cliConfig.verbose) { - console.error(err); + console.error(formatErrorForLog(err)); printQueryErrorHint(err); } else { log("For detailed error pass the --verbose or --report flag"); diff --git a/lib/sdks.ts b/lib/sdks.ts index c233abeb..969cedb9 100644 --- a/lib/sdks.ts +++ b/lib/sdks.ts @@ -22,13 +22,18 @@ import { export const getValidAccessToken = async ( endpoint: string, + options: { forceRefresh?: boolean } = {}, ): Promise => { const accessToken = globalConfig.getAccessToken(); const tokenExpiry = globalConfig.getTokenExpiry(); const clientId = globalConfig.getClientId() || OAUTH2_CLIENT_ID; const currentSession = globalConfig.getCurrentSession(); - if (accessToken && tokenExpiry > Date.now() + 60_000) { + if ( + !options.forceRefresh && + accessToken && + tokenExpiry > Date.now() + 60_000 + ) { return accessToken; } diff --git a/lib/utils.ts b/lib/utils.ts index 9ad04d02..6ce4728a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -41,9 +41,13 @@ export const createSettingsObject = ( policyById.set(policy.$id, policy); } - const policyTotal = (id: ProjectPolicyId): number | undefined => { + const policyTotal = (id: ProjectPolicyId): number | null | undefined => { const policy = policyById.get(id); - return policy && "total" in policy ? policy.total : undefined; + if (!policy || !("total" in policy)) { + return undefined; + } + + return policy.total === 0 ? null : policy.total; }; const policyEnabled = (id: ProjectPolicyId): boolean | undefined => { @@ -387,6 +391,21 @@ export const isRegionalCloudEndpoint = (endpoint: string): boolean => { } }; +export const getCloudEndpointRegion = (endpoint: string): string | null => { + try { + const hostname = new URL(endpoint).hostname; + + if (!isCloudHostname(hostname) || hostname === "cloud.appwrite.io") { + return null; + } + + const region = hostname.split(".")[0]; + return CLOUD_REGION_CODES.has(region) ? region : null; + } catch (_error) { + return null; + } +}; + export const isLocalhostHostname = (hostname: string): boolean => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"; @@ -394,6 +413,32 @@ const isCloudEnvironmentHostname = (hostname: string): boolean => hostname.endsWith(".cloud.appwrite.io") && CLOUD_LOGIN_ENVIRONMENTS.has(hostname.split(".")[0]); +const getCloudConsoleHostname = (hostname: string): string | null => { + if (hostname === "cloud.appwrite.io") { + return hostname; + } + + const labels = hostname.split("."); + if (labels.length < 4 || labels.slice(-3).join(".") !== "cloud.appwrite.io") { + return null; + } + + if (CLOUD_REGION_CODES.has(labels[0])) { + const environment = labels[1]; + if (environment && CLOUD_LOGIN_ENVIRONMENTS.has(environment)) { + return `${environment}.cloud.appwrite.io`; + } + + return "cloud.appwrite.io"; + } + + if (CLOUD_LOGIN_ENVIRONMENTS.has(labels[0])) { + return hostname; + } + + return null; +}; + export const isCloudLoginEndpoint = (endpoint: string): boolean => { try { const hostname = new URL(endpoint).hostname; @@ -410,9 +455,10 @@ export const isCloudLoginEndpoint = (endpoint: string): boolean => { export const getConsoleBaseUrl = (endpoint: string): string => { try { const url = new URL(endpoint); + const consoleHostname = getCloudConsoleHostname(url.hostname); - if (isCloudHostname(url.hostname)) { - url.hostname = "cloud.appwrite.io"; + if (consoleHostname) { + url.hostname = consoleHostname; } url.pathname = url.pathname.replace(/\/v1\/?$/, ""); diff --git a/package-lock.json b/package-lock.json index 601b3f50..a9d87293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite-cli", - "version": "22.2.2", + "version": "22.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "22.2.2", + "version": "22.3.0", "license": "BSD-3-Clause", "dependencies": { "@appwrite.io/console": "^15.1.1", diff --git a/package.json b/package.json index 5506c55a..d9850de4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "22.2.2", + "version": "22.3.0", "license": "BSD-3-Clause", "main": "dist/index.cjs", "module": "dist/index.js", diff --git a/scoop/appwrite.config.json b/scoop/appwrite.config.json index 8352a9dc..1b3ad9c8 100644 --- a/scoop/appwrite.config.json +++ b/scoop/appwrite.config.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json", - "version": "22.2.2", + "version": "22.3.0", "description": "The Appwrite CLI is a command-line application that allows you to interact with Appwrite and perform server-side tasks using your terminal.", "homepage": "https://github.com/appwrite/sdk-for-cli", "license": "BSD-3-Clause", "architecture": { "64bit": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.2/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.3.0/appwrite-cli-win-x64.exe", "bin": [ [ "appwrite-cli-win-x64.exe", @@ -15,7 +15,7 @@ ] }, "arm64": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.2/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.3.0/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe",