codify-plugin-lib 1.0.76 → 1.0.77

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 (70) hide show
  1. package/.eslintrc.json +11 -4
  2. package/.github/workflows/release.yaml +19 -0
  3. package/.github/workflows/unit-test-ci.yaml +19 -0
  4. package/dist/errors.d.ts +4 -0
  5. package/dist/errors.js +7 -0
  6. package/dist/index.d.ts +10 -10
  7. package/dist/index.js +9 -9
  8. package/dist/messages/handlers.d.ts +1 -1
  9. package/dist/messages/handlers.js +2 -1
  10. package/dist/plan/change-set.d.ts +37 -0
  11. package/dist/plan/change-set.js +146 -0
  12. package/dist/plan/plan-types.d.ts +23 -0
  13. package/dist/plan/plan-types.js +1 -0
  14. package/dist/plan/plan.d.ts +59 -0
  15. package/dist/plan/plan.js +228 -0
  16. package/dist/plugin/plugin.d.ts +17 -0
  17. package/dist/plugin/plugin.js +83 -0
  18. package/dist/resource/config-parser.d.ts +14 -0
  19. package/dist/resource/config-parser.js +48 -0
  20. package/dist/resource/parsed-resource-settings.d.ts +26 -0
  21. package/dist/resource/parsed-resource-settings.js +126 -0
  22. package/dist/resource/resource-controller.d.ts +30 -0
  23. package/dist/resource/resource-controller.js +247 -0
  24. package/dist/resource/resource-settings.d.ts +149 -0
  25. package/dist/resource/resource-settings.js +9 -0
  26. package/dist/resource/resource.d.ts +137 -0
  27. package/dist/resource/resource.js +44 -0
  28. package/dist/resource/stateful-parameter.d.ts +164 -0
  29. package/dist/resource/stateful-parameter.js +94 -0
  30. package/dist/utils/utils.d.ts +19 -3
  31. package/dist/utils/utils.js +52 -3
  32. package/package.json +5 -3
  33. package/src/index.ts +10 -11
  34. package/src/messages/handlers.test.ts +10 -37
  35. package/src/messages/handlers.ts +2 -2
  36. package/src/plan/change-set.test.ts +220 -0
  37. package/src/plan/change-set.ts +225 -0
  38. package/src/plan/plan-types.ts +27 -0
  39. package/src/{entities → plan}/plan.test.ts +35 -29
  40. package/src/plan/plan.ts +353 -0
  41. package/src/{entities → plugin}/plugin.test.ts +14 -13
  42. package/src/{entities → plugin}/plugin.ts +28 -24
  43. package/src/resource/config-parser.ts +77 -0
  44. package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
  45. package/src/resource/parsed-resource-settings.ts +179 -0
  46. package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
  47. package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
  48. package/src/resource/resource-controller.ts +340 -0
  49. package/src/resource/resource-settings.test.ts +494 -0
  50. package/src/resource/resource-settings.ts +192 -0
  51. package/src/resource/resource.ts +149 -0
  52. package/src/resource/stateful-parameter.test.ts +93 -0
  53. package/src/resource/stateful-parameter.ts +217 -0
  54. package/src/utils/test-utils.test.ts +87 -0
  55. package/src/utils/utils.test.ts +2 -2
  56. package/src/utils/utils.ts +51 -5
  57. package/tsconfig.json +0 -1
  58. package/vitest.config.ts +10 -0
  59. package/src/entities/change-set.test.ts +0 -155
  60. package/src/entities/change-set.ts +0 -244
  61. package/src/entities/plan-types.ts +0 -44
  62. package/src/entities/plan.ts +0 -178
  63. package/src/entities/resource-options.ts +0 -155
  64. package/src/entities/resource-parameters.test.ts +0 -604
  65. package/src/entities/resource-types.ts +0 -31
  66. package/src/entities/resource.ts +0 -470
  67. package/src/entities/stateful-parameter.test.ts +0 -114
  68. package/src/entities/stateful-parameter.ts +0 -92
  69. package/src/entities/transform-parameter.ts +0 -13
  70. /package/src/{entities/errors.ts → errors.ts} +0 -0
package/.eslintrc.json CHANGED
@@ -9,13 +9,20 @@
9
9
  "warn",
10
10
  "always"
11
11
  ],
12
- "perfectionist/sort-classes": [
13
- "off"
14
- ],
12
+ "perfectionist/sort-classes": "off",
13
+ "perfectionist/sort-interfaces": "off",
14
+ "perfectionist/sort-enums": "off",
15
+ "perfectionist/sort-objects": "off",
16
+ "perfectionist/sort-object-types": "off",
17
+ "unicorn/no-array-reduce": "off",
18
+ "unicorn/no-array-for-each": "off",
19
+ "unicorn/prefer-object-from-entries": "off",
20
+ "unicorn/prefer-type-error": "off",
15
21
  "quotes": [
16
22
  "error",
17
23
  "single"
18
- ]
24
+ ],
25
+ "no-await-in-loop": "off"
19
26
  },
20
27
  "ignorePatterns": [
21
28
  "*.test.ts"
@@ -0,0 +1,19 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: Release
5
+
6
+ on: workflow_dispatch
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: '20.x'
16
+ cache: 'npm'
17
+ - run: npm ci
18
+ - run: tsc
19
+ - run: npm publish
@@ -0,0 +1,19 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: Unit tests
5
+
6
+ on: [ push ]
7
+
8
+ jobs:
9
+ build-and-test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: '20.x'
16
+ cache: 'npm'
17
+ - run: npm ci
18
+ - run: tsc
19
+ - run: npm run test
@@ -0,0 +1,4 @@
1
+ export declare class SudoError extends Error {
2
+ command: string;
3
+ constructor(command: string);
4
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ export class SudoError extends Error {
2
+ command;
3
+ constructor(command) {
4
+ super();
5
+ this.command = command;
6
+ }
7
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { Plugin } from './entities/plugin.js';
2
- export * from './entities/resource.js';
3
- export * from './entities/resource-types.js';
4
- export * from './entities/resource-options.js';
5
- export * from './entities/plugin.js';
6
- export * from './entities/change-set.js';
7
- export * from './entities/plan.js';
8
- export * from './entities/plan-types.js';
9
- export * from './entities/stateful-parameter.js';
10
- export * from './entities/errors.js';
1
+ import { Plugin } from './plugin/plugin.js';
2
+ export * from './errors.js';
3
+ export * from './plan/change-set.js';
4
+ export * from './plan/plan.js';
5
+ export * from './plan/plan-types.js';
6
+ export * from './plugin/plugin.js';
7
+ export * from './resource/parsed-resource-settings.js';
8
+ export * from './resource/resource.js';
9
+ export * from './resource/resource-settings.js';
10
+ export * from './resource/stateful-parameter.js';
11
11
  export * from './utils/utils.js';
12
12
  export declare function runPlugin(plugin: Plugin): Promise<void>;
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { MessageHandler } from './messages/handlers.js';
2
- export * from './entities/resource.js';
3
- export * from './entities/resource-types.js';
4
- export * from './entities/resource-options.js';
5
- export * from './entities/plugin.js';
6
- export * from './entities/change-set.js';
7
- export * from './entities/plan.js';
8
- export * from './entities/plan-types.js';
9
- export * from './entities/stateful-parameter.js';
10
- export * from './entities/errors.js';
2
+ export * from './errors.js';
3
+ export * from './plan/change-set.js';
4
+ export * from './plan/plan.js';
5
+ export * from './plan/plan-types.js';
6
+ export * from './plugin/plugin.js';
7
+ export * from './resource/parsed-resource-settings.js';
8
+ export * from './resource/resource.js';
9
+ export * from './resource/resource-settings.js';
10
+ export * from './resource/stateful-parameter.js';
11
11
  export * from './utils/utils.js';
12
12
  export async function runPlugin(plugin) {
13
13
  const messageHandler = new MessageHandler(plugin);
@@ -1,4 +1,4 @@
1
- import { Plugin } from '../entities/plugin.js';
1
+ import { Plugin } from '../plugin/plugin.js';
2
2
  export declare class MessageHandler {
3
3
  private ajv;
4
4
  private readonly plugin;
@@ -1,7 +1,7 @@
1
1
  import { Ajv } from 'ajv';
2
2
  import addFormats from 'ajv-formats';
3
3
  import { ApplyRequestDataSchema, ApplyResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
4
- import { SudoError } from '../entities/errors.js';
4
+ import { SudoError } from '../errors.js';
5
5
  const SupportedRequests = {
6
6
  'apply': {
7
7
  async handler(plugin, data) {
@@ -81,6 +81,7 @@ export class MessageHandler {
81
81
  if (!message.hasOwnProperty('cmd')) {
82
82
  return;
83
83
  }
84
+ // @ts-ignore
84
85
  const cmd = message.cmd + '_Response';
85
86
  if (e instanceof SudoError) {
86
87
  return process.send?.({
@@ -0,0 +1,37 @@
1
+ import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
2
+ import { ParameterSetting } from '../resource/resource-settings.js';
3
+ /**
4
+ * A parameter change describes a parameter level change to a resource.
5
+ */
6
+ export interface ParameterChange<T extends StringIndexedObject> {
7
+ /**
8
+ * The name of the parameter
9
+ */
10
+ name: keyof T & string;
11
+ /**
12
+ * The operation to be performed on the parameter.
13
+ */
14
+ operation: ParameterOperation;
15
+ /**
16
+ * The previous value of the resource (the current value on the system)
17
+ */
18
+ previousValue: any | null;
19
+ /**
20
+ * The new value of the resource (the desired value)
21
+ */
22
+ newValue: any | null;
23
+ }
24
+ export declare class ChangeSet<T extends StringIndexedObject> {
25
+ operation: ResourceOperation;
26
+ parameterChanges: Array<ParameterChange<T>>;
27
+ constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
28
+ get desiredParameters(): T;
29
+ get currentParameters(): T;
30
+ static empty<T extends StringIndexedObject>(): ChangeSet<T>;
31
+ static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
32
+ static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
33
+ static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): ChangeSet<T>;
34
+ private static calculateParameterChanges;
35
+ private static combineResourceOperations;
36
+ private static isSame;
37
+ }
@@ -0,0 +1,146 @@
1
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
2
+ import { areArraysEqual } from '../utils/utils.js';
3
+ // Change set will coerce undefined values to null because undefined is not valid JSON
4
+ export class ChangeSet {
5
+ operation;
6
+ parameterChanges;
7
+ constructor(operation, parameterChanges) {
8
+ this.operation = operation;
9
+ this.parameterChanges = parameterChanges;
10
+ }
11
+ get desiredParameters() {
12
+ return this.parameterChanges
13
+ .reduce((obj, pc) => ({
14
+ ...obj,
15
+ [pc.name]: pc.newValue,
16
+ }), {});
17
+ }
18
+ get currentParameters() {
19
+ return this.parameterChanges
20
+ .reduce((obj, pc) => ({
21
+ ...obj,
22
+ [pc.name]: pc.previousValue,
23
+ }), {});
24
+ }
25
+ static empty() {
26
+ return new ChangeSet(ResourceOperation.NOOP, []);
27
+ }
28
+ static create(desired) {
29
+ const parameterChanges = Object.entries(desired)
30
+ .map(([k, v]) => ({
31
+ name: k,
32
+ operation: ParameterOperation.ADD,
33
+ previousValue: null,
34
+ newValue: v ?? null,
35
+ }));
36
+ return new ChangeSet(ResourceOperation.CREATE, parameterChanges);
37
+ }
38
+ static destroy(current) {
39
+ const parameterChanges = Object.entries(current)
40
+ .map(([k, v]) => ({
41
+ name: k,
42
+ operation: ParameterOperation.REMOVE,
43
+ previousValue: v ?? null,
44
+ newValue: null,
45
+ }));
46
+ return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
47
+ }
48
+ static calculateModification(desired, current, parameterSettings = {}) {
49
+ const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
50
+ const statefulParameterKeys = new Set(Object.entries(parameterSettings)
51
+ .filter(([, v]) => v?.type === 'stateful')
52
+ .map(([k]) => k));
53
+ const resourceOperation = pc
54
+ .filter((change) => change.operation !== ParameterOperation.NOOP)
55
+ .reduce((operation, curr) => {
56
+ let newOperation;
57
+ if (statefulParameterKeys.has(curr.name)) {
58
+ newOperation = ResourceOperation.MODIFY; // All stateful parameters are modify only
59
+ }
60
+ else if (parameterSettings[curr.name]?.canModify) {
61
+ newOperation = ResourceOperation.MODIFY;
62
+ }
63
+ else {
64
+ newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
65
+ }
66
+ return ChangeSet.combineResourceOperations(operation, newOperation);
67
+ }, ResourceOperation.NOOP);
68
+ return new ChangeSet(resourceOperation, pc);
69
+ }
70
+ static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
71
+ const parameterChangeSet = new Array();
72
+ // Filter out null and undefined values or else the diff below will not work
73
+ const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
74
+ const current = Object.fromEntries(Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined));
75
+ for (const [k, v] of Object.entries(current)) {
76
+ if (desired?.[k] === null || desired?.[k] === undefined) {
77
+ parameterChangeSet.push({
78
+ name: k,
79
+ previousValue: v ?? null,
80
+ newValue: null,
81
+ operation: ParameterOperation.REMOVE,
82
+ });
83
+ delete current[k];
84
+ continue;
85
+ }
86
+ if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
87
+ parameterChangeSet.push({
88
+ name: k,
89
+ previousValue: v ?? null,
90
+ newValue: desired[k] ?? null,
91
+ operation: ParameterOperation.MODIFY,
92
+ });
93
+ delete current[k];
94
+ delete desired[k];
95
+ continue;
96
+ }
97
+ parameterChangeSet.push({
98
+ name: k,
99
+ previousValue: v ?? null,
100
+ newValue: desired[k] ?? null,
101
+ operation: ParameterOperation.NOOP,
102
+ });
103
+ delete current[k];
104
+ delete desired[k];
105
+ }
106
+ if (Object.keys(current).length > 0) {
107
+ throw new Error('Diff algorithm error');
108
+ }
109
+ for (const [k, v] of Object.entries(desired)) {
110
+ parameterChangeSet.push({
111
+ name: k,
112
+ previousValue: null,
113
+ newValue: v ?? null,
114
+ operation: ParameterOperation.ADD,
115
+ });
116
+ }
117
+ return parameterChangeSet;
118
+ }
119
+ static combineResourceOperations(prev, next) {
120
+ const orderOfOperations = [
121
+ ResourceOperation.NOOP,
122
+ ResourceOperation.MODIFY,
123
+ ResourceOperation.RECREATE,
124
+ ResourceOperation.CREATE,
125
+ ResourceOperation.DESTROY,
126
+ ];
127
+ const indexPrev = orderOfOperations.indexOf(prev);
128
+ const indexNext = orderOfOperations.indexOf(next);
129
+ return orderOfOperations[Math.max(indexPrev, indexNext)];
130
+ }
131
+ static isSame(desired, current, setting) {
132
+ switch (setting?.type) {
133
+ case 'stateful': {
134
+ const statefulSetting = setting.definition.getSettings();
135
+ return ChangeSet.isSame(desired, current, statefulSetting);
136
+ }
137
+ case 'array': {
138
+ const arrayParameter = setting;
139
+ return areArraysEqual(arrayParameter, desired, current);
140
+ }
141
+ default: {
142
+ return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
143
+ }
144
+ }
145
+ }
146
+ }
@@ -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,59 @@
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
+ changeSet: ChangeSet<T>;
12
+ coreParameters: ResourceConfig;
13
+ constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
14
+ /**
15
+ * The desired config that a plan will achieve after executing all the actions.
16
+ */
17
+ get desiredConfig(): T | null;
18
+ /**
19
+ * The current config that the plan is changing.
20
+ */
21
+ get currentConfig(): T | null;
22
+ /**
23
+ * When multiples of the same resource are allowed, this matching function will match a given config with one of the
24
+ * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
25
+ * the application name and location to match it to our desired configs name and location.
26
+ *
27
+ * @param params
28
+ * @private
29
+ */
30
+ private static matchCurrentParameters;
31
+ /**
32
+ * The type (id) of the resource
33
+ *
34
+ * @return string
35
+ */
36
+ getResourceType(): string;
37
+ static calculate<T extends StringIndexedObject>(params: {
38
+ desiredParameters: Partial<T> | null;
39
+ currentParametersArray: Partial<T>[] | null;
40
+ stateParameters: Partial<T> | null;
41
+ coreParameters: ResourceConfig;
42
+ settings: ParsedResourceSettings<T>;
43
+ statefulMode: boolean;
44
+ }): Plan<T>;
45
+ /**
46
+ * Only keep relevant params for the plan. We don't want to change settings that were not already
47
+ * defined.
48
+ *
49
+ * 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
50
+ * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
51
+ * or wants to set. If a parameter is not specified then it's not managed by Codify.
52
+ */
53
+ private static filterCurrentParams;
54
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
55
+ /**
56
+ * Convert the plan to a JSON response object
57
+ */
58
+ toResponse(): PlanResponseData;
59
+ }
@@ -0,0 +1,228 @@
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
+ changeSet;
12
+ coreParameters;
13
+ constructor(id, changeSet, resourceMetadata) {
14
+ this.id = id;
15
+ this.changeSet = changeSet;
16
+ this.coreParameters = resourceMetadata;
17
+ }
18
+ /**
19
+ * The desired config that a plan will achieve after executing all the actions.
20
+ */
21
+ get desiredConfig() {
22
+ if (this.changeSet.operation === ResourceOperation.DESTROY) {
23
+ return null;
24
+ }
25
+ return {
26
+ ...this.coreParameters,
27
+ ...this.changeSet.desiredParameters,
28
+ };
29
+ }
30
+ /**
31
+ * The current config that the plan is changing.
32
+ */
33
+ get currentConfig() {
34
+ if (this.changeSet.operation === ResourceOperation.CREATE) {
35
+ return null;
36
+ }
37
+ return {
38
+ ...this.coreParameters,
39
+ ...this.changeSet.currentParameters,
40
+ };
41
+ }
42
+ /**
43
+ * When multiples of the same resource are allowed, this matching function will match a given config with one of the
44
+ * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
45
+ * the application name and location to match it to our desired configs name and location.
46
+ *
47
+ * @param params
48
+ * @private
49
+ */
50
+ static matchCurrentParameters(params) {
51
+ const { desiredParameters, currentParametersArray, stateParameters, settings, statefulMode } = params;
52
+ if (!settings.allowMultiple) {
53
+ return currentParametersArray?.[0] ?? null;
54
+ }
55
+ if (!currentParametersArray) {
56
+ return null;
57
+ }
58
+ if (statefulMode) {
59
+ return stateParameters
60
+ ? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
61
+ : null;
62
+ }
63
+ return settings.allowMultiple.matcher(desiredParameters, currentParametersArray);
64
+ }
65
+ /**
66
+ * The type (id) of the resource
67
+ *
68
+ * @return string
69
+ */
70
+ getResourceType() {
71
+ return this.coreParameters.type;
72
+ }
73
+ static calculate(params) {
74
+ const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
75
+ const currentParameters = Plan.matchCurrentParameters({
76
+ desiredParameters,
77
+ currentParametersArray,
78
+ stateParameters,
79
+ settings,
80
+ statefulMode
81
+ });
82
+ const filteredCurrentParameters = Plan.filterCurrentParams({
83
+ desiredParameters,
84
+ currentParameters,
85
+ stateParameters,
86
+ settings,
87
+ statefulMode
88
+ });
89
+ // Empty
90
+ if (!filteredCurrentParameters && !desiredParameters) {
91
+ return new Plan(uuidV4(), ChangeSet.empty(), coreParameters);
92
+ }
93
+ // CREATE
94
+ if (!filteredCurrentParameters && desiredParameters) {
95
+ return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters);
96
+ }
97
+ // DESTROY
98
+ if (filteredCurrentParameters && !desiredParameters) {
99
+ return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
100
+ }
101
+ // NO-OP, MODIFY or RE-CREATE
102
+ const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
103
+ return new Plan(uuidV4(), changeSet, coreParameters);
104
+ }
105
+ /**
106
+ * Only keep relevant params for the plan. We don't want to change settings that were not already
107
+ * defined.
108
+ *
109
+ * 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
110
+ * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
111
+ * or wants to set. If a parameter is not specified then it's not managed by Codify.
112
+ */
113
+ static filterCurrentParams(params) {
114
+ const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, statefulMode } = params;
115
+ if (!current) {
116
+ return null;
117
+ }
118
+ const filteredCurrent = filterCurrent();
119
+ if (!filteredCurrent) {
120
+ return null;
121
+ }
122
+ // For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
123
+ // requires additional filtering for stateful parameter arrays and objects.
124
+ if (statefulMode) {
125
+ return filteredCurrent;
126
+ }
127
+ // TODO: Add object handling here in addition to arrays in the future
128
+ const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
129
+ .filter(([k, v]) => isArrayStatefulParameter(k, v))
130
+ .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
131
+ return { ...filteredCurrent, ...arrayStatefulParameters };
132
+ function filterCurrent() {
133
+ if (!current) {
134
+ return null;
135
+ }
136
+ if (statefulMode) {
137
+ const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]);
138
+ return Object.fromEntries(Object.entries(current)
139
+ .filter(([k]) => keys.has(k)));
140
+ }
141
+ // Stateless mode
142
+ const keys = new Set(Object.keys(desired ?? {}));
143
+ return Object.fromEntries(Object.entries(current)
144
+ .filter(([k]) => keys.has(k)));
145
+ }
146
+ function isArrayStatefulParameter(k, v) {
147
+ return settings.parameterSettings?.[k]?.type === 'stateful'
148
+ && settings.parameterSettings[k].definition.getSettings().type === 'array'
149
+ && Array.isArray(v);
150
+ }
151
+ function filterArrayStatefulParameter(k, v) {
152
+ const desiredArray = desired[k];
153
+ const matcher = settings.parameterSettings[k]
154
+ .definition
155
+ .getSettings()
156
+ .isElementEqual;
157
+ return v.filter((cv) => desiredArray.find((dv) => (matcher ?? ((a, b) => a === b))(dv, cv)));
158
+ }
159
+ }
160
+ // TODO: This needs to be revisited. I don't think this is valid anymore.
161
+ // 1. For all scenarios, there shouldn't be an apply without a plan beforehand
162
+ // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
163
+ static fromResponse(data, defaultValues) {
164
+ if (!data) {
165
+ throw new Error('Data is empty');
166
+ }
167
+ addDefaultValues();
168
+ return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters), {
169
+ type: data.resourceType,
170
+ name: data.resourceName,
171
+ });
172
+ function addDefaultValues() {
173
+ Object.entries(defaultValues ?? {})
174
+ .forEach(([key, defaultValue]) => {
175
+ const configValueExists = data
176
+ .parameters
177
+ .some((p) => p.name === key);
178
+ // Only set default values if the value does not exist in the config
179
+ if (configValueExists) {
180
+ return;
181
+ }
182
+ switch (data.operation) {
183
+ case ResourceOperation.CREATE: {
184
+ data.parameters.push({
185
+ name: key,
186
+ operation: ParameterOperation.ADD,
187
+ previousValue: null,
188
+ newValue: defaultValue,
189
+ });
190
+ break;
191
+ }
192
+ case ResourceOperation.DESTROY: {
193
+ data.parameters.push({
194
+ name: key,
195
+ operation: ParameterOperation.REMOVE,
196
+ previousValue: defaultValue,
197
+ newValue: null,
198
+ });
199
+ break;
200
+ }
201
+ case ResourceOperation.MODIFY:
202
+ case ResourceOperation.RECREATE:
203
+ case ResourceOperation.NOOP: {
204
+ data.parameters.push({
205
+ name: key,
206
+ operation: ParameterOperation.NOOP,
207
+ previousValue: defaultValue,
208
+ newValue: defaultValue,
209
+ });
210
+ break;
211
+ }
212
+ }
213
+ });
214
+ }
215
+ }
216
+ /**
217
+ * Convert the plan to a JSON response object
218
+ */
219
+ toResponse() {
220
+ return {
221
+ planId: this.id,
222
+ operation: this.changeSet.operation,
223
+ resourceName: this.coreParameters.name,
224
+ resourceType: this.coreParameters.type,
225
+ parameters: this.changeSet.parameterChanges,
226
+ };
227
+ }
228
+ }
@@ -0,0 +1,17 @@
1
+ import { ApplyRequestData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
2
+ import { Plan } from '../plan/plan.js';
3
+ import { Resource } from '../resource/resource.js';
4
+ import { ResourceController } from '../resource/resource-controller.js';
5
+ export declare class Plugin {
6
+ name: string;
7
+ resourceControllers: Map<string, ResourceController<ResourceConfig>>;
8
+ planStorage: Map<string, Plan<any>>;
9
+ constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
10
+ static create(name: string, resources: Resource<any>[]): Plugin;
11
+ initialize(): Promise<InitializeResponseData>;
12
+ validate(data: ValidateRequestData): Promise<ValidateResponseData>;
13
+ plan(data: PlanRequestData): Promise<PlanResponseData>;
14
+ apply(data: ApplyRequestData): Promise<void>;
15
+ private resolvePlan;
16
+ protected crossValidateResources(configs: ResourceConfig[]): Promise<void>;
17
+ }