Skip to content

Commit 2172a38

Browse files
authored
refactor(utils/env): rename variables, add comments, and clarify intent (#7234)
* refactor: rename and comment `utils/env` for clarity * refactor: improve types in utils/env * refactor: clarify intent in `getValueForContext` * refactor: declare array type instead of scattering `as const`s * refactor: match Envlope env var context type to API type exactly
1 parent 444ddff commit 2172a38

File tree

6 files changed

+126
-71
lines changed

6 files changed

+126
-71
lines changed

src/commands/env/env-get.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OptionValues } from 'commander'
22

33
import { chalk, log, logJson } from '../../utils/command-helpers.js'
4-
import { AVAILABLE_CONTEXTS, getEnvelopeEnv } from '../../utils/env/index.js'
4+
import { SUPPORTED_CONTEXTS, getEnvelopeEnv } from '../../utils/env/index.js'
55
import BaseCommand from '../base-command.js'
66

77
export const envGet = async (name: string, options: OptionValues, command: BaseCommand) => {
@@ -27,7 +27,7 @@ export const envGet = async (name: string, options: OptionValues, command: BaseC
2727
}
2828

2929
if (!value) {
30-
const contextType = AVAILABLE_CONTEXTS.includes(context) ? 'context' : 'branch'
30+
const contextType = SUPPORTED_CONTEXTS.includes(context) ? 'context' : 'branch'
3131
const withContext = `in the ${chalk.magenta(context)} ${contextType}`
3232
const withScope = scope === 'any' ? '' : ` and the ${chalk.magenta(scope)} scope`
3333
log(`No value set ${withContext}${withScope} for environment variable ${chalk.yellow(name)}`)

src/commands/env/env-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import inquirer from 'inquirer'
66
import logUpdate from 'log-update'
77

88
import { chalk, log, logJson } from '../../utils/command-helpers.js'
9-
import { AVAILABLE_CONTEXTS, getEnvelopeEnv, getHumanReadableScopes } from '../../utils/env/index.js'
9+
import { SUPPORTED_CONTEXTS, getEnvelopeEnv, getHumanReadableScopes } from '../../utils/env/index.js'
1010
import type BaseCommand from '../base-command.js'
1111
import { EnvironmentVariables } from '../../utils/types.js'
1212

@@ -81,7 +81,7 @@ export const envList = async (options: OptionValues, command: BaseCommand) => {
8181
}
8282

8383
const forSite = `for site ${chalk.green(siteInfo.name)}`
84-
const contextType = AVAILABLE_CONTEXTS.includes(context) ? 'context' : 'branch'
84+
const contextType = SUPPORTED_CONTEXTS.includes(context) ? 'context' : 'branch'
8585
const withContext = `in the ${chalk.magenta(options.context)} ${contextType}`
8686
const withScope = scope === 'any' ? '' : `and ${chalk.yellow(options.scope)} scope`
8787
if (Object.keys(environment).length === 0) {

src/commands/env/env-set.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OptionValues } from 'commander'
22

33
import { chalk, logAndThrowError, log, logJson } from '../../utils/command-helpers.js'
4-
import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
4+
import { SUPPORTED_CONTEXTS, ALL_ENVELOPE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
55
import { promptOverwriteEnvVariable } from '../../utils/prompts/env-set-prompts.js'
66
import BaseCommand from '../base-command.js'
77

@@ -30,7 +30,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo
3030
// fetch envelope env vars
3131
const envelopeVariables = await api.getEnvVars({ accountId, siteId })
3232
const contexts = context || ['all']
33-
let scopes = scope || AVAILABLE_SCOPES
33+
let scopes = scope || ALL_ENVELOPE_SCOPES
3434

3535
if (secret) {
3636
// post_processing (aka post-processing) scope is not allowed with secrets
@@ -41,7 +41,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo
4141
// if the passed context is unknown, it is actually a branch name
4242
// @ts-expect-error TS(7006) FIXME: Parameter 'ctx' implicitly has an 'any' type.
4343
let values = contexts.map((ctx) =>
44-
AVAILABLE_CONTEXTS.includes(ctx) ? { context: ctx, value } : { context: 'branch', context_parameter: ctx, value },
44+
SUPPORTED_CONTEXTS.includes(ctx) ? { context: ctx, value } : { context: 'branch', context_parameter: ctx, value },
4545
)
4646

4747
// @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type.
@@ -78,7 +78,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo
7878
if (values.some((val) => val.context === 'all')) {
7979
log(`This secret's value will be empty in the dev context.`)
8080
log(`Run \`netlify env:set ${key} <value> --context dev\` to set a new value for the dev context.`)
81-
values = AVAILABLE_CONTEXTS.filter((ctx) => ctx !== 'all').map((ctx) => ({
81+
values = SUPPORTED_CONTEXTS.filter((ctx) => ctx !== 'all').map((ctx) => ({
8282
context: ctx,
8383
// empty out dev value so that secret is indeed secret
8484
// @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type.
@@ -132,7 +132,7 @@ export const envSet = async (key: string, value: string, options: OptionValues,
132132

133133
const withScope = scope ? ` scoped to ${chalk.white(scope)}` : ''
134134
const withSecret = secret ? ` as a ${chalk.blue('secret')}` : ''
135-
const contextType = AVAILABLE_CONTEXTS.includes(context || 'all') ? 'context' : 'branch'
135+
const contextType = SUPPORTED_CONTEXTS.includes(context || 'all') ? 'context' : 'branch'
136136
log(
137137
`Set environment variable ${chalk.yellow(
138138
`${key}${value && !secret ? `=${value}` : ''}`,

src/commands/env/env-unset.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OptionValues } from 'commander'
22

33
import { chalk, log, logJson, exit } from '../../utils/command-helpers.js'
4-
import { AVAILABLE_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
4+
import { SUPPORTED_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
55
import { promptOverwriteEnvVariable } from '../../utils/prompts/env-unset-prompts.js'
66
import BaseCommand from '../base-command.js'
77
/**
@@ -43,7 +43,7 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => {
4343
await Promise.all(values.map((value) => api.deleteEnvVarValue({ ...params, id: value.id })))
4444
// if this was the `all` context, we need to create 3 values in the other contexts
4545
if (values.length === 1 && values[0].context === 'all') {
46-
const newContexts = AVAILABLE_CONTEXTS.filter((ctx) => !context.includes(ctx))
46+
const newContexts = SUPPORTED_CONTEXTS.filter((ctx) => !context.includes(ctx))
4747
const allValue = values[0].value
4848
await Promise.all(
4949
newContexts
@@ -87,6 +87,6 @@ export const envUnset = async (key: string, options: OptionValues, command: Base
8787
return false
8888
}
8989

90-
const contextType = AVAILABLE_CONTEXTS.includes(context || 'all') ? 'context' : 'branch'
90+
const contextType = SUPPORTED_CONTEXTS.includes(context || 'all') ? 'context' : 'branch'
9191
log(`Unset environment variable ${chalk.yellow(key)} in the ${chalk.magenta(context || 'all')} ${contextType}`)
9292
}

src/utils/env/index.ts

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,117 @@
11
import type { NetlifyAPI } from 'netlify'
22

3-
import { $TSFixMe } from '../../commands/types.js'
43
import { logAndThrowError } from '../command-helpers.js'
54
import type { SiteInfo, EnvironmentVariableSource } from '../../utils/types.js'
65

7-
export const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev']
8-
export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing']
9-
10-
type EnvironmentVariableContext = 'all' | 'production' | 'deploy-preview' | 'branch-deploy' | 'dev'
11-
12-
type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing'
6+
/**
7+
* Supported values for the user-provided env `context` option.
8+
* These all match possible `context` values returned by the Envelope API.
9+
* Note that a user may also specify a branch name with the special `branch:my-branch-name` format.
10+
*/
11+
export const SUPPORTED_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] as const
12+
/**
13+
* Additional aliases for the user-provided env `context` option.
14+
*/
15+
const SUPPORTED_CONTEXT_ALIASES = {
16+
dp: 'deploy-preview',
17+
prod: 'production',
18+
}
19+
/**
20+
* Supported values for the user-provided env `scope` option.
21+
* These exactly match possible `scope` values returned by the Envelope API.
22+
* Note that `any` is also supported.
23+
*/
24+
export const ALL_ENVELOPE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] as const
1325

14-
type EnvironmentVariableValue = {
15-
context: EnvironmentVariableContext
26+
// TODO(serhalp) Netlify API is incorrect - the returned scope is `post_processing`, not `post-processing`
27+
type EnvelopeEnvVarScope =
28+
| Exclude<NonNullable<Awaited<ReturnType<NetlifyAPI['getEnvVars']>>[number]['scopes']>[number], 'post-processing'>
29+
| 'post_processing'
30+
type EnvelopeEnvVar = Awaited<ReturnType<NetlifyAPI['getEnvVars']>>[number] & {
31+
scopes: EnvelopeEnvVarScope[]
32+
}
33+
type EnvelopeEnvVarContext = NonNullable<NonNullable<EnvelopeEnvVar['values']>[number]['context']>
34+
export type EnvelopeEnvVarValue = {
35+
/**
36+
* The deploy context of the this env var value
37+
*/
38+
context?: EnvelopeEnvVarContext
39+
/**
40+
* For parameterized contexts (i.e. only `branch`), context parameter (i.e. the branch name)
41+
*/
1642
context_parameter?: string | undefined
17-
value: string
43+
/**
44+
* The value of the environment variable for this context. Note that this appears to be an empty string
45+
* when the env var is not set for this context.
46+
*/
47+
value?: string | undefined
48+
}
49+
50+
export type EnvelopeItem = {
51+
// FIXME(serhalp) Netlify API types claim this is optional. Investigate and fix here or there.
52+
key: string
53+
scopes: EnvelopeEnvVarScope[]
54+
values: EnvelopeEnvVarValue[]
1855
}
1956

57+
// AFAICT, Envelope uses only `post_processing` on returned env vars; the CLI documents and expects
58+
// only `post-processing` as a valid user-provided scope; the code handles both everywhere. Consider
59+
// explicitly normalizing and dropping undocumented support for user-provided `post_processing`.
60+
type SupportedScope = EnvelopeEnvVarScope | 'post_processing' | 'any'
61+
62+
type ContextOrBranch = string
63+
2064
/**
21-
* @param context The deploy context or branch of the environment variable value
22-
* @returns The normalized context or branch name
65+
* Normalizes a user-provided "context". Note that this may be the special `branch:my-branch-name` format.
66+
*
67+
* - If this is a supported alias of a context, it will be normalized to the canonical context.
68+
* - Valid canonical contexts are returned as is.
69+
* - If this starts with `branch:`, it will be normalized to the branch name.
70+
*
71+
* @param context A user-provided context, context alias, or a string in the `branch:my-branch-name` format.
72+
*
73+
* @returns The normalized context name or just the branch name
2374
*/
24-
export const normalizeContext = (context: string): string => {
75+
export const normalizeContext = (context: string): ContextOrBranch => {
2576
if (!context) {
2677
return context
2778
}
28-
const CONTEXT_SYNONYMS = {
29-
dp: 'deploy-preview',
30-
prod: 'production',
31-
}
79+
3280
context = context.toLowerCase()
33-
if (context in CONTEXT_SYNONYMS) {
34-
context = CONTEXT_SYNONYMS[context as keyof typeof CONTEXT_SYNONYMS]
81+
if (context in SUPPORTED_CONTEXT_ALIASES) {
82+
context = SUPPORTED_CONTEXT_ALIASES[context as keyof typeof SUPPORTED_CONTEXT_ALIASES]
3583
}
36-
const forbiddenContexts = AVAILABLE_CONTEXTS.map((ctx) => `branch:${ctx}`)
84+
const forbiddenContexts = SUPPORTED_CONTEXTS.map((ctx) => `branch:${ctx}`)
3785
if (forbiddenContexts.includes(context)) {
3886
return logAndThrowError(`The context ${context} includes a reserved keyword and is not allowed`)
3987
}
4088
return context.replace(/^branch:/, '')
4189
}
4290

4391
/**
44-
* Finds a matching environment variable value from a given context
92+
* Finds a matching environment variable value for a given context
93+
* @private
4594
*/
46-
export const findValueInValues = (
95+
export const getValueForContext = (
4796
/**
4897
* An array of environment variable values from Envelope
4998
*/
50-
values: EnvironmentVariableValue[],
99+
values: EnvelopeEnvVarValue[],
51100
/**
52101
* The deploy context or branch of the environment variable value
53102
*/
54-
context: string,
55-
) =>
56-
values.find((val) => {
57-
if (!AVAILABLE_CONTEXTS.includes(context)) {
103+
contextOrBranch: ContextOrBranch,
104+
): EnvelopeEnvVarValue | undefined => {
105+
const valueForContext = values.find((val) => {
106+
const isSupportedContext = (SUPPORTED_CONTEXTS as readonly string[]).includes(contextOrBranch)
107+
if (!isSupportedContext) {
58108
// the "context" option passed in is actually the name of a branch
59-
return val.context === 'all' || val.context_parameter === context
109+
return val.context === 'all' || val.context_parameter === contextOrBranch
60110
}
61-
return [context, 'all'].includes(val.context)
111+
return val.context === 'all' || val.context === contextOrBranch
62112
})
113+
return valueForContext ?? undefined
114+
}
63115

64116
/**
65117
* Finds environment variables that match a given source
@@ -70,7 +122,6 @@ export const findValueInValues = (
70122
export const filterEnvBySource = (env: object, source: EnvironmentVariableSource): typeof env =>
71123
Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source))
72124

73-
// Fetches data from Envelope
74125
const fetchEnvelopeItems = async function ({
75126
accountId,
76127
api,
@@ -81,19 +132,21 @@ const fetchEnvelopeItems = async function ({
81132
api: NetlifyAPI
82133
key: string
83134
siteId?: string | undefined
84-
}): Promise<Awaited<ReturnType<NetlifyAPI['getEnvVar']>>[]> {
135+
}): Promise<EnvelopeItem[]> {
85136
if (accountId === undefined) {
86137
return []
87138
}
88139
try {
89140
// if a single key is passed, fetch that single env var
90141
if (key) {
91142
const envelopeItem = await api.getEnvVar({ accountId, key, siteId })
92-
return [envelopeItem]
143+
// See FIXME(serhalp) above
144+
return [envelopeItem as EnvelopeItem]
93145
}
94146
// otherwise, fetch the entire list of env vars
95147
const envelopeItems = await api.getEnvVars({ accountId, siteId })
96-
return envelopeItems
148+
// See FIXME(serhalp) above
149+
return envelopeItems as EnvelopeItem[]
97150
} catch {
98151
// Collaborators aren't allowed to read shared env vars,
99152
// so return an empty array silently in that case
@@ -130,38 +183,38 @@ export const formatEnvelopeData = ({
130183
scope = 'any',
131184
source,
132185
}: {
133-
context?: string
134-
envelopeItems: $TSFixMe[]
135-
scope?: string
186+
context?: ContextOrBranch
187+
envelopeItems: EnvelopeItem[]
188+
scope?: SupportedScope
136189
source: string
137190
}): Record<
138191
string,
139192
{
140-
context: string
141-
branch: string
193+
context: ContextOrBranch
194+
branch: string | undefined
142195
scopes: string[]
143196
sources: string[]
144197
value: string
145198
}
146199
> =>
147200
envelopeItems
148201
// filter by context
149-
.filter(({ values }) => Boolean(findValueInValues(values, context)))
202+
.filter(({ values }) => Boolean(getValueForContext(values, context)))
150203
// filter by scope
151204
.filter(({ scopes }) => (scope === 'any' ? true : scopes.includes(scope)))
152205
// sort alphabetically, case insensitive
153206
.sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1))
154207
// format the data
155208
.reduce((acc, cur) => {
156-
const val = findValueInValues(cur.values, context)
209+
const val = getValueForContext(cur.values, context)
157210
if (val === undefined) {
158211
throw new TypeError(`failed to locate environment variable value for ${context} context`)
159212
}
160-
const { context: ctx, context_parameter: branch, value } = val
213+
const { context: itemContext, context_parameter: branch, value } = val
161214
return {
162215
...acc,
163216
[cur.key]: {
164-
context: ctx,
217+
context: itemContext,
165218
branch,
166219
scopes: cur.scopes,
167220
sources: [source],
@@ -191,11 +244,11 @@ export const getEnvelopeEnv = async ({
191244
siteInfo,
192245
}: {
193246
api: NetlifyAPI
194-
context?: string | undefined
247+
context?: ContextOrBranch | undefined
195248
env: object
196249
key?: string | undefined
197250
raw?: boolean | undefined
198-
scope?: string | undefined
251+
scope?: SupportedScope | undefined
199252
siteInfo: SiteInfo
200253
}) => {
201254
const { account_slug: accountId, id: siteId } = siteInfo
@@ -243,13 +296,15 @@ export const getEnvelopeEnv = async ({
243296
* @param scopes An array of scopes
244297
* @returns A human-readable, comma-separated list of scopes
245298
*/
246-
export const getHumanReadableScopes = (scopes?: (EnvironmentVariableScope | 'post-processing')[]): string => {
299+
export const getHumanReadableScopes = (scopes?: EnvelopeEnvVarScope[]): string => {
247300
const HUMAN_SCOPES = ['Builds', 'Functions', 'Runtime', 'Post processing']
248301
const SCOPES_MAP = {
249302
builds: HUMAN_SCOPES[0],
250303
functions: HUMAN_SCOPES[1],
251304
runtime: HUMAN_SCOPES[2],
252305
post_processing: HUMAN_SCOPES[3],
306+
// TODO(serhalp) I believe this isn't needed, as `post-processing` is a user-provided
307+
// CLI option, not a scope returned by the Envelope API.
253308
'post-processing': HUMAN_SCOPES[3],
254309
}
255310
if (!scopes) {
@@ -272,10 +327,10 @@ export const getHumanReadableScopes = (scopes?: (EnvironmentVariableScope | 'pos
272327
export const translateFromMongoToEnvelope = (env: Record<string, string> = {}) => {
273328
const envVars = Object.entries(env).map(([key, value]) => ({
274329
key,
275-
scopes: AVAILABLE_SCOPES,
330+
scopes: ALL_ENVELOPE_SCOPES,
276331
values: [
277332
{
278-
context: 'all',
333+
context: 'all' as const,
279334
value,
280335
},
281336
],

0 commit comments

Comments
 (0)