@codifycli/plugin-core 1.0.0-beta1

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 (152) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +30 -0
  3. package/.github/workflows/release.yaml +19 -0
  4. package/.github/workflows/unit-test-ci.yaml +18 -0
  5. package/.prettierrc.json +1 -0
  6. package/bin/build.js +189 -0
  7. package/dist/bin/build.d.ts +1 -0
  8. package/dist/bin/build.js +80 -0
  9. package/dist/bin/deploy-plugin.d.ts +2 -0
  10. package/dist/bin/deploy-plugin.js +8 -0
  11. package/dist/common/errors.d.ts +8 -0
  12. package/dist/common/errors.js +24 -0
  13. package/dist/entities/change-set.d.ts +24 -0
  14. package/dist/entities/change-set.js +152 -0
  15. package/dist/entities/errors.d.ts +4 -0
  16. package/dist/entities/errors.js +7 -0
  17. package/dist/entities/plan-types.d.ts +25 -0
  18. package/dist/entities/plan-types.js +1 -0
  19. package/dist/entities/plan.d.ts +15 -0
  20. package/dist/entities/plan.js +127 -0
  21. package/dist/entities/plugin.d.ts +16 -0
  22. package/dist/entities/plugin.js +80 -0
  23. package/dist/entities/resource-options.d.ts +31 -0
  24. package/dist/entities/resource-options.js +76 -0
  25. package/dist/entities/resource-types.d.ts +11 -0
  26. package/dist/entities/resource-types.js +1 -0
  27. package/dist/entities/resource.d.ts +42 -0
  28. package/dist/entities/resource.js +303 -0
  29. package/dist/entities/stateful-parameter.d.ts +29 -0
  30. package/dist/entities/stateful-parameter.js +46 -0
  31. package/dist/entities/transform-parameter.d.ts +4 -0
  32. package/dist/entities/transform-parameter.js +2 -0
  33. package/dist/errors.d.ts +4 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/index.d.ts +20 -0
  36. package/dist/index.js +26 -0
  37. package/dist/messages/handlers.d.ts +14 -0
  38. package/dist/messages/handlers.js +134 -0
  39. package/dist/messages/sender.d.ts +11 -0
  40. package/dist/messages/sender.js +57 -0
  41. package/dist/plan/change-set.d.ts +53 -0
  42. package/dist/plan/change-set.js +153 -0
  43. package/dist/plan/plan-types.d.ts +23 -0
  44. package/dist/plan/plan-types.js +1 -0
  45. package/dist/plan/plan.d.ts +66 -0
  46. package/dist/plan/plan.js +328 -0
  47. package/dist/plugin/plugin.d.ts +24 -0
  48. package/dist/plugin/plugin.js +200 -0
  49. package/dist/pty/background-pty.d.ts +21 -0
  50. package/dist/pty/background-pty.js +127 -0
  51. package/dist/pty/index.d.ts +50 -0
  52. package/dist/pty/index.js +20 -0
  53. package/dist/pty/promise-queue.d.ts +5 -0
  54. package/dist/pty/promise-queue.js +26 -0
  55. package/dist/pty/seqeuntial-pty.d.ts +17 -0
  56. package/dist/pty/seqeuntial-pty.js +119 -0
  57. package/dist/pty/vitest.config.d.ts +2 -0
  58. package/dist/pty/vitest.config.js +11 -0
  59. package/dist/resource/config-parser.d.ts +11 -0
  60. package/dist/resource/config-parser.js +21 -0
  61. package/dist/resource/parsed-resource-settings.d.ts +47 -0
  62. package/dist/resource/parsed-resource-settings.js +196 -0
  63. package/dist/resource/resource-controller.d.ts +36 -0
  64. package/dist/resource/resource-controller.js +402 -0
  65. package/dist/resource/resource-settings.d.ts +303 -0
  66. package/dist/resource/resource-settings.js +147 -0
  67. package/dist/resource/resource.d.ts +144 -0
  68. package/dist/resource/resource.js +44 -0
  69. package/dist/resource/stateful-parameter.d.ts +165 -0
  70. package/dist/resource/stateful-parameter.js +94 -0
  71. package/dist/scripts/deploy.d.ts +1 -0
  72. package/dist/scripts/deploy.js +2 -0
  73. package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
  74. package/dist/stateful-parameter/stateful-parameter-controller.js +81 -0
  75. package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
  76. package/dist/stateful-parameter/stateful-parameter.js +43 -0
  77. package/dist/test.d.ts +1 -0
  78. package/dist/test.js +5 -0
  79. package/dist/utils/codify-spawn.d.ts +29 -0
  80. package/dist/utils/codify-spawn.js +136 -0
  81. package/dist/utils/debug.d.ts +2 -0
  82. package/dist/utils/debug.js +10 -0
  83. package/dist/utils/file-utils.d.ts +23 -0
  84. package/dist/utils/file-utils.js +186 -0
  85. package/dist/utils/functions.d.ts +12 -0
  86. package/dist/utils/functions.js +74 -0
  87. package/dist/utils/index.d.ts +46 -0
  88. package/dist/utils/index.js +271 -0
  89. package/dist/utils/internal-utils.d.ts +12 -0
  90. package/dist/utils/internal-utils.js +74 -0
  91. package/dist/utils/load-resources.d.ts +1 -0
  92. package/dist/utils/load-resources.js +46 -0
  93. package/dist/utils/package-json-utils.d.ts +12 -0
  94. package/dist/utils/package-json-utils.js +34 -0
  95. package/dist/utils/pty-local-storage.d.ts +2 -0
  96. package/dist/utils/pty-local-storage.js +2 -0
  97. package/dist/utils/spawn-2.d.ts +5 -0
  98. package/dist/utils/spawn-2.js +7 -0
  99. package/dist/utils/spawn.d.ts +29 -0
  100. package/dist/utils/spawn.js +124 -0
  101. package/dist/utils/utils.d.ts +18 -0
  102. package/dist/utils/utils.js +86 -0
  103. package/dist/utils/verbosity-level.d.ts +5 -0
  104. package/dist/utils/verbosity-level.js +9 -0
  105. package/package.json +59 -0
  106. package/rollup.config.js +24 -0
  107. package/src/common/errors.test.ts +43 -0
  108. package/src/common/errors.ts +31 -0
  109. package/src/errors.ts +8 -0
  110. package/src/index.test.ts +6 -0
  111. package/src/index.ts +30 -0
  112. package/src/messages/handlers.test.ts +329 -0
  113. package/src/messages/handlers.ts +181 -0
  114. package/src/messages/sender.ts +69 -0
  115. package/src/plan/change-set.test.ts +280 -0
  116. package/src/plan/change-set.ts +236 -0
  117. package/src/plan/plan-types.ts +27 -0
  118. package/src/plan/plan.test.ts +413 -0
  119. package/src/plan/plan.ts +499 -0
  120. package/src/plugin/plugin.test.ts +533 -0
  121. package/src/plugin/plugin.ts +291 -0
  122. package/src/pty/background-pty.test.ts +69 -0
  123. package/src/pty/background-pty.ts +154 -0
  124. package/src/pty/index.test.ts +129 -0
  125. package/src/pty/index.ts +66 -0
  126. package/src/pty/promise-queue.ts +33 -0
  127. package/src/pty/seqeuntial-pty.ts +151 -0
  128. package/src/pty/sequential-pty.test.ts +194 -0
  129. package/src/resource/config-parser.ts +42 -0
  130. package/src/resource/parsed-resource-settings.test.ts +186 -0
  131. package/src/resource/parsed-resource-settings.ts +307 -0
  132. package/src/resource/resource-controller-stateful-mode.test.ts +253 -0
  133. package/src/resource/resource-controller.test.ts +1081 -0
  134. package/src/resource/resource-controller.ts +563 -0
  135. package/src/resource/resource-settings.test.ts +1213 -0
  136. package/src/resource/resource-settings.ts +545 -0
  137. package/src/resource/resource.ts +157 -0
  138. package/src/stateful-parameter/stateful-parameter-controller.test.ts +244 -0
  139. package/src/stateful-parameter/stateful-parameter-controller.ts +111 -0
  140. package/src/stateful-parameter/stateful-parameter.ts +160 -0
  141. package/src/utils/debug.ts +11 -0
  142. package/src/utils/file-utils.test.ts +7 -0
  143. package/src/utils/file-utils.ts +231 -0
  144. package/src/utils/functions.ts +103 -0
  145. package/src/utils/index.ts +340 -0
  146. package/src/utils/internal-utils.test.ts +52 -0
  147. package/src/utils/pty-local-storage.ts +3 -0
  148. package/src/utils/test-utils.test.ts +96 -0
  149. package/src/utils/verbosity-level.ts +11 -0
  150. package/tsconfig.json +26 -0
  151. package/tsconfig.test.json +9 -0
  152. package/vitest.config.ts +10 -0
@@ -0,0 +1,53 @@
1
+ import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
2
+ import { ParsedParameterSetting } from '../resource/parsed-resource-settings.js';
3
+ import { ResourceSettings } from '../resource/resource-settings.js';
4
+ /**
5
+ * A parameter change describes a parameter level change to a resource.
6
+ */
7
+ export interface ParameterChange<T extends StringIndexedObject> {
8
+ /**
9
+ * The name of the parameter
10
+ */
11
+ name: keyof T & string;
12
+ /**
13
+ * The operation to be performed on the parameter.
14
+ */
15
+ operation: ParameterOperation;
16
+ /**
17
+ * The previous value of the resource (the current value on the system)
18
+ */
19
+ previousValue: any | null;
20
+ /**
21
+ * The new value of the resource (the desired value)
22
+ */
23
+ newValue: any | null;
24
+ /**
25
+ * Whether the parameter is sensitive
26
+ */
27
+ isSensitive: boolean;
28
+ }
29
+ export declare class ChangeSet<T extends StringIndexedObject> {
30
+ operation: ResourceOperation;
31
+ parameterChanges: Array<ParameterChange<T>>;
32
+ constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
33
+ get desiredParameters(): T;
34
+ get currentParameters(): T;
35
+ static empty<T extends StringIndexedObject>(): ChangeSet<T>;
36
+ static create<T extends StringIndexedObject>(desired: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
37
+ static noop<T extends StringIndexedObject>(parameters: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
38
+ static destroy<T extends StringIndexedObject>(current: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
39
+ static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParsedParameterSetting>>): ChangeSet<T>;
40
+ /**
41
+ * Calculates the differences between the desired and current parameters,
42
+ * and returns a list of parameter changes that describe what needs to be added,
43
+ * removed, or modified to match the desired state.
44
+ *
45
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
46
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
47
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
48
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
49
+ */
50
+ private static calculateParameterChanges;
51
+ private static combineResourceOperations;
52
+ private static isSame;
53
+ }
@@ -0,0 +1,153 @@
1
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
2
+ // Change set will coerce undefined values to null because undefined is not valid JSON
3
+ export class ChangeSet {
4
+ operation;
5
+ parameterChanges;
6
+ constructor(operation, parameterChanges) {
7
+ this.operation = operation;
8
+ this.parameterChanges = parameterChanges;
9
+ }
10
+ get desiredParameters() {
11
+ return this.parameterChanges
12
+ .reduce((obj, pc) => ({
13
+ ...obj,
14
+ [pc.name]: pc.newValue,
15
+ }), {});
16
+ }
17
+ get currentParameters() {
18
+ return this.parameterChanges
19
+ .reduce((obj, pc) => ({
20
+ ...obj,
21
+ [pc.name]: pc.previousValue,
22
+ }), {});
23
+ }
24
+ static empty() {
25
+ return new ChangeSet(ResourceOperation.NOOP, []);
26
+ }
27
+ static create(desired, settings) {
28
+ const parameterChanges = Object.entries(desired)
29
+ .map(([k, v]) => ({
30
+ name: k,
31
+ operation: ParameterOperation.ADD,
32
+ previousValue: null,
33
+ newValue: v ?? null,
34
+ isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
35
+ }));
36
+ return new ChangeSet(ResourceOperation.CREATE, parameterChanges);
37
+ }
38
+ static noop(parameters, settings) {
39
+ const parameterChanges = Object.entries(parameters)
40
+ .map(([k, v]) => ({
41
+ name: k,
42
+ operation: ParameterOperation.NOOP,
43
+ previousValue: v ?? null,
44
+ newValue: v ?? null,
45
+ isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
46
+ }));
47
+ return new ChangeSet(ResourceOperation.NOOP, parameterChanges);
48
+ }
49
+ static destroy(current, settings) {
50
+ const parameterChanges = Object.entries(current)
51
+ .map(([k, v]) => ({
52
+ name: k,
53
+ operation: ParameterOperation.REMOVE,
54
+ previousValue: v ?? null,
55
+ newValue: null,
56
+ isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
57
+ }));
58
+ return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
59
+ }
60
+ static calculateModification(desired, current, parameterSettings = {}) {
61
+ const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
62
+ const statefulParameterKeys = new Set(Object.entries(parameterSettings)
63
+ .filter(([, v]) => v?.type === 'stateful')
64
+ .map(([k]) => k));
65
+ const resourceOperation = pc
66
+ .filter((change) => change.operation !== ParameterOperation.NOOP)
67
+ .reduce((operation, curr) => {
68
+ let newOperation;
69
+ if (statefulParameterKeys.has(curr.name)) {
70
+ newOperation = ResourceOperation.MODIFY; // All stateful parameters are modify only
71
+ }
72
+ else if (parameterSettings[curr.name]?.canModify) {
73
+ newOperation = ResourceOperation.MODIFY;
74
+ }
75
+ else {
76
+ newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
77
+ }
78
+ return ChangeSet.combineResourceOperations(operation, newOperation);
79
+ }, ResourceOperation.NOOP);
80
+ return new ChangeSet(resourceOperation, pc);
81
+ }
82
+ /**
83
+ * Calculates the differences between the desired and current parameters,
84
+ * and returns a list of parameter changes that describe what needs to be added,
85
+ * removed, or modified to match the desired state.
86
+ *
87
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
88
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
89
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
90
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
91
+ */
92
+ static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
93
+ const parameterChangeSet = new Array();
94
+ // Filter out null and undefined values or else the diff below will not work
95
+ const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
96
+ const current = Object.fromEntries(Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined));
97
+ for (const k of new Set([...Object.keys(current), ...Object.keys(desired)])) {
98
+ if (ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
99
+ parameterChangeSet.push({
100
+ name: k,
101
+ previousValue: current[k] ?? null,
102
+ newValue: desired[k] ?? null,
103
+ operation: ParameterOperation.NOOP,
104
+ isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
105
+ });
106
+ continue;
107
+ }
108
+ if ((desired?.[k] === null || desired?.[k] === undefined) && (current?.[k] !== null && current?.[k] !== undefined)) {
109
+ parameterChangeSet.push({
110
+ name: k,
111
+ previousValue: current[k] ?? null,
112
+ newValue: null,
113
+ operation: ParameterOperation.REMOVE,
114
+ isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
115
+ });
116
+ continue;
117
+ }
118
+ if ((current?.[k] === null || current?.[k] === undefined) && (desired?.[k] !== null && desired?.[k] !== undefined)) {
119
+ parameterChangeSet.push({
120
+ name: k,
121
+ previousValue: null,
122
+ newValue: desired[k] ?? null,
123
+ operation: ParameterOperation.ADD,
124
+ isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
125
+ });
126
+ continue;
127
+ }
128
+ parameterChangeSet.push({
129
+ name: k,
130
+ previousValue: current[k] ?? null,
131
+ newValue: desired[k] ?? null,
132
+ operation: ParameterOperation.MODIFY,
133
+ isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
134
+ });
135
+ }
136
+ return parameterChangeSet;
137
+ }
138
+ static combineResourceOperations(prev, next) {
139
+ const orderOfOperations = [
140
+ ResourceOperation.NOOP,
141
+ ResourceOperation.MODIFY,
142
+ ResourceOperation.RECREATE,
143
+ ResourceOperation.CREATE,
144
+ ResourceOperation.DESTROY,
145
+ ];
146
+ const indexPrev = orderOfOperations.indexOf(prev);
147
+ const indexNext = orderOfOperations.indexOf(next);
148
+ return orderOfOperations[Math.max(indexPrev, indexNext)];
149
+ }
150
+ static isSame(desired, current, setting) {
151
+ return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
152
+ }
153
+ }
@@ -0,0 +1,23 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+ import { Plan } from './plan.js';
3
+ /**
4
+ * A narrower type for plans for CREATE operations. Only desiredConfig is not null.
5
+ */
6
+ export interface CreatePlan<T extends StringIndexedObject> extends Plan<T> {
7
+ desiredConfig: T;
8
+ currentConfig: null;
9
+ }
10
+ /**
11
+ * A narrower type for plans for DESTROY operations. Only currentConfig is not null.
12
+ */
13
+ export interface DestroyPlan<T extends StringIndexedObject> extends Plan<T> {
14
+ desiredConfig: null;
15
+ currentConfig: T;
16
+ }
17
+ /**
18
+ * A narrower type for plans for MODIFY and RE-CREATE operations.
19
+ */
20
+ export interface ModifyPlan<T extends StringIndexedObject> extends Plan<T> {
21
+ desiredConfig: T;
22
+ currentConfig: T;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { ApplyRequestData, PlanResponseData, ResourceConfig, StringIndexedObject } from 'codify-schemas';
2
+ import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
3
+ import { ChangeSet } from './change-set.js';
4
+ /**
5
+ * A plan represents a set of actions that after taken will turn the current resource into the desired one.
6
+ * A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
7
+ * operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
8
+ */
9
+ export declare class Plan<T extends StringIndexedObject> {
10
+ id: string;
11
+ /**
12
+ * List of changes to make
13
+ */
14
+ changeSet: ChangeSet<T>;
15
+ /**
16
+ * Ex: name, type, dependsOn etc. Metadata parameters
17
+ */
18
+ coreParameters: ResourceConfig;
19
+ isStateful: boolean;
20
+ constructor(id: string, changeSet: ChangeSet<T>, coreParameters: ResourceConfig, isStateful: boolean);
21
+ /**
22
+ * The desired config that a plan will achieve after executing all the actions.
23
+ */
24
+ get desiredConfig(): T | null;
25
+ /**
26
+ * The current config that the plan is changing.
27
+ */
28
+ get currentConfig(): T | null;
29
+ get resourceId(): string;
30
+ static calculate<T extends StringIndexedObject>(params: {
31
+ desired: Partial<T> | null;
32
+ currentArray: Partial<T>[] | null;
33
+ state: Partial<T> | null;
34
+ core: ResourceConfig;
35
+ settings: ParsedResourceSettings<T>;
36
+ isStateful: boolean;
37
+ }): Plan<T>;
38
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
39
+ /**
40
+ * The type (id) of the resource
41
+ *
42
+ * @return string
43
+ */
44
+ getResourceType(): string;
45
+ /**
46
+ * When multiples of the same resource are allowed, this matching function will match a given config with one of the
47
+ * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
48
+ * the application name and location to match it to our desired configs name and location.
49
+ *
50
+ * @param params
51
+ * @private
52
+ */
53
+ private static matchCurrentParameters;
54
+ /**
55
+ * Only keep relevant params for the plan. We don't want to change settings that were not already
56
+ * defined.
57
+ *
58
+ * 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
59
+ * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
60
+ * or wants to set. If a parameter is not specified then it's not managed by Codify.
61
+ */
62
+ private static filterCurrentParams;
63
+ requiresChanges(): boolean;
64
+ /** Convert the plan to a JSON response object */
65
+ toResponse(): PlanResponseData;
66
+ }
@@ -0,0 +1,328 @@
1
+ import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
2
+ import { v4 as uuidV4 } from 'uuid';
3
+ import { ChangeSet } from './change-set.js';
4
+ /**
5
+ * A plan represents a set of actions that after taken will turn the current resource into the desired one.
6
+ * A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
7
+ * operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
8
+ */
9
+ export class Plan {
10
+ id;
11
+ /**
12
+ * List of changes to make
13
+ */
14
+ changeSet;
15
+ /**
16
+ * Ex: name, type, dependsOn etc. Metadata parameters
17
+ */
18
+ coreParameters;
19
+ isStateful;
20
+ constructor(id, changeSet, coreParameters, isStateful) {
21
+ this.id = id;
22
+ this.changeSet = changeSet;
23
+ this.coreParameters = coreParameters;
24
+ this.isStateful = isStateful;
25
+ }
26
+ /**
27
+ * The desired config that a plan will achieve after executing all the actions.
28
+ */
29
+ get desiredConfig() {
30
+ if (this.changeSet.operation === ResourceOperation.DESTROY) {
31
+ return null;
32
+ }
33
+ return this.changeSet.desiredParameters;
34
+ }
35
+ /**
36
+ * The current config that the plan is changing.
37
+ */
38
+ get currentConfig() {
39
+ if (this.changeSet.operation === ResourceOperation.CREATE) {
40
+ return null;
41
+ }
42
+ return this.changeSet.currentParameters;
43
+ }
44
+ get resourceId() {
45
+ return this.coreParameters.name
46
+ ? `${this.coreParameters.type}.${this.coreParameters.name}`
47
+ : this.coreParameters.type;
48
+ }
49
+ static calculate(params) {
50
+ const { desired, currentArray, state, core, settings, isStateful } = params;
51
+ const current = Plan.matchCurrentParameters({
52
+ desired,
53
+ currentArray,
54
+ state,
55
+ settings,
56
+ isStateful
57
+ });
58
+ const filteredCurrentParameters = Plan.filterCurrentParams({
59
+ desired,
60
+ current,
61
+ state,
62
+ settings,
63
+ isStateful
64
+ });
65
+ // Empty
66
+ if (!filteredCurrentParameters && !desired) {
67
+ return new Plan(uuidV4(), ChangeSet.empty(), core, isStateful);
68
+ }
69
+ // CREATE
70
+ if (!filteredCurrentParameters && desired) {
71
+ return new Plan(uuidV4(), ChangeSet.create(desired, settings), core, isStateful);
72
+ }
73
+ // DESTROY
74
+ if (filteredCurrentParameters && !desired) {
75
+ // We can manually override destroys. If a resource cannot be destroyed (for instance the npm resource relies on NodeJS being created and destroyed)
76
+ if (!settings.canDestroy) {
77
+ return new Plan(uuidV4(), ChangeSet.noop(filteredCurrentParameters, settings), core, isStateful);
78
+ }
79
+ return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters, settings), core, isStateful);
80
+ }
81
+ // NO-OP, MODIFY or RE-CREATE
82
+ const changeSet = ChangeSet.calculateModification(desired, filteredCurrentParameters, settings.parameterSettings);
83
+ return new Plan(uuidV4(), changeSet, core, isStateful);
84
+ }
85
+ // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
86
+ static fromResponse(data, defaultValues) {
87
+ if (!data) {
88
+ throw new Error('Data is empty');
89
+ }
90
+ addDefaultValues();
91
+ return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters.map((p) => ({
92
+ ...p,
93
+ isSensitive: p.isSensitive ?? false,
94
+ }))), {
95
+ type: data.resourceType,
96
+ name: data.resourceName,
97
+ }, data.isStateful);
98
+ function addDefaultValues() {
99
+ Object.entries(defaultValues ?? {})
100
+ .forEach(([key, defaultValue]) => {
101
+ const configValueExists = data
102
+ .parameters
103
+ .some((p) => p.name === key);
104
+ // Only set default values if the value does not exist in the config
105
+ if (configValueExists) {
106
+ return;
107
+ }
108
+ switch (data.operation) {
109
+ case ResourceOperation.CREATE: {
110
+ data.parameters.push({
111
+ name: key,
112
+ operation: ParameterOperation.ADD,
113
+ previousValue: null,
114
+ newValue: defaultValue,
115
+ });
116
+ break;
117
+ }
118
+ case ResourceOperation.DESTROY: {
119
+ data.parameters.push({
120
+ name: key,
121
+ operation: ParameterOperation.REMOVE,
122
+ previousValue: defaultValue,
123
+ newValue: null,
124
+ });
125
+ break;
126
+ }
127
+ case ResourceOperation.MODIFY:
128
+ case ResourceOperation.RECREATE:
129
+ case ResourceOperation.NOOP: {
130
+ data.parameters.push({
131
+ name: key,
132
+ operation: ParameterOperation.NOOP,
133
+ previousValue: defaultValue,
134
+ newValue: defaultValue,
135
+ });
136
+ break;
137
+ }
138
+ }
139
+ });
140
+ }
141
+ }
142
+ /**
143
+ * The type (id) of the resource
144
+ *
145
+ * @return string
146
+ */
147
+ getResourceType() {
148
+ return this.coreParameters.type;
149
+ }
150
+ /**
151
+ * When multiples of the same resource are allowed, this matching function will match a given config with one of the
152
+ * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
153
+ * the application name and location to match it to our desired configs name and location.
154
+ *
155
+ * @param params
156
+ * @private
157
+ */
158
+ static matchCurrentParameters(params) {
159
+ const { desired, currentArray, state, settings, isStateful } = params;
160
+ if (!settings.allowMultiple) {
161
+ return currentArray?.[0] ?? null;
162
+ }
163
+ if (!currentArray) {
164
+ return null;
165
+ }
166
+ const { matcher: parameterMatcher, id } = settings;
167
+ const matcher = (desired, currentArray) => {
168
+ const matched = currentArray.filter((c) => parameterMatcher(desired, c));
169
+ if (matched.length > 1) {
170
+ console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`);
171
+ }
172
+ return matched[0];
173
+ };
174
+ if (isStateful) {
175
+ return state
176
+ ? matcher(state, currentArray) ?? null
177
+ : null;
178
+ }
179
+ return matcher(desired, currentArray) ?? null;
180
+ }
181
+ /**
182
+ * Only keep relevant params for the plan. We don't want to change settings that were not already
183
+ * defined.
184
+ *
185
+ * 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
186
+ * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
187
+ * or wants to set. If a parameter is not specified then it's not managed by Codify.
188
+ */
189
+ static filterCurrentParams(params) {
190
+ const { desired, current, state, settings, isStateful } = params;
191
+ if (!current) {
192
+ return null;
193
+ }
194
+ const filteredCurrent = filterCurrent();
195
+ if (!filteredCurrent) {
196
+ return null;
197
+ }
198
+ // For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
199
+ // requires additional filtering for stateful parameter arrays and objects.
200
+ if (isStateful && desired) {
201
+ return filteredCurrent;
202
+ }
203
+ // We also want to filter parameters when in delete mode. We don't want to delete parameters that
204
+ // are not specified in the original config.
205
+ if (isStateful && !desired) {
206
+ const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
207
+ .filter(([k, v]) => isArrayParameterWithFiltering(k, v))
208
+ .map(([k, v]) => [k, filterArrayParameterForDeletes(k, v)]));
209
+ return { ...filteredCurrent, ...arrayStatefulParameters };
210
+ }
211
+ // TODO: Add object handling here in addition to arrays in the future
212
+ const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
213
+ .filter(([k, v]) => isArrayParameterWithFiltering(k, v))
214
+ .map(([k, v]) => [k, filterArrayParameterForStatelessMode(k, v)]));
215
+ return { ...filteredCurrent, ...arrayStatefulParameters };
216
+ function filterCurrent() {
217
+ if (!current) {
218
+ return null;
219
+ }
220
+ if (isStateful) {
221
+ const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]);
222
+ return Object.fromEntries(Object.entries(current)
223
+ .filter(([k]) => keys.has(k)));
224
+ }
225
+ // Stateless mode
226
+ const keys = new Set(Object.keys(desired ?? {}));
227
+ return Object.fromEntries(Object.entries(current)
228
+ .filter(([k]) => keys.has(k)));
229
+ }
230
+ function getFilterParameter(k) {
231
+ if (settings.parameterSettings?.[k]?.type === 'stateful') {
232
+ const statefulSetting = settings.parameterSettings[k];
233
+ if (statefulSetting.nestedSettings.type === 'array') {
234
+ return statefulSetting.nestedSettings.filterInStatelessMode;
235
+ }
236
+ }
237
+ if (settings.parameterSettings?.[k]?.type === 'array') {
238
+ return (settings.parameterSettings?.[k]).filterInStatelessMode;
239
+ }
240
+ return undefined;
241
+ }
242
+ function isArrayParameterWithFiltering(k, v) {
243
+ const filterParameter = getFilterParameter(k);
244
+ if (settings.parameterSettings?.[k]?.type === 'stateful') {
245
+ const statefulSetting = settings.parameterSettings[k];
246
+ return statefulSetting.nestedSettings.type === 'array' &&
247
+ (filterParameter ?? true)
248
+ && Array.isArray(v);
249
+ }
250
+ return settings.parameterSettings?.[k]?.type === 'array'
251
+ && (filterParameter ?? true)
252
+ && Array.isArray(v);
253
+ }
254
+ // For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
255
+ function filterArrayParameterForStatelessMode(k, v) {
256
+ const desiredArray = desired[k];
257
+ const matcher = settings.parameterSettings[k].type === 'stateful'
258
+ ? settings.parameterSettings[k]
259
+ .nestedSettings
260
+ .isElementEqual
261
+ : settings.parameterSettings[k]
262
+ .isElementEqual;
263
+ const desiredCopy = [...desiredArray];
264
+ const currentCopy = [...v];
265
+ const defaultFilterMethod = ((desired, current) => {
266
+ const result = [];
267
+ for (let counter = desired.length - 1; counter >= 0; counter--) {
268
+ const idx = currentCopy.findIndex((e2) => matcher(desired[counter], e2));
269
+ if (idx === -1) {
270
+ continue;
271
+ }
272
+ desired.splice(counter, 1);
273
+ const [element] = current.splice(idx, 1);
274
+ result.push(element);
275
+ }
276
+ return result;
277
+ });
278
+ const filterParameter = getFilterParameter(k);
279
+ return typeof filterParameter === 'function'
280
+ ? filterParameter(desiredCopy, currentCopy)
281
+ : defaultFilterMethod(desiredCopy, currentCopy);
282
+ }
283
+ function filterArrayParameterForDeletes(k, v) {
284
+ const stateArray = state[k];
285
+ const matcher = settings.parameterSettings[k].type === 'stateful'
286
+ ? settings.parameterSettings[k]
287
+ .nestedSettings
288
+ .isElementEqual
289
+ : settings.parameterSettings[k]
290
+ .isElementEqual;
291
+ const stateCopy = [...stateArray];
292
+ const currentCopy = [...v];
293
+ const defaultFilterMethod = ((state, current) => {
294
+ const result = [];
295
+ for (let counter = state.length - 1; counter >= 0; counter--) {
296
+ const idx = currentCopy.findIndex((e2) => matcher(state[counter], e2));
297
+ if (idx === -1) {
298
+ continue;
299
+ }
300
+ state.splice(counter, 1);
301
+ const [element] = current.splice(idx, 1);
302
+ result.push(element);
303
+ }
304
+ return result;
305
+ });
306
+ const filterParameter = getFilterParameter(k);
307
+ return typeof filterParameter === 'function'
308
+ ? filterParameter(stateCopy, currentCopy)
309
+ : defaultFilterMethod(stateCopy, currentCopy);
310
+ }
311
+ }
312
+ // TODO: This needs to be revisited. I don't think this is valid anymore.
313
+ // 1. For all scenarios, there shouldn't be an apply without a plan beforehand
314
+ requiresChanges() {
315
+ return this.changeSet.operation !== ResourceOperation.NOOP;
316
+ }
317
+ /** Convert the plan to a JSON response object */
318
+ toResponse() {
319
+ return {
320
+ planId: this.id,
321
+ operation: this.changeSet.operation,
322
+ isStateful: this.isStateful,
323
+ resourceName: this.coreParameters.name,
324
+ resourceType: this.coreParameters.type,
325
+ parameters: this.changeSet.parameterChanges,
326
+ };
327
+ }
328
+ }
@@ -0,0 +1,24 @@
1
+ import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeRequestData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, SetVerbosityRequestData, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
2
+ import { Plan } from '../plan/plan.js';
3
+ import { BackgroundPty } from '../pty/background-pty.js';
4
+ import { Resource } from '../resource/resource.js';
5
+ import { ResourceController } from '../resource/resource-controller.js';
6
+ export declare class Plugin {
7
+ name: string;
8
+ resourceControllers: Map<string, ResourceController<ResourceConfig>>;
9
+ planStorage: Map<string, Plan<any>>;
10
+ planPty: BackgroundPty;
11
+ constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
12
+ static create(name: string, resources: Resource<any>[]): Plugin;
13
+ initialize(data: InitializeRequestData): Promise<InitializeResponseData>;
14
+ getResourceInfo(data: GetResourceInfoRequestData): GetResourceInfoResponseData;
15
+ match(data: MatchRequestData): Promise<MatchResponseData>;
16
+ import(data: ImportRequestData): Promise<ImportResponseData>;
17
+ validate(data: ValidateRequestData): Promise<ValidateResponseData>;
18
+ plan(data: PlanRequestData): Promise<PlanResponseData>;
19
+ apply(data: ApplyRequestData): Promise<void>;
20
+ setVerbosityLevel(data: SetVerbosityRequestData): Promise<void>;
21
+ kill(): Promise<void>;
22
+ private resolvePlan;
23
+ protected crossValidateResources(resources: ResourceJson[]): Promise<void>;
24
+ }