|
| 1 | +import chalk from 'chalk'; |
| 2 | +import { ParameterOperation, PlanResponseData, ResourceOperation } from 'codify-schemas'; |
| 3 | + |
| 4 | +export function prettyFormatPlans(plans: PlanResponseData[]) { |
| 5 | + const builder = [ |
| 6 | + '', |
| 7 | + '', |
| 8 | + chalk.bold('Codify Plan'), |
| 9 | + 'The following actions will be performed', |
| 10 | + '', |
| 11 | + ]; |
| 12 | + |
| 13 | + plans.forEach((plan: PlanResponseData) => { |
| 14 | + const formattedPlan = prettyFormatPlan(plan); |
| 15 | + |
| 16 | + builder.push(chalk.bold(plan.resourceType + (plan.resourceName ? `.${plan.resourceName}` : '')) + ' will ' + resourceOperationText(plan.operation)); |
| 17 | + builder.push(formattedPlan) |
| 18 | + }) |
| 19 | + |
| 20 | + return builder.join('\n') |
| 21 | +} |
| 22 | + |
| 23 | +export function prettyFormatPlan(plan: PlanResponseData): string { |
| 24 | + switch (plan.operation) { |
| 25 | + case ResourceOperation.CREATE: { |
| 26 | + return prettyFormatCreatePlan(plan); |
| 27 | + } |
| 28 | + |
| 29 | + case ResourceOperation.DESTROY: { |
| 30 | + return prettyFormatDestroyPlan(plan); |
| 31 | + } |
| 32 | + |
| 33 | + case ResourceOperation.MODIFY: |
| 34 | + case ResourceOperation.RECREATE: { |
| 35 | + return prettyFormatModifyPlan(plan); |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + return ''; |
| 40 | +} |
| 41 | + |
| 42 | +function prettyFormatCreatePlan(plan: PlanResponseData): string { |
| 43 | + const parameters = plan.parameters |
| 44 | + .reduce((result, parameter) => { |
| 45 | + if (parameter.newValue === null || parameter.newValue === undefined) { |
| 46 | + return result; |
| 47 | + } |
| 48 | + |
| 49 | + result[parameter.name] = typeof parameter.newValue === 'string' |
| 50 | + ? escapeNewlines(parameter.newValue) |
| 51 | + : parameter.newValue; |
| 52 | + |
| 53 | + return result; |
| 54 | + }, {} as Record<string, unknown>) |
| 55 | + |
| 56 | + const json = JSON.stringify(parameters, null, 4) |
| 57 | + .split(/\n/g) |
| 58 | + .map((l) => ` ${l}`) |
| 59 | + .join('\n') |
| 60 | + return chalk.green(json); |
| 61 | +} |
| 62 | + |
| 63 | +function prettyFormatDestroyPlan(plan: PlanResponseData): string { |
| 64 | + const parameters = plan.parameters |
| 65 | + .reduce((result, parameter) => { |
| 66 | + if (parameter.previousValue === null || parameter.previousValue === undefined) { |
| 67 | + return result; |
| 68 | + } |
| 69 | + |
| 70 | + result[parameter.name] = typeof parameter.previousValue === 'string' |
| 71 | + ? escapeNewlines(parameter.previousValue) |
| 72 | + : parameter.previousValue; |
| 73 | + |
| 74 | + return result; |
| 75 | + }, {} as Record<string, unknown>) |
| 76 | + |
| 77 | + const json = JSON.stringify(parameters, null, 4) |
| 78 | + .split(/\n/g) |
| 79 | + .map((l) => ` ${l}`) |
| 80 | + .join('\n') |
| 81 | + return chalk.red(json); |
| 82 | +} |
| 83 | + |
| 84 | +function prettyFormatModifyPlan(plan: PlanResponseData): string { |
| 85 | + const builder = [ |
| 86 | + ' {' |
| 87 | + ]; |
| 88 | + |
| 89 | + for (const parameter of plan.parameters) { |
| 90 | + |
| 91 | + // TODO: Add support for object types as well in the future |
| 92 | + if ((Array.isArray(parameter.previousValue) || parameter.previousValue === null) |
| 93 | + && (Array.isArray(parameter.newValue) || parameter.newValue === null) |
| 94 | + && !(parameter.previousValue === null && parameter.newValue === null) |
| 95 | + ) { |
| 96 | + const line = formatArray(parameter); |
| 97 | + builder.push(line); |
| 98 | + } else { |
| 99 | + const formattedParameter = formatParameter(parameter); |
| 100 | + |
| 101 | + const line = formattedParameter.split(/\n/g) |
| 102 | + .map((l) => ` ${l}`) |
| 103 | + .map((l, idx) => idx === 0 ? operationSymbol(parameter.operation) + l : ` ${l}`) |
| 104 | + .join('\n') |
| 105 | + |
| 106 | + builder.push(line); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + builder.push(' }') |
| 111 | + return builder.join('\n'); |
| 112 | +} |
| 113 | + |
| 114 | +function escapeNewlines(str: string): string { |
| 115 | + return str.replaceAll('\n', '\\n'); |
| 116 | +} |
| 117 | + |
| 118 | + |
| 119 | +function formatParameter(parameter: PlanResponseData['parameters'][0]): string { |
| 120 | + switch (parameter.operation) { |
| 121 | + case ParameterOperation.NOOP: { |
| 122 | + return typeof parameter.newValue === 'string' |
| 123 | + ? `"${parameter.name}": "${escapeNewlines(parameter.newValue)}",` |
| 124 | + : `"${parameter.name}": ${parameter.newValue},` |
| 125 | + } |
| 126 | + |
| 127 | + case ParameterOperation.ADD: { |
| 128 | + return typeof parameter.newValue === 'string' |
| 129 | + ? chalk.green(`"${parameter.name}": "${escapeNewlines(parameter.newValue)}",`) |
| 130 | + : chalk.green(`"${parameter.name}": ${parameter.newValue},`) |
| 131 | + } |
| 132 | + |
| 133 | + case ParameterOperation.REMOVE: { |
| 134 | + return typeof parameter.previousValue === 'string' |
| 135 | + ? chalk.red(`"${parameter.name}": "${escapeNewlines(parameter.previousValue)}",`) |
| 136 | + : chalk.red(`"${parameter.name}": ${parameter.previousValue},`) |
| 137 | + } |
| 138 | + |
| 139 | + case ParameterOperation.MODIFY: { |
| 140 | + return typeof parameter.newValue === 'string' && typeof parameter.previousValue === 'string' |
| 141 | + ? `"${parameter.name}": "${escapeNewlines(parameter.previousValue)}" -> "${escapeNewlines(parameter.newValue)}",` |
| 142 | + : `"${parameter.name}": ${parameter.previousValue} -> ${parameter.newValue},` |
| 143 | + } |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +function resourceOperationText(operation: ResourceOperation): string { |
| 148 | + switch (operation) { |
| 149 | + case ResourceOperation.CREATE: |
| 150 | + return 'be created' |
| 151 | + case ResourceOperation.MODIFY: |
| 152 | + return 'be modified' |
| 153 | + case ResourceOperation.RECREATE: |
| 154 | + return 'be recreated' |
| 155 | + case ResourceOperation.DESTROY: |
| 156 | + return 'be destroyed' |
| 157 | + case ResourceOperation.NOOP: |
| 158 | + return 'not be changed' |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +function operationSymbol(operation: ParameterOperation): string { |
| 163 | + switch (operation) { |
| 164 | + case ParameterOperation.ADD: { |
| 165 | + return chalk.green('+') |
| 166 | + } |
| 167 | + |
| 168 | + case ParameterOperation.NOOP: { |
| 169 | + return ' ' |
| 170 | + } |
| 171 | + |
| 172 | + case ParameterOperation.MODIFY: { |
| 173 | + return chalk.yellow('~') |
| 174 | + } |
| 175 | + |
| 176 | + case ParameterOperation.REMOVE: { |
| 177 | + return chalk.red('-') |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +function formatArray(parameter: PlanResponseData['parameters'][0]): string { |
| 183 | + const { name, newValue, operation, previousValue } = parameter; |
| 184 | + const a = previousValue as null | unknown[]; |
| 185 | + const b = newValue as null | unknown[]; |
| 186 | + |
| 187 | + const mappedA = a?.map((l) => |
| 188 | + typeof l === 'object' ? JSON.stringify(l) : l |
| 189 | + ) ?? []; |
| 190 | + const mappedB = b?.map((l) => |
| 191 | + typeof l === 'object' ? JSON.stringify(l) : l |
| 192 | + ) ?? []; |
| 193 | + |
| 194 | + if (operation === ParameterOperation.ADD) { |
| 195 | + return JSON.stringify(mappedB, null, 4) |
| 196 | + .split(/\n/g) |
| 197 | + .map((l, idx) => idx === 0 ? `"${name}": ${l}` : l) |
| 198 | + .map((l) => ` ${chalk.green(l)}`) |
| 199 | + .map((l, idx) => idx === 0 ? operationSymbol(operation) + l : ` ${l}`) |
| 200 | + .join('\n') + ',' |
| 201 | + } |
| 202 | + |
| 203 | + if (operation === ParameterOperation.REMOVE) { |
| 204 | + return JSON.stringify(mappedA, null, 4) |
| 205 | + .split(/\n/g) |
| 206 | + .map((l, idx) => idx === 0 ? `"${name}": ${l}` : l) |
| 207 | + .map((l) => ` ${chalk.red(l)}`) |
| 208 | + .map((l, idx) => idx === 0 ? operationSymbol(operation) + l : ` ${l}`) |
| 209 | + .join('\n') + ',' |
| 210 | + } |
| 211 | + |
| 212 | + if (operation === ParameterOperation.NOOP) { |
| 213 | + return JSON.stringify(mappedB, null, 4) |
| 214 | + .split(/\n/g) |
| 215 | + .map((l) => ` ${l}`) |
| 216 | + .join('\n') + ',' |
| 217 | + } |
| 218 | + |
| 219 | + const noop = mappedA.filter((l) => mappedB.includes(l)) |
| 220 | + const remove = mappedA.filter((l) => !mappedB.includes(l)); |
| 221 | + const add = mappedB.filter((l) => !mappedA.includes(l)); |
| 222 | + |
| 223 | + return [ |
| 224 | + `${operationSymbol(operation)} "${name}": [`, |
| 225 | + ...noop.map((l) => ` ${l},`), |
| 226 | + ...add.map((l) => `${operationSymbol(ParameterOperation.ADD)} ${chalk.green(l + ',')}`), |
| 227 | + ...remove.map((l) => `${operationSymbol(ParameterOperation.REMOVE)} ${chalk.red(l + ',')}`), |
| 228 | + ' ],' |
| 229 | + ].join('\n') |
| 230 | +} |
0 commit comments