From e7447a23bc3c19d762d5bb13da6e4f8f8c868b58 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 11:30:56 -0500 Subject: [PATCH 01/13] fix: add types to env commands Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/env/env-unset.ts | 12 +++---- src/commands/env/env.ts | 6 ++-- src/commands/env/types.d.ts | 9 ++++++ src/commands/types.d.ts | 59 +++++++++++++++++++++++++++++++++-- src/utils/env/index.ts | 8 ++--- 5 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 src/commands/env/types.d.ts diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 044a32f4942..10bf12b550f 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -4,22 +4,24 @@ import { chalk, log, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import type { UnsetInEnvelope } 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 }: UnsetInEnvelope) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id + console.log('siteId is type of', typeof siteId) // fetch envelope env vars const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] - + console.log('envVar', envelopeVariables) 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 +32,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') { diff --git a/src/commands/env/env.ts b/src/commands/env/env.ts index 9016b4b3df2..31a085b1c33 100644 --- a/src/commands/env/env.ts +++ b/src/commands/env/env.ts @@ -92,8 +92,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([ @@ -127,8 +126,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', diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts new file mode 100644 index 00000000000..13a215dc1cf --- /dev/null +++ b/src/commands/env/types.d.ts @@ -0,0 +1,9 @@ +import type { ExtendedNetlifyAPI, Context } from '../types.d.ts' + +export interface UnsetInEnvelope { + api: ExtendedNetlifyAPI + context: Context[] + key: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + siteInfo: any +} diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index dce9224fe7c..9ec650aea93 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -17,6 +17,8 @@ export type NetlifySite = { set id(id: string): void } +export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' + type PatchedConfig = NetlifyTOML & Pick & { functionsDirectory?: string build: NetlifyTOML['build'] & { @@ -51,14 +53,67 @@ type HTMLInjection = { type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' -export type EnvironmentVariables = Record +// Define the structure for the 'updated_by' field +interface UpdatedBy { + // Add specific properties here if known + // For now, we'll keep it generic + [key: string]: any; +} + +// Define the structure for each item in the array + +interface EnvVarValue { + id: string, + value: string, + context: string, + context_parameter: string +} + +interface EnvVar { + key: string; + scopes: string[]; + values: EnvVarValue[]; + is_secret: boolean; + updated_at: string; + updated_by: UpdatedBy; +} + +interface GetEnvParams { + accountId: string, + siteId?: string, + context?: Context, + scope?: EnvironmentVariableScope +} + +interface DeleteEnvVarValueParams { + accountId: string, + key: string, + id: string, + siteId?: string +} + +interface SetEnvVarValueParams { + accountId: string, + key: string, + siteId?: string +} + +interface DeleteEnvVarValueParams extends SetEnvVarValueParams{} + +interface ExtendedNetlifyAPI extends NetlifyAPI { + getEnvVars( params: GetEnvParams): Promise + deleteEnvVarValue( params: DeleteEnvVarValueParams ): Promise + setEnvVarValue( params: SetEnvVarValueParams): +} + +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 */ diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 6e2ed213327..68020a44a6b 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,7 +1,8 @@ import { $TSFixMe } from '../../commands/types.js' +import type { EnvVar, Context } from '../../commands/types.js' import { error } from '../command-helpers.js' -export const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] +export const AVAILABLE_CONTEXTS: Context[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] /** @@ -254,17 +255,14 @@ 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') => 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, } } From 76fab2b43dbb67e3207c2655038874344f6e28e0 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 14:33:52 -0500 Subject: [PATCH 02/13] feat: updated types for api and env Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/api-types.d.ts | 69 ++++++++++++++++++++++++++++++++++ src/commands/base-command.ts | 3 +- src/commands/env/env-clone.ts | 6 +-- src/commands/env/env-import.ts | 9 +++-- src/commands/env/env-set.ts | 33 ++++++++++------ src/commands/env/env-unset.ts | 24 ++++++++---- src/commands/env/types.d.ts | 13 ++++++- src/commands/types.d.ts | 57 +++------------------------- src/utils/env/index.ts | 24 ++++++++++-- src/utils/types.ts | 4 ++ 10 files changed, 156 insertions(+), 86 deletions(-) create mode 100644 src/commands/api-types.d.ts diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts new file mode 100644 index 00000000000..bf220ee4ab4 --- /dev/null +++ b/src/commands/api-types.d.ts @@ -0,0 +1,69 @@ +import type { NetlifyAPI } from 'netlify' + +// Define the structure for the 'updated_by' field +interface UpdatedBy { + // Add specific properties here if known + // For now, we'll keep it generic + [key: string]: any; +} + +// Define the structure for each item in the array + +interface EnvVarValue { + id: string, + value: string, + context: string, + context_parameter: string +} + +export interface EnvVar { + key: string; + scopes: string[]; + values: EnvVarValue[]; + is_secret: boolean; + updated_at?: string; + updated_by?: UpdatedBy; +} + +interface GetEnvParams { + accountId: string, + siteId?: string, + context?: Context, + 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 UpdateEnvVarParams { + accountId: string, + key: string, + siteId?: string + body: EnvVar +} + +export interface ExtendedNetlifyAPI extends NetlifyAPI { + getEnvVars( params: GetEnvParams): Promise + deleteEnvVarValue( params: DeleteEnvVarValueParams ): Promise + setEnvVarValue( params: SetEnvVarValueParams): Promise + deleteEnvVar(params: DeleteEnvVarValueParams): Promise + updateEnvVar(params: UpdateEnvVarParams): Promise +} \ No newline at end of file diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 592f464b755..a9071897730 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..2f429283566 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -1,6 +1,7 @@ import { OptionValues } from 'commander' import { chalk, log, error as logError } from '../../utils/command-helpers.js' +import { isAPIEnvError } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' // @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. @@ -43,9 +44,8 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }): Promise => { // 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 diff --git a/src/commands/env/env-import.ts b/src/commands/env/env-import.ts index 9268580b914..2625949794a 100644 --- a/src/commands/env/env-import.ts +++ b/src/commands/env/env-import.ts @@ -5,7 +5,7 @@ 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' /** @@ -36,9 +36,10 @@ const importDotEnv = async ({ api, importedEnv, options, siteInfo }) => { 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) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index daa45a2ca84..1ef5dd1efcc 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,20 +1,34 @@ 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, + APIEnvErrorMessage, +} from '../../utils/env/index.js' import BaseCommand from '../base-command.js' +import type { SetInEnvelopeParams } 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 +49,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) => 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,14 +78,11 @@ 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.`) @@ -95,8 +103,9 @@ 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_ + if (error_ instanceof APIEnvErrorMessage) { + throw error_.json ? error_.json.msg : error_ + } } const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev') diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 10bf12b550f..4e7015efb3a 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -1,24 +1,30 @@ 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 { UnsetInEnvelope } from './types.d.ts' +import type { UnsetInEnvelopeParams } from './types.d.ts' /** * Deletes a given key from the env of a site configured with Envelope * @returns {Promise} */ -const unsetInEnvelope = async ({ api, context, key, siteInfo }: UnsetInEnvelope) => { +const unsetInEnvelope = async ({ + api, + context, + key, + siteInfo, +}: UnsetInEnvelopeParams): Promise => { const accountId = siteInfo.account_slug const siteId = siteInfo.id console.log('siteId is type of', typeof siteId) // fetch envelope env vars const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] - console.log('envVar', envelopeVariables) + const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev') // check if the given key exists @@ -52,12 +58,14 @@ const unsetInEnvelope = async ({ api, context, key, siteInfo }: UnsetInEnvelope) // otherwise, if no context passed, delete the whole key 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_ + } catch (error_: unknown) { + if (isAPIEnvError(error_)) { + const errorMessage = error_.json ? error_.json.msg : error_ + throw errorMessage + } + throw error_ } - // @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 diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts index 13a215dc1cf..0253bfae766 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -1,9 +1,18 @@ -import type { ExtendedNetlifyAPI, Context } from '../types.d.ts' +import type { EnvironmentVariableScope } from '../../types.d.ts' +import type { ExtendedNetlifyAPI, Context } from '../apiTypes.d.ts' -export interface UnsetInEnvelope { + +export interface UnsetInEnvelopeParams { api: ExtendedNetlifyAPI context: Context[] key: string // eslint-disable-next-line @typescript-eslint/no-explicit-any siteInfo: any } + + +export interface SetInEnvelopeParams extends UnsetInEnvelopeParams { + value: string, + scope: EnvironmentVariableScope[], + secret: boolean, +} \ No newline at end of file diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index 9ec650aea93..6d089a7a891 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; @@ -19,6 +20,7 @@ export type NetlifySite = { export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' + type PatchedConfig = NetlifyTOML & Pick & { functionsDirectory?: string build: NetlifyTOML['build'] & { @@ -54,57 +56,8 @@ type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_proce type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' -// Define the structure for the 'updated_by' field -interface UpdatedBy { - // Add specific properties here if known - // For now, we'll keep it generic - [key: string]: any; -} - -// Define the structure for each item in the array - -interface EnvVarValue { - id: string, - value: string, - context: string, - context_parameter: string -} - -interface EnvVar { - key: string; - scopes: string[]; - values: EnvVarValue[]; - is_secret: boolean; - updated_at: string; - updated_by: UpdatedBy; -} - -interface GetEnvParams { - accountId: string, - siteId?: string, - context?: Context, - scope?: EnvironmentVariableScope -} - -interface DeleteEnvVarValueParams { - accountId: string, - key: string, - id: string, - siteId?: string -} - -interface SetEnvVarValueParams { - accountId: string, - key: string, - siteId?: string -} - -interface DeleteEnvVarValueParams extends SetEnvVarValueParams{} - -interface ExtendedNetlifyAPI extends NetlifyAPI { - getEnvVars( params: GetEnvParams): Promise - deleteEnvVarValue( params: DeleteEnvVarValueParams ): Promise - setEnvVarValue( params: SetEnvVarValueParams): +type EnviromentVariables = { + [key: string]: string } export type EnvironmentVariables = Record diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 68020a44a6b..a0e60a5bd34 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,6 +1,7 @@ -import { $TSFixMe } from '../../commands/types.js' -import type { EnvVar, Context } from '../../commands/types.js' +import { EnvVar } from '../../commands/api-types.d.js' +import type { Context, EnviromentVariables, $TSFixMe } from '../../commands/types.js' import { error } from '../command-helpers.js' +import { APIEnvError } from '../types.js' export const AVAILABLE_CONTEXTS: Context[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] @@ -255,7 +256,8 @@ 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: EnvVar[] = [], context = 'dev') => + +export const translateFromEnvelopeToMongo = (envVars: EnvVar[] = [], context = 'dev'): EnviromentVariables => envVars .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1)) .reduce((acc, cur) => { @@ -268,3 +270,19 @@ export const translateFromEnvelopeToMongo = (envVars: EnvVar[] = [], context = ' } return acc }, {}) + +// {key: string: string} + +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/types.ts b/src/utils/types.ts index 95f1b5266ba..4114e4b41ab 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -62,3 +62,7 @@ export interface Request extends IncomingMessage { } export type Rewriter = (req: Request) => Match | null + +export interface APIEnvError { + json: { msg: string } +} From 8a3066075e142d2ec115f87e3f8816aa36a3f3dc Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 14:35:55 -0500 Subject: [PATCH 03/13] fix: prettier fix Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-set.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 1ef5dd1efcc..11be9d61daf 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -16,15 +16,7 @@ import type { SetInEnvelopeParams } from './types.d.ts' * @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 -}: SetInEnvelopeParams) => { +const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value }: SetInEnvelopeParams) => { const accountId = siteInfo.account_slug const siteId = siteInfo.id From 113cf8be697eca499d56ef37ffe88bb196ec1fcd Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 16:07:58 -0500 Subject: [PATCH 04/13] feat: added types for the env commands Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/api-types.d.ts | 160 ++++++++++++++++++++++++++++++++-- src/commands/env/env-clone.ts | 5 +- src/commands/env/env-set.ts | 14 ++- src/commands/env/env-unset.ts | 9 +- src/commands/env/types.d.ts | 7 +- src/commands/types.d.ts | 1 - src/types/User.ts | 1 + src/utils/env/index.ts | 2 - 8 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 src/types/User.ts diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts index bf220ee4ab4..dcc3aa5610d 100644 --- a/src/commands/api-types.d.ts +++ b/src/commands/api-types.d.ts @@ -1,20 +1,28 @@ import type { NetlifyAPI } from 'netlify' +import { Context } from './types.d.ts' + +type ApiContext = Context | 'branch' + // Define the structure for the 'updated_by' field interface UpdatedBy { // Add specific properties here if known // For now, we'll keep it generic - [key: string]: any; + id: string; + full_name: string; + email: string; + avatar_url: string; } +export type Value = Pick // Define the structure for each item in the array interface EnvVarValue { - id: string, value: string, - context: string, - context_parameter: string -} + context: ApiContext, + context_parameter?: string, + id?: string, +} export interface EnvVar { key: string; @@ -43,8 +51,8 @@ interface SetEnvVarValueBody { context: string, value: string, contextParameter?: string, - } + interface SetEnvVarValueParams { accountId: string, key: string, @@ -52,6 +60,12 @@ interface SetEnvVarValueParams { body: SetEnvVarValueBody } +interface UpdateEnvVarBody { + key: string, + scopes: string[], + values: EnvVar[] + is_secret: boolean +} interface UpdateEnvVarParams { accountId: string, @@ -59,6 +73,138 @@ interface UpdateEnvVarParams { siteId?: string body: EnvVar } + +interface createEnvVarParams { + accountId: string, + key: string, + siteId?: string, + body: EnvVar[] +} + +// Top-Level Interface +interface SiteData { + id: string; + state: string; + plan: string; + name: string; + custom_domain: string; + domain_aliases: string[]; + branch_deploy_custom_domain: string; + deploy_preview_custom_domain: string; + password: string; + notification_email: string; + 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; + 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; + functions_region: string; +} + +// Published Deploy Interface +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; + draft: boolean; + required: string[]; + required_functions: string[]; + error_message: string; + branch: string; + commit_ref: string; + commit_url: string; + skipped: boolean; + created_at: string; + updated_at: string; + published_at: string; + title: string; + context: string; + locked: boolean; + review_url: string; + framework: string; + function_schedules: FunctionSchedule[]; +} + +// Function Schedule Interface +interface FunctionSchedule { + name: string; + cron: string; +} + +// Capabilities Interface +interface Capabilities { + [key: string]: Record; +} + +// Processing Settings Interface +interface ProcessingSettings { + html: HTMLProcessingSettings; +} + +// HTML Processing Settings Interface +interface HTMLProcessingSettings { + pretty_urls: boolean; +} + +// Build Settings Interface +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; +} + +// Environment Variables Interface +interface EnvVariables { + [key: string]: string; +} + +// Default Hooks Data Interface +interface DefaultHooksData { + access_token: string; +} + export interface ExtendedNetlifyAPI extends NetlifyAPI { getEnvVars( params: GetEnvParams): Promise @@ -66,4 +212,6 @@ export interface ExtendedNetlifyAPI extends NetlifyAPI { setEnvVarValue( params: SetEnvVarValueParams): Promise deleteEnvVar(params: DeleteEnvVarValueParams): Promise updateEnvVar(params: UpdateEnvVarParams): Promise + createEnvVars(params: createEnvVarParams): Promise + getSite(siteId: string): Promise } \ No newline at end of file diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 2f429283566..11b8e50761f 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -4,8 +4,9 @@ import { chalk, log, error as logError } from '../../utils/command-helpers.js' import { isAPIEnvError } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' -// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. -const safeGetSite = async (api, siteId) => { +import type { SafeGetSite } from './types.d.ts' + +const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { const data = await api.getSite({ siteId }) return { data } diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 11be9d61daf..56931853aa0 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -5,8 +5,9 @@ import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo, - APIEnvErrorMessage, + isAPIEnvError, } from '../../utils/env/index.js' +import type { Value } from '../api-types.js' import BaseCommand from '../base-command.js' import type { SetInEnvelopeParams } from './types.d.ts' @@ -45,7 +46,7 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } // if the passed context is unknown, it is actually a branch name - let values = contexts.map((ctx) => + let values: Value[] = contexts.map((ctx) => AVAILABLE_CONTEXTS.includes(ctx) ? { context: ctx, value } : { context: 'branch', context_parameter: ctx, value }, ) @@ -81,8 +82,7 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value 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,15 +95,13 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value await api.createEnvVars({ ...params, body }) } } catch (error_) { - if (error_ instanceof APIEnvErrorMessage) { - 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], } } diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 4e7015efb3a..07b25556592 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -58,12 +58,9 @@ const unsetInEnvelope = async ({ // otherwise, if no context passed, delete the whole key await api.deleteEnvVar({ accountId, siteId, key }) } - } catch (error_: unknown) { - if (isAPIEnvError(error_)) { - const errorMessage = error_.json ? error_.json.msg : error_ - throw errorMessage - } - throw error_ + } catch (error_) { + const errortoThrow = isAPIEnvError(error_) ? error_.json.msg : error_ + throw errortoThrow } delete env[key] diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts index 0253bfae766..c7f567c8911 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -1,5 +1,5 @@ import type { EnvironmentVariableScope } from '../../types.d.ts' -import type { ExtendedNetlifyAPI, Context } from '../apiTypes.d.ts' +import type { ExtendedNetlifyAPI, Context } from '../api-types.d.ts' export interface UnsetInEnvelopeParams { @@ -15,4 +15,9 @@ export interface SetInEnvelopeParams extends UnsetInEnvelopeParams { value: string, scope: EnvironmentVariableScope[], secret: boolean, +} + +export interface SafeGetSite { + api: ExtendedNetlifyAPI + siteId: string } \ No newline at end of file diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index 6d089a7a891..86e18315488 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -20,7 +20,6 @@ export type NetlifySite = { export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' - type PatchedConfig = NetlifyTOML & Pick & { functionsDirectory?: string build: NetlifyTOML['build'] & { diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/src/types/User.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index a0e60a5bd34..498a1fa09dd 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -271,8 +271,6 @@ export const translateFromEnvelopeToMongo = (envVars: EnvVar[] = [], context = ' return acc }, {}) -// {key: string: string} - export const isAPIEnvError = (err: unknown): err is APIEnvError => /** * Checks if an error is an APIEnvError From 0a5a07e5ef395f10ae5d5b22170e22e41e07a9c3 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 16:08:52 -0500 Subject: [PATCH 05/13] fix: prettier Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/env/env-clone.ts | 2 +- src/types/User.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 11b8e50761f..62956aa9999 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -4,7 +4,7 @@ import { chalk, log, error as logError } from '../../utils/command-helpers.js' import { isAPIEnvError } from '../../utils/env/index.js' import BaseCommand from '../base-command.js' -import type { SafeGetSite } from './types.d.ts' +import type { ExtendedNetlifyAPI } from './types.d.ts' const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { diff --git a/src/types/User.ts b/src/types/User.ts index 0519ecba6ea..e69de29bb2d 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -1 +0,0 @@ - \ No newline at end of file From a7a8f40701c7b70bd68e5b7235f115897fd6d5fc Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Wed, 23 Oct 2024 16:39:42 -0500 Subject: [PATCH 06/13] feat: env-clone types update in progress Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/api-types.d.ts | 2 +- src/commands/env/env-clone.ts | 15 ++++++++------- src/commands/env/types.d.ts | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts index dcc3aa5610d..487d842e1bc 100644 --- a/src/commands/api-types.d.ts +++ b/src/commands/api-types.d.ts @@ -213,5 +213,5 @@ export interface ExtendedNetlifyAPI extends NetlifyAPI { deleteEnvVar(params: DeleteEnvVarValueParams): Promise updateEnvVar(params: UpdateEnvVarParams): Promise createEnvVars(params: createEnvVarParams): Promise - getSite(siteId: string): Promise + getSite({siteId: string}): Promise } \ No newline at end of file diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 62956aa9999..f4fd13021a1 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -2,10 +2,9 @@ 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 type { ExtendedNetlifyAPI } from './types.d.ts' - const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { const data = await api.getSite({ siteId }) @@ -82,13 +81,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/types.d.ts b/src/commands/env/types.d.ts index c7f567c8911..5117e99e49e 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -20,4 +20,5 @@ export interface SetInEnvelopeParams extends UnsetInEnvelopeParams { export interface SafeGetSite { api: ExtendedNetlifyAPI siteId: string -} \ No newline at end of file +} +\ \ No newline at end of file From 8267aa5e310cae90429cf43c683c44313d06bf25 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 24 Oct 2024 14:04:04 -0400 Subject: [PATCH 07/13] feat: more types in Co-authored-by: Will --- src/commands/api-types.d.ts | 43 +++++++++++++++++------------- src/commands/env/env-clone.ts | 7 ++--- src/commands/env/env-get.ts | 3 ++- src/commands/env/types.d.ts | 13 ++++++--- src/commands/sites/sites-delete.ts | 2 +- src/commands/types.d.ts | 5 ++-- src/utils/env/index.ts | 24 ++++++++--------- src/utils/env/types.d.ts | 23 ++++++++++++++++ 8 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 src/utils/env/types.d.ts diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts index 487d842e1bc..bd6aabf7d7d 100644 --- a/src/commands/api-types.d.ts +++ b/src/commands/api-types.d.ts @@ -76,23 +76,23 @@ interface UpdateEnvVarParams { interface createEnvVarParams { accountId: string, - key: string, + key?: string, siteId?: string, body: EnvVar[] } // Top-Level Interface -interface SiteData { +interface SiteInfo { id: string; state: string; plan: string; name: string; - custom_domain: string; + custom_domain: string | null; domain_aliases: string[]; - branch_deploy_custom_domain: string; - deploy_preview_custom_domain: string; - password: string; - notification_email: 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; @@ -102,23 +102,24 @@ interface SiteData { user_id: string; session_id: string; ssl: boolean; - force_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; + git_provider?: string; deploy_hook: string; capabilities: Capabilities; processing_settings: ProcessingSettings; build_settings: BuildSettings; id_domain: string; - default_hooks_data: DefaultHooksData; + default_hooks_data?: DefaultHooksData; build_image: string; - prerender: string; + prerender: string | null; functions_region: string; + feature_flags: FeatureFlags; } // Published Deploy Interface @@ -135,7 +136,7 @@ interface PublishedDeploy { deploy_url: string; deploy_ssl_url: string; screenshot_url: string; - review_id: number; + review_id: number | null; draft: boolean; required: string[]; required_functions: string[]; @@ -143,16 +144,16 @@ interface PublishedDeploy { branch: string; commit_ref: string; commit_url: string; - skipped: boolean; + skipped: boolean | null; created_at: string; updated_at: string; published_at: string; title: string; context: string; - locked: boolean; - review_url: string; + locked: boolean | null; + review_url: string | null; framework: string; - function_schedules: FunctionSchedule[]; + function_schedules: FunctionSchedule[] | []; } // Function Schedule Interface @@ -203,15 +204,21 @@ interface EnvVariables { // Default Hooks Data Interface 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({siteId: string}): Promise + getSite(params: GetSiteParams): Promise } \ No newline at end of file diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index f4fd13021a1..ecb1683f903 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -4,6 +4,7 @@ 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 { CloneEnvParams } from './types.js' const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { @@ -18,14 +19,12 @@ const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { * 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) { @@ -35,10 +34,8 @@ 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 diff --git a/src/commands/env/env-get.ts b/src/commands/env/env-get.ts index 901fc51cd8a..b253508bf13 100644 --- a/src/commands/env/env-get.ts +++ b/src/commands/env/env-get.ts @@ -15,8 +15,9 @@ 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] || {} // Return json response for piping commands diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts index 5117e99e49e..63e9f28d127 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -12,13 +12,18 @@ export interface UnsetInEnvelopeParams { export interface SetInEnvelopeParams extends UnsetInEnvelopeParams { - value: string, - scope: EnvironmentVariableScope[], - secret: boolean, + value: string + scope: EnvironmentVariableScope[] + secret: boolean } export interface SafeGetSite { api: ExtendedNetlifyAPI siteId: string } -\ \ No newline at end of file + +export interface CloneEnvParams { + api: ExtendedNetlifyAPI + siteFrom: Site + siteTo: Site +} \ No newline at end of file diff --git a/src/commands/sites/sites-delete.ts b/src/commands/sites/sites-delete.ts index 70956c893c3..893029a3f2a 100644 --- a/src/commands/sites/sites-delete.ts +++ b/src/commands/sites/sites-delete.ts @@ -30,7 +30,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 86e18315488..a7208c1600a 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -19,6 +19,7 @@ export type NetlifySite = { } export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' +export type Scope = 'builds' | 'functions' | 'runtime' | 'post_processing' type PatchedConfig = NetlifyTOML & Pick & { functionsDirectory?: string @@ -73,9 +74,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/utils/env/index.ts b/src/utils/env/index.ts index 498a1fa09dd..abec6ea0835 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,10 +1,11 @@ -import { EnvVar } from '../../commands/api-types.d.js' -import type { Context, EnviromentVariables, $TSFixMe } from '../../commands/types.js' +import { EnvVar, ExtendedNetlifyAPI } from '../../commands/api-types.d.js' +import type { Context, EnviromentVariables, $TSFixMe, Scope } from '../../commands/types.js' import { error } from '../command-helpers.js' import { APIEnvError } from '../types.js' +import { GetEnvelopeEnvParams, ProcessedEnvVars } from './types.js' export const AVAILABLE_CONTEXTS: Context[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] -export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] +export const AVAILABLE_SCOPES: Scope[] = ['builds', 'functions', 'runtime', 'post_processing'] /** * @param {string|undefined} context - The deploy context or branch of the environment variable value @@ -56,7 +57,7 @@ export const findValueInValues = (values, context) => * @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) => // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source)) @@ -68,10 +69,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 [] } @@ -121,10 +122,10 @@ export const formatEnvelopeData = ({ source, }: { context?: string - envelopeItems: $TSFixMe[] + envelopeItems: EnvVar[] scope?: string source: string -}) => +}): ProcessedEnvVars => envelopeItems // filter by context .filter(({ values }) => Boolean(findValueInValues(values, context))) @@ -158,12 +159,10 @@ 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<$TSFixMe> => { 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 }), ]) @@ -176,7 +175,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, }), {}, diff --git a/src/utils/env/types.d.ts b/src/utils/env/types.d.ts new file mode 100644 index 00000000000..48e6f0117c6 --- /dev/null +++ b/src/utils/env/types.d.ts @@ -0,0 +1,23 @@ +import { Context, Scope } from "../../types.d.ts" +import { EnviromentVariables } from "../../commands/types.js" +import { SiteInfo } from "../../commands/api-types.js" + +export interface GetEnvelopeEnvParams { + api: ExtendedNetlifyAPI, + context?: Context, + env: EnvironmentVariables, + key: string, + scope: Scope, + raw?: boolean, + siteInfo: SiteInfo +} + +export type ProcessedEnvVars = { + [key: string]: { + context: string; + branch?: string; + scopes: string[]; + sources: string[]; + value: string; + }; +}; \ No newline at end of file From 54f664f53ead5d542b680381c5d1964213a04912 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 24 Oct 2024 14:06:35 -0400 Subject: [PATCH 08/13] fix: prettier --- src/commands/env/env-get.ts | 4 ++-- src/utils/env/index.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/commands/env/env-get.ts b/src/commands/env/env-get.ts index b253508bf13..d3596105ab1 100644 --- a/src/commands/env/env-get.ts +++ b/src/commands/env/env-get.ts @@ -15,9 +15,9 @@ 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] || {} // Return json response for piping commands diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index abec6ea0835..6b14be38be4 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -159,7 +159,15 @@ export const formatEnvelopeData = ({ * @param {object} siteInfo - The site object * @returns {object} An object of environment variables keys and their metadata */ -export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw = false, scope = 'any', siteInfo }: GetEnvelopeEnvParams):Promise<$TSFixMe> => { +export const getEnvelopeEnv = async ({ + api, + context = 'dev', + env, + key = '', + raw = false, + scope = 'any', + siteInfo, +}: GetEnvelopeEnvParams): Promise<$TSFixMe> => { const { account_slug: accountId, id: siteId } = siteInfo const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([ From 0a23af19e1d1b6b2386729d5f1d385f20f07f8bc Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Thu, 24 Oct 2024 15:49:59 -0500 Subject: [PATCH 09/13] feat: finished adding types for Env folder Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/api-types.d.ts | 4 +-- src/commands/env/env-clone.ts | 9 +++---- src/commands/env/env-get.ts | 8 +++--- src/commands/env/env-import.ts | 19 ++++++-------- src/commands/env/env-list.ts | 10 +++----- src/commands/env/env-set.ts | 6 ++--- src/commands/env/env-unset.ts | 6 ++--- src/commands/env/env.ts | 21 +++++++++++----- src/commands/env/types.d.ts | 46 ++++++++++++++++++++++++++++++++-- src/commands/types.d.ts | 5 ++-- src/utils/env/index.ts | 30 ++++++++++++++-------- src/utils/env/types.d.ts | 11 ++++---- 12 files changed, 113 insertions(+), 62 deletions(-) diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts index bd6aabf7d7d..5048e841267 100644 --- a/src/commands/api-types.d.ts +++ b/src/commands/api-types.d.ts @@ -26,9 +26,9 @@ interface EnvVarValue { export interface EnvVar { key: string; - scopes: string[]; + scopes: Scope[]; values: EnvVarValue[]; - is_secret: boolean; + is_secret?: boolean; updated_at?: string; updated_by?: UpdatedBy; } diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index ecb1683f903..482e2579bee 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -1,10 +1,9 @@ -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 { CloneEnvParams } from './types.js' + +import { CloneEnvParams, EnvCloneOptions } from './types.js' const safeGetSite = async (api: ExtendedNetlifyAPI, siteId: string) => { try { @@ -48,10 +47,10 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }: CloneEnvParams): Promise< 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) { + if (!site.id || !options.from) { log( 'Please include the source site Id as the `--from` option, or run `netlify link` to link this folder to a Netlify site', ) diff --git a/src/commands/env/env-get.ts b/src/commands/env/env-get.ts index d3596105ab1..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 @@ -27,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 2625949794a..68f13049b6d 100644 --- a/src/commands/env/env-import.ts +++ b/src/commands/env/env-import.ts @@ -1,35 +1,32 @@ 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, 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 @@ -44,13 +41,12 @@ const importDotEnv = async ({ api, importedEnv, options, siteInfo }) => { // 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 @@ -63,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..0a6f232101b 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) @@ -81,7 +79,7 @@ export const envList = async (options: OptionValues, command: BaseCommand) => { } const forSite = `for site ${chalk.green(siteInfo.name)}` - const contextType = AVAILABLE_CONTEXTS.includes(context) ? 'context' : 'branch' + const contextType = context === undefined ? 'branch' : AVAILABLE_CONTEXTS.includes(context) const withContext = `in the ${chalk.magenta(options.context)} ${contextType}` const withScope = scope === 'any' ? '' : `and ${chalk.yellow(options.scope)} scope` if (Object.keys(environment).length === 0) { diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index 56931853aa0..f3ee4ae5cdf 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -1,5 +1,3 @@ -import { OptionValues } from 'commander' - import { chalk, error, log, logJson } from '../../utils/command-helpers.js' import { AVAILABLE_CONTEXTS, @@ -10,7 +8,7 @@ import { import type { Value } from '../api-types.js' import BaseCommand from '../base-command.js' -import type { SetInEnvelopeParams } from './types.d.ts' +import type { SetInEnvelopeParams, EnvSetOptions } from './types.d.ts' /** * Updates the env for a site configured with Envelope with a new key/value pair @@ -106,7 +104,7 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } } -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 07b25556592..1d429967242 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -1,11 +1,9 @@ -import { OptionValues } from 'commander' - import { chalk, log, logJson } from '../../utils/command-helpers.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 { UnsetInEnvelopeParams } 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 @@ -68,7 +66,7 @@ const unsetInEnvelope = async ({ 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 31a085b1c33..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) }) @@ -113,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) }) @@ -134,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) }) @@ -146,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 index 63e9f28d127..61fd2ae4e61 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -1,6 +1,41 @@ +import { OptionValues } from 'commander' + import type { EnvironmentVariableScope } from '../../types.d.ts' -import type { ExtendedNetlifyAPI, Context } from '../api-types.d.ts' +import type { ExtendedNetlifyAPI, Context, SiteInfo } from '../api-types.d.ts' +import { $TSFixMe } from '../types.js' + + + +export interface EnvOptions extends OptionValues { + context: Context + 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 @@ -26,4 +61,11 @@ export interface CloneEnvParams { api: ExtendedNetlifyAPI siteFrom: Site siteTo: Site -} \ No newline at end of file +} + +export interface ImportDotEnvParams { + api: ExtendedNetlifyAPI + options: EnvImportOptions + importedEnv: $TSFixMe + siteInfo: SiteInfo +} diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index a7208c1600a..fb094583065 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -16,6 +16,7 @@ export type NetlifySite = { siteId?: string get id(): string | undefined set id(id: string): void + id: string } export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' @@ -52,8 +53,8 @@ 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' type EnviromentVariables = { diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 6b14be38be4..9184fed90b4 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,7 +1,15 @@ -import { EnvVar, ExtendedNetlifyAPI } from '../../commands/api-types.d.js' -import type { Context, EnviromentVariables, $TSFixMe, Scope } from '../../commands/types.js' +import { EnvVar, EnvVarValue, ExtendedNetlifyAPI } from '../../commands/api-types.d.js' +import type { + Context, + EnviromentVariables, + $TSFixMe, + Scope, + EnvironmentVariableSource, + EnvironmentVariableScope, +} from '../../commands/types.js' import { error } from '../command-helpers.js' import { APIEnvError } from '../types.js' + import { GetEnvelopeEnvParams, ProcessedEnvVars } from './types.js' export const AVAILABLE_CONTEXTS: Context[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] @@ -56,8 +64,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: EnviromentVariables, 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)) @@ -123,8 +130,8 @@ export const formatEnvelopeData = ({ }: { context?: string envelopeItems: EnvVar[] - scope?: string - source: string + scope?: EnvironmentVariableScope | 'any' + source: EnvironmentVariableSource }): ProcessedEnvVars => envelopeItems // filter by context @@ -167,7 +174,7 @@ export const getEnvelopeEnv = async ({ raw = false, scope = 'any', siteInfo, -}: GetEnvelopeEnvParams): Promise<$TSFixMe> => { +}: GetEnvelopeEnvParams): Promise => { const { account_slug: accountId, id: siteId } = siteInfo const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([ @@ -241,14 +248,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: Context = 'all' + const envVars = Object.entries(env).map(([key, value]) => ({ key, scopes: AVAILABLE_SCOPES, values: [ { - context: 'all', - value, + context, + value: String(value), }, ], })) diff --git a/src/utils/env/types.d.ts b/src/utils/env/types.d.ts index 48e6f0117c6..7a710988370 100644 --- a/src/utils/env/types.d.ts +++ b/src/utils/env/types.d.ts @@ -1,13 +1,12 @@ -import { Context, Scope } from "../../types.d.ts" -import { EnviromentVariables } from "../../commands/types.js" import { SiteInfo } from "../../commands/api-types.js" +import { Context, EnvironmentVariableSource, EnvironmentVariableScope } from "../../commands/types.js" export interface GetEnvelopeEnvParams { api: ExtendedNetlifyAPI, context?: Context, env: EnvironmentVariables, - key: string, - scope: Scope, + key?: string, + scope?: EnvironmentVariableScope | 'any' raw?: boolean, siteInfo: SiteInfo } @@ -16,8 +15,8 @@ export type ProcessedEnvVars = { [key: string]: { context: string; branch?: string; - scopes: string[]; - sources: string[]; + scopes: EnvironmentVariableScope[]; + sources: EnvironmentVariableSource[]; value: string; }; }; \ No newline at end of file From 2fecfa373cb65750f2ff4c8320fd0352f51af671 Mon Sep 17 00:00:00 2001 From: Will Conrad Date: Thu, 24 Oct 2024 16:27:16 -0500 Subject: [PATCH 10/13] fix: update error Type Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> --- src/commands/api/api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/api/api.ts b/src/commands/api/api.ts index 10caa2d127c..28c055fb344 100644 --- a/src/commands/api/api.ts +++ b/src/commands/api/api.ts @@ -40,7 +40,8 @@ export const apiCommand = async (apiMethod: string, options: OptionValues, comma const apiResponse = await api[apiMethod](payload) logJson(apiResponse) } catch (error_) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - error(error_) + if (error_ instanceof Error || typeof error_ === 'string') { + error(error_) + } } } From 796367f5ba887239f89664f1b11416345e02a15a Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 28 Oct 2024 10:43:02 -0500 Subject: [PATCH 11/13] fix: fixed Type issues for tests Co-authored-by: Thomas Lane <163203257+tlane25@users.noreply.github.com> Co-authored-by: Thomas Lane --- src/commands/env/env-clone.ts | 11 +++++++++-- src/commands/env/env-list.ts | 2 +- src/commands/env/env-unset.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/env/env-clone.ts b/src/commands/env/env-clone.ts index 482e2579bee..0d841d62d04 100644 --- a/src/commands/env/env-clone.ts +++ b/src/commands/env/env-clone.ts @@ -2,6 +2,7 @@ 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' @@ -50,15 +51,21 @@ const cloneEnvVars = async ({ api, siteFrom, siteTo }: CloneEnvParams): Promise< export const envClone = async (options: EnvCloneOptions, command: BaseCommand) => { const { api, site } = command.netlify - if (!site.id || !options.from) { + if (!site.id && !options.from) { log( 'Please include the source site Id as the `--from` option, or run `netlify link` to link this folder to a Netlify site', ) 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, } diff --git a/src/commands/env/env-list.ts b/src/commands/env/env-list.ts index 0a6f232101b..02d9ae72ef1 100644 --- a/src/commands/env/env-list.ts +++ b/src/commands/env/env-list.ts @@ -79,7 +79,7 @@ export const envList = async (options: EnvListOptions, command: BaseCommand) => } const forSite = `for site ${chalk.green(siteInfo.name)}` - const contextType = context === undefined ? 'branch' : AVAILABLE_CONTEXTS.includes(context) + const contextType = AVAILABLE_CONTEXTS.includes(context) ? 'context' : 'branch' const withContext = `in the ${chalk.magenta(options.context)} ${contextType}` const withScope = scope === 'any' ? '' : `and ${chalk.yellow(options.scope)} scope` if (Object.keys(environment).length === 0) { diff --git a/src/commands/env/env-unset.ts b/src/commands/env/env-unset.ts index 1d429967242..8acb88508d5 100644 --- a/src/commands/env/env-unset.ts +++ b/src/commands/env/env-unset.ts @@ -18,7 +18,7 @@ const unsetInEnvelope = async ({ }: UnsetInEnvelopeParams): Promise => { const accountId = siteInfo.account_slug const siteId = siteInfo.id - console.log('siteId is type of', typeof siteId) + // fetch envelope env vars const envelopeVariables = await api.getEnvVars({ accountId, siteId }) const contexts = context || ['all'] From c06b36e041df6f64d2debec3d22e636013dbeae0 Mon Sep 17 00:00:00 2001 From: tlane25 <163203257+tlane25@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:47:34 -0400 Subject: [PATCH 12/13] fix: prettier Co-authored-by: Will --- src/commands/init/types.d.ts | 7 +++++++ src/utils/types.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/commands/init/types.d.ts 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/utils/types.ts b/src/utils/types.ts index 74bc59bf2c2..0d1524929f0 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -194,4 +194,4 @@ export interface Template { export interface APIEnvError { json: { msg: string } -} \ No newline at end of file +} From 80e88e601a987803001fd80a67e49da6faaaa269 Mon Sep 17 00:00:00 2001 From: tlane25 <163203257+tlane25@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:35:25 -0500 Subject: [PATCH 13/13] chore: more explicit type names Co-authored-by: Will --- src/commands/api-types.d.ts | 23 ++++++----------------- src/commands/env/env-set.ts | 4 ++-- src/commands/env/types.d.ts | 6 +++--- src/commands/types.d.ts | 2 +- src/utils/env/index.ts | 7 +++---- src/utils/env/types.d.ts | 4 ++-- 6 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/commands/api-types.d.ts b/src/commands/api-types.d.ts index 5048e841267..3fe7808ecea 100644 --- a/src/commands/api-types.d.ts +++ b/src/commands/api-types.d.ts @@ -1,21 +1,17 @@ import type { NetlifyAPI } from 'netlify' -import { Context } from './types.d.ts' +import { DeployContext } from './types.d.ts' -type ApiContext = Context | 'branch' +type ApiContext = DeployContext | 'branch' -// Define the structure for the 'updated_by' field interface UpdatedBy { - // Add specific properties here if known - // For now, we'll keep it generic id: string; full_name: string; email: string; avatar_url: string; } -export type Value = Pick -// Define the structure for each item in the array +export type NarrowedEnvVarValue = Pick interface EnvVarValue { value: string, @@ -36,7 +32,7 @@ export interface EnvVar { interface GetEnvParams { accountId: string, siteId?: string, - context?: Context, + context?: DeployContext, scope?: EnvironmentVariableScope } @@ -74,14 +70,13 @@ interface UpdateEnvVarParams { body: EnvVar } -interface createEnvVarParams { +interface CreateEnvVarParams { accountId: string, key?: string, siteId?: string, body: EnvVar[] } -// Top-Level Interface interface SiteInfo { id: string; state: string; @@ -122,7 +117,6 @@ interface SiteInfo { feature_flags: FeatureFlags; } -// Published Deploy Interface interface PublishedDeploy { id: string; site_id: string; @@ -156,13 +150,11 @@ interface PublishedDeploy { function_schedules: FunctionSchedule[] | []; } -// Function Schedule Interface interface FunctionSchedule { name: string; cron: string; } -// Capabilities Interface interface Capabilities { [key: string]: Record; } @@ -177,7 +169,6 @@ interface HTMLProcessingSettings { pretty_urls: boolean; } -// Build Settings Interface interface BuildSettings { id: number; provider: string; @@ -196,12 +187,10 @@ interface BuildSettings { stop_builds: boolean; } -// Environment Variables Interface interface EnvVariables { [key: string]: string; } -// Default Hooks Data Interface interface DefaultHooksData { access_token: string; } @@ -219,6 +208,6 @@ export interface ExtendedNetlifyAPI extends NetlifyAPI { setEnvVarValue( params: SetEnvVarValueParams): Promise deleteEnvVar(params: DeleteEnvVarValueParams): Promise updateEnvVar(params: UpdateEnvVarParams): Promise - createEnvVars(params: createEnvVarParams): Promise + createEnvVars(params: CreateEnvVarParams): Promise getSite(params: GetSiteParams): Promise } \ No newline at end of file diff --git a/src/commands/env/env-set.ts b/src/commands/env/env-set.ts index f3ee4ae5cdf..e33bc9a1007 100644 --- a/src/commands/env/env-set.ts +++ b/src/commands/env/env-set.ts @@ -5,7 +5,7 @@ import { translateFromEnvelopeToMongo, isAPIEnvError, } from '../../utils/env/index.js' -import type { Value } from '../api-types.js' +import type { NarrowedEnvVarValue } from '../api-types.js' import BaseCommand from '../base-command.js' import type { SetInEnvelopeParams, EnvSetOptions } from './types.d.ts' @@ -44,7 +44,7 @@ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value } // if the passed context is unknown, it is actually a branch name - let values: Value[] = contexts.map((ctx) => + let values: NarrowedEnvVarValue[] = contexts.map((ctx) => AVAILABLE_CONTEXTS.includes(ctx) ? { context: ctx, value } : { context: 'branch', context_parameter: ctx, value }, ) diff --git a/src/commands/env/types.d.ts b/src/commands/env/types.d.ts index 61fd2ae4e61..df605ad6b83 100644 --- a/src/commands/env/types.d.ts +++ b/src/commands/env/types.d.ts @@ -1,13 +1,13 @@ import { OptionValues } from 'commander' import type { EnvironmentVariableScope } from '../../types.d.ts' -import type { ExtendedNetlifyAPI, Context, SiteInfo } from '../api-types.d.ts' +import type { ExtendedNetlifyAPI, DeployContext, SiteInfo } from '../api-types.d.ts' import { $TSFixMe } from '../types.js' export interface EnvOptions extends OptionValues { - context: Context + context: DeployContext scope: EnvironmentVariableScope json: boolean @@ -39,7 +39,7 @@ export interface EnvImportOptions extends OptionValues { export interface UnsetInEnvelopeParams { api: ExtendedNetlifyAPI - context: Context[] + context: DeployContext[] key: string // eslint-disable-next-line @typescript-eslint/no-explicit-any siteInfo: any diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index fb094583065..58bd4b852af 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -19,7 +19,7 @@ export type NetlifySite = { id: string } -export type Context = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' +export type DeployContext = 'dev' | 'production' | 'deploy-preview' | 'branch-deploy' | 'all' export type Scope = 'builds' | 'functions' | 'runtime' | 'post_processing' type PatchedConfig = NetlifyTOML & Pick & { diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 9184fed90b4..40b92de3a91 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,8 +1,7 @@ import { EnvVar, EnvVarValue, ExtendedNetlifyAPI } from '../../commands/api-types.d.js' import type { - Context, + DeployContext, EnviromentVariables, - $TSFixMe, Scope, EnvironmentVariableSource, EnvironmentVariableScope, @@ -12,7 +11,7 @@ import { APIEnvError } from '../types.js' import { GetEnvelopeEnvParams, ProcessedEnvVars } from './types.js' -export const AVAILABLE_CONTEXTS: Context[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] +export const AVAILABLE_CONTEXTS: DeployContext[] = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] export const AVAILABLE_SCOPES: Scope[] = ['builds', 'functions', 'runtime', 'post_processing'] /** @@ -250,7 +249,7 @@ export const getHumanReadableScopes = (scopes) => { */ export const translateFromMongoToEnvelope = (env = {}): EnvVar[] => { - const context: Context = 'all' + const context: DeployContext = 'all' const envVars = Object.entries(env).map(([key, value]) => ({ key, diff --git a/src/utils/env/types.d.ts b/src/utils/env/types.d.ts index 7a710988370..24c5af87ae2 100644 --- a/src/utils/env/types.d.ts +++ b/src/utils/env/types.d.ts @@ -1,9 +1,9 @@ import { SiteInfo } from "../../commands/api-types.js" -import { Context, EnvironmentVariableSource, EnvironmentVariableScope } from "../../commands/types.js" +import { DeployContext, EnvironmentVariableSource, EnvironmentVariableScope } from "../../commands/types.js" export interface GetEnvelopeEnvParams { api: ExtendedNetlifyAPI, - context?: Context, + context?: DeployContext, env: EnvironmentVariables, key?: string, scope?: EnvironmentVariableScope | 'any'