diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts new file mode 100644 index 00000000000..3fe7808ecea --- /dev/null +++ b/src/commands/api-types.d.ts @@ -0,0 +1,213 @@ +import type { NetlifyAPI } from 'netlify' + +import { DeployContext } from './types.d.ts' + +type ApiContext = DeployContext | 'branch' + +interface UpdatedBy { + id: string; + full_name: string; + email: string; + avatar_url: string; +} + +export type NarrowedEnvVarValue = Pick + +interface EnvVarValue { + value: string, + context: ApiContext, + context_parameter?: string, + id?: string, +} + +export interface EnvVar { + key: string; + scopes: Scope[]; + values: EnvVarValue[]; + is_secret?: boolean; + updated_at?: string; + updated_by?: UpdatedBy; +} + +interface GetEnvParams { + accountId: string, + siteId?: string, + context?: DeployContext, + scope?: EnvironmentVariableScope +} + +interface DeleteEnvVarValueParams { + accountId: string, + key: string, + id?: string, + siteId?: string +} + +interface SetEnvVarValueBody { + context: string, + value: string, + contextParameter?: string, +} + +interface SetEnvVarValueParams { + accountId: string, + key: string, + siteId?: string, + body: SetEnvVarValueBody +} + +interface UpdateEnvVarBody { + key: string, + scopes: string[], + values: EnvVar[] + is_secret: boolean +} + +interface UpdateEnvVarParams { + accountId: string, + key: string, + siteId?: string + body: EnvVar +} + +interface CreateEnvVarParams { + accountId: string, + key?: string, + siteId?: string, + body: EnvVar[] +} + +interface SiteInfo { + id: string; + state: string; + plan: string; + name: string; + custom_domain: string | null; + domain_aliases: string[]; + branch_deploy_custom_domain: string | null; + deploy_preview_custom_domain: string | null; + password: string | null; + notification_email: string | null; + url: string; + ssl_url: string; + admin_url: string; + screenshot_url: string; + created_at: string; + updated_at: string; + user_id: string; + session_id: string; + ssl: boolean; + force_ssl: boolean | null; + managed_dns: boolean; + deploy_url: string; + published_deploy: PublishedDeploy; + account_id: string; + account_name: string; + account_slug: string; + git_provider?: string; + deploy_hook: string; + capabilities: Capabilities; + processing_settings: ProcessingSettings; + build_settings: BuildSettings; + id_domain: string; + default_hooks_data?: DefaultHooksData; + build_image: string; + prerender: string | null; + functions_region: string; + feature_flags: FeatureFlags; +} + +interface PublishedDeploy { + id: string; + site_id: string; + user_id: string; + build_id: string; + state: string; + name: string; + url: string; + ssl_url: string; + admin_url: string; + deploy_url: string; + deploy_ssl_url: string; + screenshot_url: string; + review_id: number | null; + draft: boolean; + required: string[]; + required_functions: string[]; + error_message: string; + branch: string; + commit_ref: string; + commit_url: string; + skipped: boolean | null; + created_at: string; + updated_at: string; + published_at: string; + title: string; + context: string; + locked: boolean | null; + review_url: string | null; + framework: string; + function_schedules: FunctionSchedule[] | []; +} + +interface FunctionSchedule { + name: string; + cron: string; +} + +interface Capabilities { + [key: string]: Record; +} + +// Processing Settings Interface +interface ProcessingSettings { + html: HTMLProcessingSettings; +} + +// HTML Processing Settings Interface +interface HTMLProcessingSettings { + pretty_urls: boolean; +} + +interface BuildSettings { + id: number; + provider: string; + deploy_key_id: string; + repo_path: string; + repo_branch: string; + dir: string; + functions_dir: string; + cmd: string; + allowed_branches: string[]; + public_repo: boolean; + private_logs: boolean; + repo_url: string; + env: EnvVariables; + installation_id: number; + stop_builds: boolean; +} + +interface EnvVariables { + [key: string]: string; +} + +interface DefaultHooksData { + access_token: string; +} + +interface GetSiteParams { + siteId?: string, + feature_flags?: string + site_id?: string +} + +export interface ExtendedNetlifyAPI extends NetlifyAPI { + getEnvVar(params: GetEnvVarParams): Promise + getEnvVars( params: GetEnvParams): Promise + deleteEnvVarValue( params: DeleteEnvVarValueParams ): Promise + setEnvVarValue( params: SetEnvVarValueParams): Promise + deleteEnvVar(params: DeleteEnvVarValueParams): Promise + updateEnvVar(params: UpdateEnvVarParams): Promise + createEnvVars(params: CreateEnvVarParams): Promise + getSite(params: GetSiteParams): Promise +} \ No newline at end of file diff --git a/src/commands/api/api.ts b/src/commands/api/api.ts index b594b1646dd..28c055fb344 100644 --- a/src/commands/api/api.ts +++ b/src/commands/api/api.ts @@ -40,6 +40,8 @@ export const apiCommand = async (apiMethod: string, options: OptionValues, comma const apiResponse = await api[apiMethod](payload) logJson(apiResponse) } catch (error_) { - error(error_) + if (error_ instanceof Error || typeof error_ === 'string') { + error(error_) + } } } diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index f8a7c469124..406d8a546b2 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -1,5 +1,3 @@ -import { isCI } from 'ci-info' - import { existsSync } from 'fs' import { join, relative, resolve } from 'path' import process from 'process' @@ -8,6 +6,7 @@ import { format } from 'util' import { DefaultLogger, Project } from '@netlify/build-info' import { NodeFS, NoopLogger } from '@netlify/build-info/node' import { resolveConfig } from '@netlify/config' +import { isCI } from 'ci-info' import { Command, Help, Option } from 'commander' // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'debu... Remove this comment to see the full error message import debug from 'debug' diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 1d06a515eed..0d841d62d04 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -1,10 +1,12 @@ -import { OptionValues } from 'commander' - import { chalk, log, error as logError } from '../../utils/command-helpers.js' +import { isAPIEnvError } from '../../utils/env/index.js' +import type { ExtendedNetlifyAPI } from '../api-types.d.ts' import BaseCommand from '../base-command.js' +import { $TSFixMe } from '../types.js' + +import { CloneEnvParams, EnvCloneOptions } from './types.js' -// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. -const safeGetSite = async (api, siteId) => { +const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { const data = await api.getSite({ siteId }) return { data } @@ -17,14 +19,12 @@ const safeGetSite = async (api, siteId) => { * Copies the env from a site configured with Envelope to a different site configured with Envelope * @returns {Promise} */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { +const cloneEnvVars = async ({ api, siteFrom, siteTo }: CloneEnvParams): Promise => { const [envelopeFrom, envelopeTo] = await Promise.all([ api.getEnvVars({ accountId: siteFrom.account_slug, siteId: siteFrom.id }), api.getEnvVars({ accountId: siteTo.account_slug, siteId: siteTo.id }), ]) - // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message const keysFrom = envelopeFrom.map(({ key }) => key) if (keysFrom.length === 0) { @@ -34,24 +34,21 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { const accountId = siteTo.account_slug const siteId = siteTo.id - // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key)) // delete marked env vars in parallel - // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message await Promise.all(envVarsToDelete.map(({ key }) => api.deleteEnvVar({ accountId, siteId, key }))) // hit create endpoint try { await api.createEnvVars({ accountId, siteId, body: envelopeFrom }) - } catch (error) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - throw error.json ? error.json.msg : error + } catch (error: unknown) { + if (isAPIEnvError(error)) throw error.json ? error.json.msg : error } return true } -export const envClone = async (options: OptionValues, command: BaseCommand) => { +export const envClone = async (options: EnvCloneOptions, command: BaseCommand) => { const { api, site } = command.netlify if (!site.id && !options.from) { @@ -61,8 +58,14 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => { return false } + const sourceId = options.from || site.id + + if (!sourceId) { + throw new Error('Site ID is required') + } + const siteId = { - from: options.from || site.id, + from: sourceId, to: options.to, } @@ -81,13 +84,15 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => { return false } - const success = await cloneEnvVars({ api, siteFrom, siteTo }) + if (siteFrom && siteTo) { + const success = await cloneEnvVars({ api, siteFrom, siteTo }) - if (!success) { - return false - } + if (!success) { + return false + } - log(`Successfully cloned environment variables from ${chalk.green(siteFrom.name)} to ${chalk.green(siteTo.name)}`) + log(`Successfully cloned environment variables from ${chalk.green(siteFrom.name)} to ${chalk.green(siteTo.name)}`) + } return true } diff --git a/src/commands/env/env-get.ts b/src/commands/env/env-get.ts index 901fc51cd8a..4adbbfae4c8 100644 --- a/src/commands/env/env-get.ts +++ b/src/commands/env/env-get.ts @@ -1,10 +1,10 @@ -import { OptionValues } from 'commander' - import { chalk, log, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, getEnvelopeEnv } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' -export const envGet = async (name: string, options: OptionValues, command: BaseCommand) => { +import { EnvOptions } from './types.js' + +export const envGet = async (name: string, options: EnvOptions, command: BaseCommand) => { const { context, scope } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id @@ -15,6 +15,7 @@ export const envGet = async (name: string, options: OptionValues, command: BaseC } const { siteInfo } = cachedConfig + const env = await getEnvelopeEnv({ api, context, env: cachedConfig.env, key: name, scope, siteInfo }) const { value } = env[name] || {} @@ -26,7 +27,7 @@ export const envGet = async (name: string, options: OptionValues, command: BaseC } if (!value) { - const contextType = AVAILABLE_CONTEXTS.includes(context) ? 'context' : 'branch' + const contextType = context === undefined ? 'branch' : AVAILABLE_CONTEXTS.includes(context) const withContext = `in the ${chalk.magenta(context)} ${contextType}` const withScope = scope === 'any' ? '' : ` and the ${chalk.magenta(scope)} scope` log(`No value set ${withContext}${withScope} for environment variable ${chalk.yellow(name)}`) diff --git a/src/commands/env/env-import.ts b/src/commands/env/env-import.ts index 9268580b914..68f13049b6d 100644 --- a/src/commands/env/env-import.ts +++ b/src/commands/env/env-import.ts @@ -1,55 +1,52 @@ import { readFile } from 'fs/promises' import AsciiTable from 'ascii-table' -import { OptionValues } from 'commander' import dotenv from 'dotenv' import { exit, log, logJson } from '../../utils/command-helpers.js' -import { translateFromEnvelopeToMongo, translateFromMongoToEnvelope } from '../../utils/env/index.js' +import { translateFromEnvelopeToMongo, translateFromMongoToEnvelope, isAPIEnvError } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import { EnvImportOptions, EnvOptions, ImportDotEnvParams } from './types.js' + /** * Saves the imported env in the Envelope service * @returns {Promise} */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const importDotEnv = async ({ api, importedEnv, options, siteInfo }) => { +const importDotEnv = async ({ api, importedEnv, options, siteInfo }: ImportDotEnvParams) => { // fetch env vars const accountId = siteInfo.account_slug const siteId = siteInfo.id const dotEnvKeys = Object.keys(importedEnv) const envelopeVariables = await api.getEnvVars({ accountId, siteId }) - // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message const envelopeKeys = envelopeVariables.map(({ key }) => key) // if user intends to replace all existing env vars // either replace; delete all existing env vars on the site // or, merge; delete only the existing env vars that would collide with new .env entries - // @ts-expect-error TS(7006) FIXME: Parameter 'key' implicitly has an 'any' type. const keysToDelete = options.replaceExisting ? envelopeKeys : envelopeKeys.filter((key) => dotEnvKeys.includes(key)) // delete marked env vars in parallel - // @ts-expect-error TS(7006) FIXME: Parameter 'key' implicitly has an 'any' type. await Promise.all(keysToDelete.map((key) => api.deleteEnvVar({ accountId, siteId, key }))) // hit create endpoint const body = translateFromMongoToEnvelope(importedEnv) try { await api.createEnvVars({ accountId, siteId, body }) - } catch (error) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - throw error.json ? error.json.msg : error + } catch (error: unknown) { + if (isAPIEnvError(error)) { + throw error.json ? error.json.msg : error + } } // return final env to aid in --json output (for testing) return { - // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message ...translateFromEnvelopeToMongo(envelopeVariables.filter(({ key }) => !keysToDelete.includes(key))), ...importedEnv, } } -export const envImport = async (fileName: string, options: OptionValues, command: BaseCommand) => { +export const envImport = async (fileName: string, options: EnvImportOptions, command: BaseCommand) => { const { api, cachedConfig, site } = command.netlify const siteId = site.id @@ -62,9 +59,10 @@ export const envImport = async (fileName: string, options: OptionValues, command try { const envFileContents = await readFile(fileName, 'utf-8') importedEnv = dotenv.parse(envFileContents) - } catch (error) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - log(error.message) + } catch (error: unknown) { + if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { + log(error.message) + } exit(1) } diff --git a/src/commands/env/env-list.ts b/src/commands/env/env-list.ts index 278cb639bf0..02d9ae72ef1 100644 --- a/src/commands/env/env-list.ts +++ b/src/commands/env/env-list.ts @@ -1,7 +1,6 @@ import ansiEscapes from 'ansi-escapes' import AsciiTable from 'ascii-table' import { isCI } from 'ci-info' -import { OptionValues } from 'commander' import inquirer from 'inquirer' import logUpdate from 'log-update' @@ -10,6 +9,8 @@ import { AVAILABLE_CONTEXTS, getEnvelopeEnv, getHumanReadableScopes } from '../. import BaseCommand from '../base-command.js' import { EnvironmentVariables } from '../types.js' +import { EnvListOptions } from './types.js' + const MASK_LENGTH = 50 const MASK = '*'.repeat(MASK_LENGTH) @@ -40,7 +41,7 @@ const getTable = ({ return table.toString() } -export const envList = async (options: OptionValues, command: BaseCommand) => { +export const envList = async (options: EnvListOptions, command: BaseCommand) => { const { context, scope } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id @@ -56,7 +57,6 @@ export const envList = async (options: OptionValues, command: BaseCommand) => { // filter out general sources environment = Object.fromEntries( Object.entries(environment).filter( - // @ts-expect-error TS(18046) - 'variable' is of type 'unknown' ([, variable]) => variable.sources[0] !== 'general' && variable.sources[0] !== 'internal', ), ) @@ -64,7 +64,6 @@ export const envList = async (options: OptionValues, command: BaseCommand) => { // Return json response for piping commands if (options.json) { const envDictionary = Object.fromEntries( - // @ts-expect-error TS(18046) - 'variable' is of type 'unknown' Object.entries(environment).map(([key, variable]) => [key, variable.value]), ) logJson(envDictionary) @@ -73,7 +72,6 @@ export const envList = async (options: OptionValues, command: BaseCommand) => { if (options.plain) { const plaintext = Object.entries(environment) - // @ts-expect-error TS(18046) - 'variable' is of type 'unknown' .map(([key, variable]) => `${key}=${variable.value}`) .join('\n') log(plaintext) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index daa45a2ca84..e33bc9a1007 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,20 +1,25 @@ -import { OptionValues } from 'commander' - import { chalk, error, log, logJson } from '../../utils/command-helpers.js' -import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js' +import { + AVAILABLE_CONTEXTS, + AVAILABLE_SCOPES, + translateFromEnvelopeToMongo, + isAPIEnvError, +} from '../../utils/env/index.js' +import type { NarrowedEnvVarValue } from '../api-types.js' import BaseCommand from '../base-command.js' +import type { SetInEnvelopeParams, EnvSetOptions } from './types.d.ts' + /** * Updates the env for a site configured with Envelope with a new key/value pair * @returns {Promise} */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value }) => { +// //@ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message +const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value }: SetInEnvelopeParams) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id // secret values may not be used in the post-processing scope - // @ts-expect-error TS(7006) FIXME: Parameter 'sco' implicitly has an 'any' type. if (secret && scope && scope.some((sco) => /post[-_]processing/.test(sco))) { error(`Secret values cannot be used within the post-processing scope.`) return false @@ -35,17 +40,14 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value if (secret) { // post_processing (aka post-processing) scope is not allowed with secrets - // @ts-expect-error TS(7006) FIXME: Parameter 'sco' implicitly has an 'any' type. scopes = scopes.filter((sco) => !/post[-_]processing/.test(sco)) } // if the passed context is unknown, it is actually a branch name - // @ts-expect-error TS(7006) FIXME: Parameter 'ctx' implicitly has an 'any' type. - let values = contexts.map((ctx) => + let values: NarrowedEnvVarValue[] = contexts.map((ctx) => AVAILABLE_CONTEXTS.includes(ctx) ? { context: ctx, value } : { context: 'branch', context_parameter: ctx, value }, ) - // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const existing = envelopeVariables.find((envVar) => envVar.key === key) const params = { accountId, siteId, key } @@ -67,22 +69,18 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } if (context) { // update individual value(s) - // @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type. await Promise.all(values.map((val) => api.setEnvVarValue({ ...params, body: val }))) } else { // otherwise update whole env var if (secret) { - // @ts-expect-error TS(7006) FIXME: Parameter 'sco' implicitly has an 'any' type. scopes = scopes.filter((sco) => !/post[-_]processing/.test(sco)) - // @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type. if (values.some((val) => val.context === 'all')) { log(`This secret's value will be empty in the dev context.`) log(`Run \`netlify env:set ${key} --context dev\` to set a new value for the dev context.`) values = AVAILABLE_CONTEXTS.filter((ctx) => ctx !== 'all').map((ctx) => ({ context: ctx, // empty out dev value so that secret is indeed secret - // @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type. - value: ctx === 'dev' ? '' : values.find((val) => val.context === 'all').value, + value: ctx === 'dev' ? '' : values.find((val) => val.context === 'all')?.value ?? '', })) } } @@ -95,19 +93,18 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value await api.createEnvVars({ ...params, body }) } } catch (error_) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - throw error_.json ? error_.json.msg : error_ + const errortoThrow = isAPIEnvError(error_) ? error_.json.msg : error_ + throw errortoThrow } const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev') return { ...env, - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message [key]: value || env[key], } } -export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => { +export const envSet = async (key: string, value: string, options: EnvSetOptions, command: BaseCommand) => { const { context, scope, secret } = options const { api, cachedConfig, site } = command.netlify diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 044a32f4942..8acb88508d5 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -1,17 +1,24 @@ -import { OptionValues } from 'commander' - import { chalk, log, logJson } from '../../utils/command-helpers.js' -import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' +import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo, isAPIEnvError } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import type { EnviromentVariables } from '../types.d.ts' + +import type { EnvUnsetOptions, UnsetInEnvelopeParams } from './types.d.ts' /** * Deletes a given key from the env of a site configured with Envelope * @returns {Promise} */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { + +const unsetInEnvelope = async ({ + api, + context, + key, + siteInfo, +}: UnsetInEnvelopeParams): Promise => { const accountId = siteInfo.account_slug const siteId = siteInfo.id + // fetch envelope env vars const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] @@ -19,7 +26,6 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev') // check if the given key exists - // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type. const variable = envelopeVariables.find((envVar) => envVar.key === key) if (!variable) { // if not, no need to call delete; return early @@ -30,12 +36,10 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { try { if (context) { // if context(s) are passed, delete the matching contexts / branches, and the `all` context - // @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type. const values = variable.values.filter((val) => [...contexts, 'all'].includes(val.context_parameter || val.context), ) if (values) { - // @ts-expect-error TS(7006) FIXME: Parameter 'value' implicitly has an 'any' type. await Promise.all(values.map((value) => api.deleteEnvVarValue({ ...params, id: value.id }))) // if this was the `all` context, we need to create 3 values in the other contexts if (values.length === 1 && values[0].context === 'all') { @@ -53,17 +57,16 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }) => { await api.deleteEnvVar({ accountId, siteId, key }) } } catch (error_) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - throw error_.json ? error_.json.msg : error_ + const errortoThrow = isAPIEnvError(error_) ? error_.json.msg : error_ + throw errortoThrow } - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message delete env[key] return env } -export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => { +export const envUnset = async (key: string, options: EnvUnsetOptions, command: BaseCommand) => { const { context } = options const { api, cachedConfig, site } = command.netlify const siteId = site.id diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 9016b4b3df2..7d69bf7f659 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -3,6 +3,15 @@ import { OptionValues, Option } from 'commander' import { normalizeContext } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import { + EnvCloneOptions, + EnvImportOptions, + EnvListOptions, + EnvOptions, + EnvSetOptions, + EnvUnsetOptions, +} from './types.js' + const env = (options: OptionValues, command: BaseCommand) => { command.help() } @@ -29,7 +38,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'netlify env:get MY_VAR --scope functions', ]) .description('Get resolved value of specified environment variable (includes netlify.toml)') - .action(async (name: string, options: OptionValues, command: BaseCommand) => { + .action(async (name: string, options: EnvOptions, command: BaseCommand) => { const { envGet } = await import('./env-get.js') await envGet(name, options, command) }) @@ -51,7 +60,7 @@ export const createEnvCommand = (program: BaseCommand) => { false, ) .description('Import and set environment variables from .env file') - .action(async (fileName: string, options: OptionValues, command: BaseCommand) => { + .action(async (fileName: string, options: EnvImportOptions, command: BaseCommand) => { const { envImport } = await import('./env-import.js') await envImport(fileName, options, command) }) @@ -79,7 +88,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'netlify env:list --plain', ]) .description('Lists resolved environment variables for site (includes netlify.toml)') - .action(async (options: OptionValues, command: BaseCommand) => { + .action(async (options: EnvListOptions, command: BaseCommand) => { const { envList } = await import('./env-list.js') await envList(options, command) }) @@ -92,8 +101,7 @@ export const createEnvCommand = (program: BaseCommand) => { '-c, --context ', 'Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts)', // spread over an array for variadic options - // @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. - (context, previous = []) => [...previous, normalizeContext(context)], + (context, previous: string[] = []) => [...previous, normalizeContext(context)], ) .addOption( new Option('-s, --scope ', 'Specify a scope (default: all scopes)').choices([ @@ -114,7 +122,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'netlify env:set VAR_NAME value --scope builds functions', 'netlify env:set VAR_NAME --secret # convert existing variable to secret', ]) - .action(async (key: string, value: string, options: OptionValues, command: BaseCommand) => { + .action(async (key: string, value: string, options: EnvSetOptions, command: BaseCommand) => { const { envSet } = await import('./env-set.js') await envSet(key, value, options, command) }) @@ -127,8 +135,7 @@ export const createEnvCommand = (program: BaseCommand) => { '-c, --context ', 'Specify a deploy context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev") (default: all contexts)', // spread over an array for variadic options - // @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. - (context, previous = []) => [...previous, normalizeContext(context)], + (context, previous: string[] = []) => [...previous, normalizeContext(context)], ) .addExamples([ 'netlify env:unset VAR_NAME # unset in all contexts', @@ -136,7 +143,7 @@ export const createEnvCommand = (program: BaseCommand) => { 'netlify env:unset VAR_NAME --context production deploy-preview', ]) .description('Unset an environment variable which removes it from the UI') - .action(async (key: string, options: OptionValues, command: BaseCommand) => { + .action(async (key: string, options: EnvUnsetOptions, command: BaseCommand) => { const { envUnset } = await import('./env-unset.js') await envUnset(key, options, command) }) @@ -148,7 +155,7 @@ export const createEnvCommand = (program: BaseCommand) => { .requiredOption('-t, --to ', 'Site ID (To)') .description(`Clone environment variables from one site to another`) .addExamples(['netlify env:clone --to ', 'netlify env:clone --to --from ']) - .action(async (options: OptionValues, command: BaseCommand) => { + .action(async (options: EnvCloneOptions, command: BaseCommand) => { const { envClone } = await import('./env-clone.js') await envClone(options, command) }) diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts new file mode 100644 index 00000000000..df605ad6b83 --- /dev/null +++ b/src/commands/env/types.d.ts @@ -0,0 +1,71 @@ +import { OptionValues } from 'commander' + +import type { EnvironmentVariableScope } from '../../types.d.ts' +import type { ExtendedNetlifyAPI, DeployContext, SiteInfo } from '../api-types.d.ts' +import { $TSFixMe } from '../types.js' + + + +export interface EnvOptions extends OptionValues { + context: DeployContext + scope: EnvironmentVariableScope + json: boolean + +} + +export interface EnvSetOptions extends EnvOptions { + secret: boolean + force: boolean +} + +export interface EnvUnsetOptions extends EnvOptions { + force: boolean +} + +export interface EnvCloneOptions extends OptionValues { + from: string, + to: string, + force: boolean +} + +export interface EnvListOptions extends EnvOptions { + plain: boolean +} + +export interface EnvImportOptions extends OptionValues { + replaceExisting: boolean + json: boolean +} + +export interface UnsetInEnvelopeParams { + api: ExtendedNetlifyAPI + context: DeployContext[] + key: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + siteInfo: any +} + + +export interface SetInEnvelopeParams extends UnsetInEnvelopeParams { + value: string + scope: EnvironmentVariableScope[] + secret: boolean +} + +export interface SafeGetSite { + api: ExtendedNetlifyAPI + siteId: string +} + +export interface CloneEnvParams { + api: ExtendedNetlifyAPI + siteFrom: Site + siteTo: Site +} + +export interface ImportDotEnvParams { + api: ExtendedNetlifyAPI + options: EnvImportOptions + importedEnv: $TSFixMe + siteInfo: SiteInfo +} diff --git a/src/commands/init/types.d.ts b/src/commands/init/types.d.ts new file mode 100644 index 00000000000..c279f0a4de4 --- /dev/null +++ b/src/commands/init/types.d.ts @@ -0,0 +1,7 @@ +import { StateConfig } from "../types.js"; +import { SiteInfo } from "../api-types.js"; + +export interface PersistStateParams { + siteInfo: SiteInfo, + state: StateConfig + } \ No newline at end of file diff --git a/src/commands/sites/sites-delete.ts b/src/commands/sites/sites-delete.ts index 512fab816ff..ce76205f296 100644 --- a/src/commands/sites/sites-delete.ts +++ b/src/commands/sites/sites-delete.ts @@ -31,7 +31,7 @@ export const sitesDelete = async (siteId: string, options: OptionValues, command const noForce = options.force !== true /* Verify the user wants to delete the site */ - if (noForce) { + if (noForce && siteData) { log(`${chalk.redBright('Warning')}: You are about to permanently delete "${chalk.bold(siteData.name)}"`) log(` Verify this siteID "${siteId}" supplied is correct and proceed.`) log(' To skip this prompt, pass a --force flag to the delete command') diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index dce9224fe7c..58bd4b852af 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -1,10 +1,11 @@ import type { NetlifyConfig } from "@netlify/build"; import type { NetlifyTOML } from '@netlify/build-info' -import type { NetlifyAPI } from 'netlify' import type { FrameworksAPIPaths } from "../utils/frameworks-api.ts"; import StateConfig from '../utils/state-config.js' +import type { ExtendedNetlifyAPI } from "./api-types.d.ts"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any type $TSFixMe = any; @@ -15,8 +16,12 @@ export type NetlifySite = { siteId?: string get id(): string | undefined set id(id: string): void + id: string } +export type DeployContext = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' +export type Scope = 'builds' | 'functions' | 'runtime' | 'post_processing' + type PatchedConfig = NetlifyTOML & Pick & { functionsDirectory?: string build: NetlifyTOML['build'] & { @@ -48,17 +53,21 @@ type HTMLInjection = { html: string } -type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' -type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' +export type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' +export type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' -export type EnvironmentVariables = Record +type EnviromentVariables = { + [key: string]: string +} + +export type EnvironmentVariables = Record /** * The netlify object inside each command with the state */ export type NetlifyOptions = { // poorly duck type the missing api functions - api: NetlifyAPI & Record Promise<$TSFixMe>> + api: ExtendedNetlifyAPI & Record Promise<$TSFixMe>> apiOpts: $TSFixMe repositoryRoot: string /** Absolute path of the netlify configuration file */ @@ -66,9 +75,9 @@ export type NetlifyOptions = { /** Relative path of the netlify configuration file */ relConfigFilePath: string site: NetlifySite - siteInfo: $TSFixMe + siteInfo: SiteInfo config: PatchedConfig - cachedConfig: Record & { env: EnvironmentVariables } + cachedConfig: Record & { env: EnvironmentVariables, siteInfo: SiteInfo } globalConfig: $TSFixMe state: StateConfig frameworksAPIPaths: FrameworksAPIPaths diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 6e2ed213327..40b92de3a91 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,8 +1,18 @@ -import { $TSFixMe } from '../../commands/types.js' +import { EnvVar, EnvVarValue, ExtendedNetlifyAPI } from '../../commands/api-types.d.js' +import type { + DeployContext, + EnviromentVariables, + Scope, + EnvironmentVariableSource, + EnvironmentVariableScope, +} from '../../commands/types.js' import { error } from '../command-helpers.js' +import { APIEnvError } from '../types.js' -export const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] -export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] +import { GetEnvelopeEnvParams, ProcessedEnvVars } from './types.js' + +export const AVAILABLE_CONTEXTS: DeployContext[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] +export const AVAILABLE_SCOPES: Scope[] = ['builds', 'functions', 'runtime', 'post_processing'] /** * @param {string|undefined} context - The deploy context or branch of the environment variable value @@ -53,8 +63,7 @@ export const findValueInValues = (values, context) => * @param {enum} source - The source of the environment variable * @returns {object} The dictionary of env vars that match the given source */ -// @ts-expect-error TS(7006) FIXME: Parameter 'env' implicitly has an 'any' type. -export const filterEnvBySource = (env, source) => +export const filterEnvBySource = (env: EnviromentVariables, source: EnvironmentVariableSource): ProcessedEnvVars => // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source)) @@ -66,10 +75,10 @@ const fetchEnvelopeItems = async function ({ siteId, }: { accountId: string - api: $TSFixMe + api: ExtendedNetlifyAPI key: string - siteId: string -}): Promise<$TSFixMe[]> { + siteId?: string +}): Promise { if (accountId === undefined) { return [] } @@ -119,10 +128,10 @@ export const formatEnvelopeData = ({ source, }: { context?: string - envelopeItems: $TSFixMe[] - scope?: string - source: string -}) => + envelopeItems: EnvVar[] + scope?: EnvironmentVariableScope | 'any' + source: EnvironmentVariableSource +}): ProcessedEnvVars => envelopeItems // filter by context .filter(({ values }) => Boolean(findValueInValues(values, context))) @@ -156,12 +165,18 @@ export const formatEnvelopeData = ({ * @param {object} siteInfo - The site object * @returns {object} An object of environment variables keys and their metadata */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw = false, scope = 'any', siteInfo }) => { +export const getEnvelopeEnv = async ({ + api, + context = 'dev', + env, + key = '', + raw = false, + scope = 'any', + siteInfo, +}: GetEnvelopeEnvParams): Promise => { const { account_slug: accountId, id: siteId } = siteInfo const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([ - // @ts-expect-error TS(2345) FIXME: Argument of type '{ api: any; accountId: any; key:... Remove this comment to see the full error message fetchEnvelopeItems({ api, accountId, key }), fetchEnvelopeItems({ api, accountId, key, siteId }), ]) @@ -174,7 +189,6 @@ export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw return entries.reduce( (obj, [envVarKey, metadata]) => ({ ...obj, - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. [envVarKey]: metadata.value, }), {}, @@ -233,14 +247,17 @@ export const getHumanReadableScopes = (scopes) => { * @param {object} env - The site's env as it exists in Mongo * @returns {Array} The array of Envelope env vars */ -export const translateFromMongoToEnvelope = (env = {}) => { + +export const translateFromMongoToEnvelope = (env = {}): EnvVar[] => { + const context: DeployContext = 'all' + const envVars = Object.entries(env).map(([key, value]) => ({ key, scopes: AVAILABLE_SCOPES, values: [ { - context: 'all', - value, + context, + value: String(value), }, ], })) @@ -254,19 +271,31 @@ export const translateFromMongoToEnvelope = (env = {}) => { * @param {string} context - The deploy context or branch of the environment variable * @returns {object} The env object as compatible with Mongo */ -export const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') => + +export const translateFromEnvelopeToMongo = (envVars: EnvVar[] = [], context = 'dev'): EnviromentVariables => envVars - // @ts-expect-error TS(2339) FIXME: Property 'key' does not exist on type 'never'. .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1)) .reduce((acc, cur) => { - // @ts-expect-error TS(2339) FIXME: Property 'values' does not exist on type 'never'. const envVar = cur.values.find((val) => [context, 'all'].includes(val.context_parameter || val.context)) if (envVar && envVar.value) { return { ...acc, - // @ts-expect-error TS(2339) FIXME: Property 'key' does not exist on type 'never'. [cur.key]: envVar.value, } } return acc }, {}) + +export const isAPIEnvError = (err: unknown): err is APIEnvError => + /** + * Checks if an error is an APIEnvError + * @param {unknown} err - The error to check + * @returns {err is APIEnvError} Whether the error is an APIEnvError + */ + err !== null && + typeof err === 'object' && + 'json' in err && + err.json !== null && + typeof err.json === 'object' && + 'msg' in err.json && + typeof err.json.msg === 'string' diff --git a/src/utils/env/types.d.ts b/src/utils/env/types.d.ts new file mode 100644 index 00000000000..24c5af87ae2 --- /dev/null +++ b/src/utils/env/types.d.ts @@ -0,0 +1,22 @@ +import { SiteInfo } from "../../commands/api-types.js" +import { DeployContext, EnvironmentVariableSource, EnvironmentVariableScope } from "../../commands/types.js" + +export interface GetEnvelopeEnvParams { + api: ExtendedNetlifyAPI, + context?: DeployContext, + env: EnvironmentVariables, + key?: string, + scope?: EnvironmentVariableScope | 'any' + raw?: boolean, + siteInfo: SiteInfo +} + +export type ProcessedEnvVars = { + [key: string]: { + context: string; + branch?: string; + scopes: EnvironmentVariableScope[]; + sources: EnvironmentVariableSource[]; + value: string; + }; +}; \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts index 3059c77ab79..0d1524929f0 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -191,3 +191,7 @@ export interface Template { sourceCodeUrl: string slug: string } + +export interface APIEnvError { + json: { msg: string } +}