diff --git a/.changeset/khaki-moose-sing.md b/.changeset/khaki-moose-sing.md new file mode 100644 index 0000000000..8beb8aeeb9 --- /dev/null +++ b/.changeset/khaki-moose-sing.md @@ -0,0 +1,5 @@ +--- +'@directus/env': minor +--- + +Fixed an issue that would prevent prefix-based casted values in environment variables not to be extracted properly. diff --git a/packages/env/src/lib/cast.test.ts b/packages/env/src/lib/cast.test.ts index 7bbf276846..5a954de98a 100644 --- a/packages/env/src/lib/cast.test.ts +++ b/packages/env/src/lib/cast.test.ts @@ -22,17 +22,39 @@ describe('Type extraction', () => { test('Uses cast flag if exists', () => { vi.mocked(getCastFlag).mockReturnValue('string'); - cast('key', 'value'); + cast('string:value', 'key'); - expect(getCastFlag).toHaveBeenCalledWith('value'); + expect(getCastFlag).toHaveBeenCalledWith('string:value'); expect(toString).toHaveBeenCalledWith('value'); }); + test('Uses cast flag for array with nested cast flags if exists', () => { + vi.mocked(getCastFlag).mockImplementation((val) => { + if (String(val).startsWith('array')) return 'array'; + if (String(val).startsWith('string')) return 'string'; + return 'number'; + }); + + vi.mocked(toString).mockReturnValue('hey'); + vi.mocked(toNumber).mockReturnValue(1); + vi.mocked(toArray).mockReturnValue(['string:hey', 'number:1']); + + const res = cast('array:string:hey,number:1', 'key'); + + expect(getCastFlag).toHaveBeenNthCalledWith(1, 'array:string:hey,number:1'); + expect(getCastFlag).toHaveBeenCalledWith('string:hey'); + expect(getCastFlag).toHaveBeenCalledWith('number:1'); + expect(toString).toHaveBeenCalledWith('hey'); + expect(toArray).toHaveBeenCalledWith('string:hey,number:1'); + expect(toNumber).toHaveBeenCalledWith('1'); + expect(res).toEqual(['hey', 1]); + }); + test('Uses type map entry if cast flag does not exist', () => { vi.mocked(getCastFlag).mockReturnValue(null); vi.mocked(getTypeFromMap).mockReturnValue('string'); - cast('key', 'value'); + cast('value', 'key'); expect(getTypeFromMap).toHaveBeenCalledWith('key'); expect(toString).toHaveBeenCalledWith('value'); @@ -43,7 +65,7 @@ describe('Type extraction', () => { vi.mocked(getTypeFromMap).mockReturnValue(null); vi.mocked(guessType).mockReturnValue('string'); - cast('key', 'value'); + cast('value', 'key'); expect(guessType).toHaveBeenCalledWith('value'); expect(toString).toHaveBeenCalledWith('value'); @@ -55,39 +77,45 @@ describe('Casting', () => { vi.mocked(getCastFlag).mockReturnValue('string'); vi.mocked(toString).mockReturnValue('cast-value'); - expect(cast('key', 'value')).toBe('cast-value'); + expect(cast('value')).toBe('cast-value', 'key'); }); test('Uses toNumber for number types', () => { vi.mocked(getCastFlag).mockReturnValue('number'); vi.mocked(toNumber).mockReturnValue(123); - expect(cast('key', 'value')).toBe(123); + expect(cast('value')).toBe(123, 'key'); }); test('Uses toBoolean for number types', () => { vi.mocked(getCastFlag).mockReturnValue('boolean'); vi.mocked(toBoolean).mockReturnValue(false); - expect(cast('key', 'value')).toBe(false); + expect(cast('value')).toBe(false, 'key'); }); test('Uses RegExp for regex types', () => { vi.mocked(getCastFlag).mockReturnValue('regex'); - expect(cast('key', 'value')).toBeInstanceOf(RegExp); + expect(cast('value')).toBeInstanceOf(RegExp, 'key'); }); test('Uses toArray for array types', () => { - vi.mocked(getCastFlag).mockReturnValue('array'); + vi.mocked(getCastFlag).mockImplementation((v) => { + if (String(v).startsWith('array')) return 'array'; + return null; + }); + vi.mocked(guessType).mockReturnValue('number'); + vi.mocked(toNumber).mockImplementation((v) => v); vi.mocked(toArray).mockReturnValue([1, 2, 3]); - expect(cast('key', 'value')).toEqual([1, 2, 3]); + + expect(cast('array:value')).toEqual([1, 2, 3], 'key'); }); test('Uses tryJson for json types', () => { vi.mocked(getCastFlag).mockReturnValue('json'); vi.mocked(tryJson).mockReturnValue('cast-value'); - expect(cast('key', 'value')).toBe('cast-value'); + expect(cast('value')).toBe('cast-value', 'key'); }); }); diff --git a/packages/env/src/lib/cast.ts b/packages/env/src/lib/cast.ts index f6179c54fa..8a8b624942 100644 --- a/packages/env/src/lib/cast.ts +++ b/packages/env/src/lib/cast.ts @@ -5,8 +5,13 @@ import { guessType } from '../utils/guess-type.js'; import { getCastFlag } from '../utils/has-cast-prefix.js'; import { tryJson } from '../utils/try-json.js'; -export const cast = (key: string, value: unknown) => { - const type = getCastFlag(value) ?? getTypeFromMap(key) ?? guessType(value); +export const cast = (value: unknown, key?: string): unknown => { + const castFlag = getCastFlag(value); + const type = castFlag ?? getTypeFromMap(key) ?? guessType(value); + + if (typeof value === 'string' && castFlag) { + value = value.substring(castFlag.length + 1); // Type length + 1 for `:` character + } switch (type) { case 'string': @@ -18,7 +23,7 @@ export const cast = (key: string, value: unknown) => { case 'regex': return new RegExp(String(value)); case 'array': - return toArray(value); + return toArray(value).map((v) => cast(v)); case 'json': return tryJson(value); } diff --git a/packages/env/src/lib/create-env.test.ts b/packages/env/src/lib/create-env.test.ts index 4392533f83..d7b1508fe0 100644 --- a/packages/env/src/lib/create-env.test.ts +++ b/packages/env/src/lib/create-env.test.ts @@ -28,7 +28,7 @@ let processConfig: Record; let fileConfig: Record; beforeEach(() => { - vi.mocked(cast).mockImplementation((_key, value) => value); + vi.mocked(cast).mockImplementation((value) => value); processConfig = { PROCESS: 'test-process' }; fileConfig = { FILE: 'test-file' }; @@ -117,7 +117,7 @@ test('Throws error if file could not be read', () => { }); test('Casts regular values', () => { - vi.mocked(cast).mockImplementation((_key, value) => `cast-${value}`); + vi.mocked(cast).mockImplementation((value) => `cast-${value}`); const env = createEnv(); diff --git a/packages/env/src/lib/create-env.ts b/packages/env/src/lib/create-env.ts index 23ef00378c..6df077093b 100644 --- a/packages/env/src/lib/create-env.ts +++ b/packages/env/src/lib/create-env.ts @@ -27,7 +27,7 @@ export const createEnv = (): Env => { throw new Error(`Failed to read value from file "${value}", defined in environment variable "${key}".`); } } else { - output[key] = cast(key, value); + output[key] = cast(value, key); } } diff --git a/packages/env/src/utils/get-type-from-map.test.ts b/packages/env/src/utils/get-type-from-map.test.ts index 80c2381479..74ae18e416 100644 --- a/packages/env/src/utils/get-type-from-map.test.ts +++ b/packages/env/src/utils/get-type-from-map.test.ts @@ -20,3 +20,8 @@ test('Returns null if key does not exist', () => { const res = getTypeFromMap('non-existing'); expect(res).toBe(null); }); + +test('Returns null if key is undefined', () => { + const res = getTypeFromMap(undefined); + expect(res).toBe(null); +}); diff --git a/packages/env/src/utils/get-type-from-map.ts b/packages/env/src/utils/get-type-from-map.ts index e624e53bed..65d309db77 100644 --- a/packages/env/src/utils/get-type-from-map.ts +++ b/packages/env/src/utils/get-type-from-map.ts @@ -1,7 +1,9 @@ import { TYPE_MAP } from '../constants/type-map.js'; import type { EnvType } from '../types/env-type.js'; -export const getTypeFromMap = (key: string): EnvType | null => { +export const getTypeFromMap = (key: string | undefined): EnvType | null => { + if (!key) return null; + const type = TYPE_MAP[key]; if (type !== undefined) {