@achs/env 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc.json +329 -0
  3. package/.vscode/extensions.json +18 -0
  4. package/.vscode/launch.json +30 -0
  5. package/.vscode/settings.json +29 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +14 -7
  8. package/jest.config.json +28 -0
  9. package/package.json +16 -16
  10. package/{arguments.js → src/arguments.ts} +38 -14
  11. package/src/commands/env.command.ts +139 -0
  12. package/src/commands/export.command.ts +88 -0
  13. package/{commands/index.d.ts → src/commands/index.ts} +0 -1
  14. package/src/commands/pull.command.ts +52 -0
  15. package/src/commands/push.command.ts +48 -0
  16. package/src/commands/schema.command.ts +31 -0
  17. package/src/exec.ts +221 -0
  18. package/{index.d.ts → src/index.ts} +0 -1
  19. package/{interfaces/index.d.ts → src/interfaces/index.ts} +0 -1
  20. package/src/interfaces/loader.interface.ts +66 -0
  21. package/src/main.ts +6 -0
  22. package/src/providers/app-settings.provider.ts +67 -0
  23. package/src/providers/azure-key-vault.provider.ts +277 -0
  24. package/src/providers/index.ts +29 -0
  25. package/src/providers/local.provider.ts +44 -0
  26. package/src/providers/package-json.provider.ts +39 -0
  27. package/src/utils/command.util.ts +223 -0
  28. package/{utils/index.d.ts → src/utils/index.ts} +0 -1
  29. package/src/utils/interpolate.util.ts +65 -0
  30. package/src/utils/json.util.ts +116 -0
  31. package/{utils/logger.js → src/utils/logger.ts} +6 -6
  32. package/src/utils/normalize.util.ts +142 -0
  33. package/src/utils/schema.util.ts +191 -0
  34. package/tests/env/appsettings.json +32 -0
  35. package/tests/env/dev.env.json +12 -0
  36. package/tests/env/dev.local.env.json +9 -0
  37. package/tests/env/env.schema.json +225 -0
  38. package/tests/env/keys.json +7 -0
  39. package/tests/env/settings/schema.json +239 -0
  40. package/tests/env/settings/settings.json +22 -0
  41. package/tests/env.int.test.ts +42 -0
  42. package/tests/exec.ts +19 -0
  43. package/tests/export.int.test.ts +9 -0
  44. package/tests/pull-push.int.test.ts +15 -0
  45. package/tests/run.js +32 -0
  46. package/tests/schema.int.test.ts +9 -0
  47. package/tests/setup.ts +13 -0
  48. package/tsconfig.build.json +10 -0
  49. package/tsconfig.json +37 -0
  50. package/arguments.d.ts +0 -25
  51. package/arguments.d.ts.map +0 -1
  52. package/arguments.js.map +0 -1
  53. package/commands/env.command.d.ts +0 -8
  54. package/commands/env.command.d.ts.map +0 -1
  55. package/commands/env.command.js +0 -85
  56. package/commands/env.command.js.map +0 -1
  57. package/commands/export.command.d.ts +0 -8
  58. package/commands/export.command.d.ts.map +0 -1
  59. package/commands/export.command.js +0 -54
  60. package/commands/export.command.js.map +0 -1
  61. package/commands/index.d.ts.map +0 -1
  62. package/commands/index.js +0 -14
  63. package/commands/index.js.map +0 -1
  64. package/commands/pull.command.d.ts +0 -7
  65. package/commands/pull.command.d.ts.map +0 -1
  66. package/commands/pull.command.js +0 -39
  67. package/commands/pull.command.js.map +0 -1
  68. package/commands/push.command.d.ts +0 -7
  69. package/commands/push.command.d.ts.map +0 -1
  70. package/commands/push.command.js +0 -38
  71. package/commands/push.command.js.map +0 -1
  72. package/commands/schema.command.d.ts +0 -4
  73. package/commands/schema.command.d.ts.map +0 -1
  74. package/commands/schema.command.js +0 -18
  75. package/commands/schema.command.js.map +0 -1
  76. package/exec.d.ts +0 -3
  77. package/exec.d.ts.map +0 -1
  78. package/exec.js +0 -142
  79. package/exec.js.map +0 -1
  80. package/index.d.ts.map +0 -1
  81. package/index.js +0 -20
  82. package/index.js.map +0 -1
  83. package/interfaces/index.d.ts.map +0 -1
  84. package/interfaces/index.js +0 -18
  85. package/interfaces/index.js.map +0 -1
  86. package/interfaces/loader.interface.d.ts +0 -21
  87. package/interfaces/loader.interface.d.ts.map +0 -1
  88. package/interfaces/loader.interface.js +0 -3
  89. package/interfaces/loader.interface.js.map +0 -1
  90. package/main.d.ts +0 -3
  91. package/main.d.ts.map +0 -1
  92. package/main.js +0 -6
  93. package/main.js.map +0 -1
  94. package/providers/app-settings.provider.d.ts +0 -8
  95. package/providers/app-settings.provider.d.ts.map +0 -1
  96. package/providers/app-settings.provider.js +0 -50
  97. package/providers/app-settings.provider.js.map +0 -1
  98. package/providers/azure-key-vault.provider.d.ts +0 -22
  99. package/providers/azure-key-vault.provider.d.ts.map +0 -1
  100. package/providers/azure-key-vault.provider.js +0 -164
  101. package/providers/azure-key-vault.provider.js.map +0 -1
  102. package/providers/index.d.ts +0 -8
  103. package/providers/index.d.ts.map +0 -1
  104. package/providers/index.js +0 -28
  105. package/providers/index.js.map +0 -1
  106. package/providers/package-json.provider.d.ts +0 -8
  107. package/providers/package-json.provider.d.ts.map +0 -1
  108. package/providers/package-json.provider.js +0 -29
  109. package/providers/package-json.provider.js.map +0 -1
  110. package/tsconfig.build.tsbuildinfo +0 -1
  111. package/utils/command.util.d.ts +0 -13
  112. package/utils/command.util.d.ts.map +0 -1
  113. package/utils/command.util.js +0 -134
  114. package/utils/command.util.js.map +0 -1
  115. package/utils/index.d.ts.map +0 -1
  116. package/utils/index.js +0 -23
  117. package/utils/index.js.map +0 -1
  118. package/utils/interpolate.util.d.ts +0 -4
  119. package/utils/interpolate.util.d.ts.map +0 -1
  120. package/utils/interpolate.util.js +0 -33
  121. package/utils/interpolate.util.js.map +0 -1
  122. package/utils/json.util.d.ts +0 -5
  123. package/utils/json.util.d.ts.map +0 -1
  124. package/utils/json.util.js +0 -48
  125. package/utils/json.util.js.map +0 -1
  126. package/utils/logger.d.ts +0 -3
  127. package/utils/logger.d.ts.map +0 -1
  128. package/utils/logger.js.map +0 -1
  129. package/utils/normalize.util.d.ts +0 -3
  130. package/utils/normalize.util.d.ts.map +0 -1
  131. package/utils/normalize.util.js +0 -61
  132. package/utils/normalize.util.js.map +0 -1
  133. package/utils/schema.util.d.ts +0 -11
  134. package/utils/schema.util.d.ts.map +0 -1
  135. package/utils/schema.util.js +0 -100
  136. package/utils/schema.util.js.map +0 -1
@@ -0,0 +1,223 @@
1
+ import chalk from 'chalk';
2
+ import merge from 'merge-deep';
3
+ import { Arguments } from 'yargs';
4
+ import { CommandArguments } from 'arguments';
5
+ import { EnvCommandArguments } from 'commands/env.command';
6
+ import { EnvProviderConfig, EnvProviderResult } from '../interfaces';
7
+ import {
8
+ createValidators,
9
+ interpolate,
10
+ logger,
11
+ readJson,
12
+ schemaFrom,
13
+ writeJson
14
+ } from '../utils';
15
+
16
+ /**
17
+ * Injects config to command arguments from file.
18
+ *
19
+ * @param {Record<string, unknown>} argv
20
+ * @param {[string, string]} delimiters
21
+ */
22
+ export async function loadConfigFile(
23
+ argv: Record<string, unknown>,
24
+ delimiters: [string, string]
25
+ ): Promise<void> {
26
+ if (typeof argv.configFile === 'string') {
27
+ const path = interpolate(argv.configFile, argv, delimiters);
28
+ const [config, success] = await readJson(path);
29
+
30
+ if (success) for (const key in config) argv[key] ??= config[key];
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Extracts subcommand from command line parameters.
36
+ *
37
+ * @export
38
+ * @param {string[]} rawArgv process.argv.slice(2)
39
+ * @param {[string, string]} delimiters
40
+ *
41
+ * @returns {string[]} subcommand for wrap if exists
42
+ */
43
+ export function getSubcommand(rawArgv: string[], delimiters: [string, string]) {
44
+ let subcommand: string[] = [];
45
+
46
+ // subcommand delimiter indexes
47
+ const begin = rawArgv.indexOf(delimiters[0]);
48
+ const count = rawArgv.lastIndexOf(delimiters[1]) - begin;
49
+
50
+ // calculates subcommand surrounded by delimiters
51
+ if (begin > 0) {
52
+ subcommand =
53
+ count > 0
54
+ ? rawArgv.splice(begin, count + 1).slice(1, -1)
55
+ : rawArgv.splice(begin).slice(1);
56
+ }
57
+
58
+ return subcommand;
59
+ }
60
+
61
+ /**
62
+ * Loads providers JSON schema from file.
63
+ *
64
+ * @param {Record<string, unknown>} argv
65
+ * @param {[string, string]} delimiters
66
+ *
67
+ * @returns {Promise<Record<string, unknown>>}
68
+ */
69
+ export async function loadSchemaFile(
70
+ argv: Record<string, unknown>,
71
+ delimiters: [string, string]
72
+ ): Promise<Record<string, unknown> | undefined> {
73
+ if (typeof argv.schemaFile === 'string') {
74
+ const path = interpolate(argv.schemaFile, argv, delimiters);
75
+ const [schema, success] = await readJson(path);
76
+
77
+ return success ? schema : undefined;
78
+ }
79
+
80
+ return undefined;
81
+ }
82
+
83
+ /**
84
+ * Reads project package.json.
85
+ *
86
+ * @export
87
+ * @returns {Promise<Record<string, unknown>> | never}
88
+ */
89
+ export async function loadProjectInfo(): Promise<
90
+ Record<string, unknown> | undefined
91
+ > {
92
+ try {
93
+ return await import(`${process.cwd()}/package.json`);
94
+ } catch {
95
+ logger.warn(
96
+ `project file ${chalk.underline.yellow('package.json')} not found`
97
+ );
98
+
99
+ return undefined;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Executes load functions from provider handlers.
105
+ *
106
+ * @param {EnvProviderConfig[]} providers
107
+ * @param {Partial<Arguments<EnvCommandArguments>>} argv
108
+ *
109
+ * @returns {EnvProviderResult[]}
110
+ */
111
+ export function loadVariablesFromProviders(
112
+ providers: EnvProviderConfig[],
113
+ argv: Partial<Arguments<EnvCommandArguments>>
114
+ ): Promise<EnvProviderResult[]> {
115
+ if (!providers) return Promise.resolve([]) as Promise<EnvProviderResult[]>;
116
+
117
+ return Promise.all(
118
+ providers.map(({ handler: { key, load }, config }) => {
119
+ logger.silly(`executing ${chalk.yellow(key)} provider`);
120
+
121
+ const value = load(argv, config);
122
+
123
+ if (value instanceof Promise) {
124
+ return value.then((value) => ({
125
+ key,
126
+ config,
127
+ value
128
+ }));
129
+ } else {
130
+ return { key, config, value };
131
+ }
132
+ })
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Flattern environment provider results.
138
+ *
139
+ * @param {EnvProviderResult[]} results
140
+ * @param {Partial<Arguments<EnvCommandArguments>>} argv
141
+ *
142
+ * @throws {Error} on schema validation failed
143
+ *
144
+ * @returns {EnvProviderResult[]} flatten results
145
+ */
146
+ export function flatResults(
147
+ results: EnvProviderResult[]
148
+ ): EnvProviderResult[] | never {
149
+ return results.flatMap(({ value }) => {
150
+ if (Array.isArray(value)) value = merge({}, ...value);
151
+
152
+ return value;
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Flattern and validates environment provider results.
158
+ *
159
+ * @param {EnvProviderResult[]} results
160
+ * @param {Partial<Arguments<EnvCommandArguments>>} argv
161
+ *
162
+ * @throws {Error} on schema validation failed
163
+ *
164
+ * @returns {EnvProviderResult[]}
165
+ */
166
+ export function flatAndValidateResults(
167
+ results: EnvProviderResult[],
168
+ argv: Partial<Arguments<EnvCommandArguments>>
169
+ ): EnvProviderResult[] | never {
170
+ if (!argv.schemaValidate) return flatResults(results);
171
+
172
+ const validators = createValidators(argv.schema!, argv.detectFormat);
173
+
174
+ return results.flatMap(({ key, value }) => {
175
+ if (Array.isArray(value)) value = merge({}, ...value);
176
+
177
+ const validator = validators![key];
178
+
179
+ if (validator(value)) return value;
180
+
181
+ logger.error(
182
+ `schema validation failed for ${chalk.yellow(key)}`,
183
+ validator.errors
184
+ );
185
+
186
+ throw new Error(`schema validation failed for ${key}`);
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Creates or updates JSON schema from
192
+ * environment variables grouped by provider key.
193
+ *
194
+ * @export
195
+ * @param {EnvProviderResult[]} env
196
+ * @param {Arguments<EnvCommandArguments>} argv
197
+ *
198
+ * @returns {Promise<object>} JSON schema grouped by provider key.
199
+ */
200
+ export async function generateSchemaFrom(
201
+ env: EnvProviderResult[],
202
+ argv: Arguments<CommandArguments>
203
+ ): Promise<object> {
204
+ const { resolve, nullable, detectFormat, schemaFile } = argv;
205
+
206
+ // generates schemas from proviers results
207
+ let schema = env.reduce((schema, { key, value }) => {
208
+ const env = Array.isArray(value) ? merge({}, ...value) : value;
209
+
210
+ schema[key] = schemaFrom(env, {
211
+ nullable,
212
+ strings: { detectFormat }
213
+ });
214
+
215
+ return schema;
216
+ }, {} as Record<string, unknown>);
217
+
218
+ if (resolve === 'merge') schema = merge(argv.schema, schema);
219
+
220
+ await writeJson(schemaFile, schema, true);
221
+
222
+ return schema;
223
+ }
@@ -4,4 +4,3 @@ export * from './json.util';
4
4
  export * from './logger';
5
5
  export * from './normalize.util';
6
6
  export * from './schema.util';
7
- //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,65 @@
1
+ import subslate from 'subslate';
2
+
3
+ /**
4
+ * Validates value must be a record.
5
+ *
6
+ * @export
7
+ * @param {unknown} obj
8
+ * @returns {*} {obj is Record<string, unknown>}
9
+ */
10
+ export function isRecord(obj: unknown): obj is Record<string, unknown> {
11
+ if (!obj || typeof obj !== 'object') return false;
12
+
13
+ return Object.keys(obj).length > 0;
14
+ }
15
+
16
+ /**
17
+ * Replaces string arguments with regex interpolation.
18
+ *
19
+ * @export
20
+ * @template T
21
+ * @param {T} value
22
+ * @param {Record<string, unknown>} args
23
+ * @param {[string, string]} [delimiters=['[[', ']]']]
24
+ *
25
+ * @returns {T} mutated value
26
+ */
27
+ export function interpolate<T extends string | unknown>(
28
+ value: T,
29
+ args: Record<string, unknown>,
30
+ delimiters: [string, string] = ['[[', ']]']
31
+ ): T {
32
+ if (typeof value === 'string') {
33
+ return subslate(value, args, {
34
+ startStopPairs: delimiters
35
+ }) as T;
36
+ }
37
+
38
+ if (Array.isArray(value))
39
+ return value.map((a) => interpolate(a, args, delimiters)) as T;
40
+
41
+ if (isRecord(value)) return interpolateJson(value, args, delimiters) as T;
42
+
43
+ return value;
44
+ }
45
+
46
+ /**
47
+ * Replaces JSON string arguments with regex interpolation.
48
+ *
49
+ * @export
50
+ * @param {Record<string, unknown>} args
51
+ * @param {Record<string, unknown>} values
52
+ * @param {[string, string]} [delimiters=['[[', ']]']]
53
+ *
54
+ * @returns {Record<string, unknown>} mutated args
55
+ */
56
+ export function interpolateJson(
57
+ values: Record<string, unknown>,
58
+ args: Record<string, unknown>,
59
+ delimiters: [string, string] = ['[[', ']]']
60
+ ): Record<string, unknown> {
61
+ for (const key in values)
62
+ values[key] = interpolate(values[key], args, delimiters);
63
+
64
+ return values;
65
+ }
@@ -0,0 +1,116 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { readFile, writeFile } from 'fs/promises';
5
+
6
+ /**
7
+ * Replaces undefined by null in JSON.stringify()
8
+ *
9
+ * @param {string} _ property key
10
+ * @param {any} value property value
11
+ *
12
+ * @returns {any} value
13
+ */
14
+ const replacer = (_: string, value: any): typeof value | null =>
15
+ value === undefined ? null : value;
16
+
17
+ /**
18
+ * Resolve a relative path for os.
19
+ *
20
+ * @export
21
+ * @param {string} filePath relative path from project root
22
+ *
23
+ * @returns {string} path
24
+ */
25
+ export function resolvePath(filePath: string): string {
26
+ const home = os.homedir();
27
+
28
+ if (home !== undefined)
29
+ filePath = filePath.replace(/^~($|\/|\\)/, `${home}$1`);
30
+
31
+ return path.resolve(process.cwd(), filePath);
32
+ }
33
+
34
+ /**
35
+ * Reads and parses a JSON file.
36
+ *
37
+ * @export
38
+ * @template T
39
+ * @param {string} path
40
+ *
41
+ * @returns {Promise<[Record<string, any>, boolean]>}
42
+ */
43
+ export async function readJson<T = Record<string, any>>(
44
+ path: string
45
+ ): Promise<[T | Record<string, any>, boolean] | never> {
46
+ if (!existsSync(path)) return [{}, false];
47
+
48
+ return [JSON.parse(await readFile(path, 'utf8')), true];
49
+ }
50
+
51
+ /**
52
+ * Saves a JSON into a file.
53
+ *
54
+ * @export
55
+ * @param {string} path
56
+ * @param {unknown} content
57
+ * @param {false} overwrite
58
+ * @param {false} undefinedAsNull replaces undefined by null
59
+ *
60
+ * @returns {Promise<boolean>}
61
+ */
62
+ export async function writeJson(
63
+ path: string,
64
+ content: Record<string, unknown>,
65
+ overwrite = false,
66
+ undefinedAsNull = false
67
+ ): Promise<boolean | never> {
68
+ const exists = existsSync(path);
69
+
70
+ if (exists && !overwrite) return false;
71
+
72
+ await writeFile(
73
+ path,
74
+ `${JSON.stringify(
75
+ content,
76
+ undefinedAsNull ? replacer : undefined,
77
+ 4
78
+ )}\n`,
79
+ 'utf8'
80
+ );
81
+
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Saves a JSON into a file as dotenv.
87
+ *
88
+ * @export
89
+ * @param {string} path
90
+ * @param {unknown} content
91
+ * @param {false} overwrite
92
+ *
93
+ * @returns {Promise<boolean>}
94
+ */
95
+ export async function writeEnvFromJson(
96
+ path: string,
97
+ content: Record<string, unknown>,
98
+ overwrite = false
99
+ ): Promise<boolean | never> {
100
+ const exists = existsSync(path);
101
+
102
+ if (exists && !overwrite) return false;
103
+
104
+ let data = '';
105
+
106
+ for (const key in content) {
107
+ let value = content[key];
108
+ if (typeof value === 'string') value = `"${value}"`;
109
+
110
+ data += `${key}=${value}\n`;
111
+ }
112
+
113
+ await writeFile(path, data, 'utf8');
114
+
115
+ return true;
116
+ }
@@ -1,8 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.logger = void 0;
4
- const tslog_1 = require("tslog");
5
- exports.logger = new tslog_1.LoggerWithoutCallSite({
1
+ import { LoggerWithoutCallSite as Logger } from 'tslog';
2
+
3
+ /**
4
+ * Global stdout wrap.
5
+ */
6
+ export const logger = new Logger({
6
7
  displayDateTime: true,
7
8
  displayLoggerName: false,
8
9
  displayInstanceName: false,
@@ -15,4 +16,3 @@ exports.logger = new tslog_1.LoggerWithoutCallSite({
15
16
  dateTimePattern: 'hour:minute:second.millisecond',
16
17
  maskPlaceholder: '***'
17
18
  });
18
- //# sourceMappingURL=logger.js.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Flatten a object keeping depth path
3
+ * in key using __ as level separator.
4
+ *
5
+ * @param {Record<string, any>} obj
6
+ * @param {string} nestingDelimiter char for delimit nesting levels
7
+ * @param {boolean} arrayDescomposition serialize or break down arrays
8
+ * @param {boolean} clearNull if null should removed from object
9
+ * @param {string} pkey first level key
10
+ *
11
+ * @returns {Record<string, string>} flattended object
12
+ */
13
+ export function flatten(
14
+ obj: Record<string, any>,
15
+ nestingDelimiter = '__',
16
+ pkey = ''
17
+ ): Record<string, string> {
18
+ const flattened: Record<string, string> = {};
19
+
20
+ for (let key in obj) {
21
+ const value = obj[key];
22
+ const type = typeof value;
23
+
24
+ if (value === undefined || type === 'function') continue;
25
+
26
+ // skipped property
27
+ if (key[0] === '#') continue;
28
+ key = pkey + key;
29
+
30
+ if (value === null || type !== 'object' || Array.isArray(value)) {
31
+ flattened[key] = value;
32
+
33
+ continue;
34
+ }
35
+
36
+ Object.assign(
37
+ flattened,
38
+ flatten(value, nestingDelimiter, `${key}${nestingDelimiter}`)
39
+ );
40
+ }
41
+
42
+ return flattened;
43
+ }
44
+
45
+ /**
46
+ * Normalizes env object, converts arrays in list strings,
47
+ * only primitives types array,
48
+ * and removes $ global character from keys.
49
+ *
50
+ * @param {Record<string, any>} obj
51
+ * @param {string} nestingDelimiter char for delimit nesting levels
52
+ * @param {boolean} arrayDescomposition serialize or break down arrays
53
+ * @param {string} pkey first level key
54
+ *
55
+ * @returns {Record<string, string>} normalized object
56
+ */
57
+ export function normalize(
58
+ obj: Record<string, any>,
59
+ nestingDelimiter = '__',
60
+ arrayDescomposition = false,
61
+ pkey = ''
62
+ ): Record<string, string> {
63
+ const flattened: Record<string, string> = {};
64
+
65
+ for (let key in obj) {
66
+ const value = obj[key];
67
+ const type = typeof value;
68
+
69
+ if (value === null || value === undefined || type === 'function')
70
+ continue;
71
+
72
+ // global property, but prefix removed for injection
73
+ key = pkey + key.replace('$', '');
74
+
75
+ if (type !== 'object') {
76
+ flattened[key] = value;
77
+
78
+ continue;
79
+ }
80
+
81
+ if (Array.isArray(value)) {
82
+ normalizeArray(
83
+ flattened,
84
+ key,
85
+ value,
86
+ nestingDelimiter,
87
+ arrayDescomposition
88
+ );
89
+ } else {
90
+ Object.assign(
91
+ flattened,
92
+ normalize(
93
+ value,
94
+ nestingDelimiter,
95
+ arrayDescomposition,
96
+ `${key}${nestingDelimiter}`
97
+ )
98
+ );
99
+ }
100
+ }
101
+
102
+ return flattened;
103
+ }
104
+
105
+ /**
106
+ * Flatten and normalizes an array.
107
+ *
108
+ * @param {Record<string, string>} flattened
109
+ * @param {string} key
110
+ * @param {any[]} value
111
+ * @param {string} [nestingDelimiter='__']
112
+ * @param {boolean} [arrayDescomposition=false]
113
+ */
114
+ function normalizeArray(
115
+ flattened: Record<string, string>,
116
+ key: string,
117
+ value: any[],
118
+ nestingDelimiter = '__',
119
+ arrayDescomposition = false
120
+ ): void {
121
+ if (arrayDescomposition) {
122
+ key = `${key}${nestingDelimiter}`;
123
+
124
+ for (let i = 0; i < value.length; i++) {
125
+ if (typeof value[i] === 'object') {
126
+ Object.assign(
127
+ flattened,
128
+ normalize(
129
+ value[i],
130
+ nestingDelimiter,
131
+ arrayDescomposition,
132
+ `${key}${i}${nestingDelimiter}`
133
+ )
134
+ );
135
+ } else {
136
+ flattened[`${key}${i}`] = value[i];
137
+ }
138
+ }
139
+ } else {
140
+ flattened[key] = value.filter((v) => typeof v !== 'object').join(',');
141
+ }
142
+ }