@cyberismo/data-handler 0.0.18 → 0.0.20

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 (134) hide show
  1. package/dist/command-handler.d.ts +2 -0
  2. package/dist/command-handler.js +31 -3
  3. package/dist/command-handler.js.map +1 -1
  4. package/dist/command-manager.d.ts +2 -0
  5. package/dist/command-manager.js +3 -0
  6. package/dist/command-manager.js.map +1 -1
  7. package/dist/commands/create.d.ts +3 -1
  8. package/dist/commands/create.js +10 -1
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/migrate.d.ts +33 -0
  11. package/dist/commands/migrate.js +66 -0
  12. package/dist/commands/migrate.js.map +1 -0
  13. package/dist/containers/project/card-cache.js +13 -1
  14. package/dist/containers/project/card-cache.js.map +1 -1
  15. package/dist/containers/project/project-paths.d.ts +1 -0
  16. package/dist/containers/project/project-paths.js +5 -2
  17. package/dist/containers/project/project-paths.js.map +1 -1
  18. package/dist/containers/project.d.ts +10 -0
  19. package/dist/containers/project.js +39 -2
  20. package/dist/containers/project.js.map +1 -1
  21. package/dist/containers/template.js +2 -0
  22. package/dist/containers/template.js.map +1 -1
  23. package/dist/interfaces/command-options.d.ts +6 -1
  24. package/dist/interfaces/project-interfaces.d.ts +4 -0
  25. package/dist/interfaces/project-interfaces.js.map +1 -1
  26. package/dist/macros/include/index.js +3 -1
  27. package/dist/macros/include/index.js.map +1 -1
  28. package/dist/macros/include/types.d.ts +6 -0
  29. package/dist/migrations/index.d.ts +14 -0
  30. package/dist/migrations/index.js +14 -0
  31. package/dist/migrations/index.js.map +1 -0
  32. package/dist/migrations/migration-executor.d.ts +79 -0
  33. package/dist/migrations/migration-executor.js +312 -0
  34. package/dist/migrations/migration-executor.js.map +1 -0
  35. package/dist/migrations/migration-worker.d.ts +13 -0
  36. package/dist/migrations/migration-worker.js +156 -0
  37. package/dist/migrations/migration-worker.js.map +1 -0
  38. package/dist/migrations/worker-executor.d.ts +24 -0
  39. package/dist/migrations/worker-executor.js +157 -0
  40. package/dist/migrations/worker-executor.js.map +1 -0
  41. package/dist/project-settings.d.ts +2 -0
  42. package/dist/project-settings.js +7 -0
  43. package/dist/project-settings.js.map +1 -1
  44. package/dist/resources/calculation-resource.d.ts +9 -0
  45. package/dist/resources/calculation-resource.js +13 -2
  46. package/dist/resources/calculation-resource.js.map +1 -1
  47. package/dist/resources/card-type-resource.d.ts +7 -2
  48. package/dist/resources/card-type-resource.js +13 -13
  49. package/dist/resources/card-type-resource.js.map +1 -1
  50. package/dist/resources/create-defaults.js +1 -1
  51. package/dist/resources/create-defaults.js.map +1 -1
  52. package/dist/resources/field-type-resource.d.ts +5 -0
  53. package/dist/resources/field-type-resource.js +23 -10
  54. package/dist/resources/field-type-resource.js.map +1 -1
  55. package/dist/resources/file-resource.d.ts +13 -1
  56. package/dist/resources/file-resource.js +17 -8
  57. package/dist/resources/file-resource.js.map +1 -1
  58. package/dist/resources/folder-resource.js +20 -8
  59. package/dist/resources/folder-resource.js.map +1 -1
  60. package/dist/resources/graph-model-resource.d.ts +5 -0
  61. package/dist/resources/graph-model-resource.js +6 -0
  62. package/dist/resources/graph-model-resource.js.map +1 -1
  63. package/dist/resources/graph-view-resource.d.ts +5 -0
  64. package/dist/resources/graph-view-resource.js +6 -0
  65. package/dist/resources/graph-view-resource.js.map +1 -1
  66. package/dist/resources/link-type-resource.d.ts +6 -0
  67. package/dist/resources/link-type-resource.js +26 -0
  68. package/dist/resources/link-type-resource.js.map +1 -1
  69. package/dist/resources/report-resource.d.ts +6 -1
  70. package/dist/resources/report-resource.js +7 -1
  71. package/dist/resources/report-resource.js.map +1 -1
  72. package/dist/resources/resource-object.d.ts +22 -7
  73. package/dist/resources/resource-object.js +44 -15
  74. package/dist/resources/resource-object.js.map +1 -1
  75. package/dist/resources/template-resource.d.ts +5 -1
  76. package/dist/resources/template-resource.js +6 -1
  77. package/dist/resources/template-resource.js.map +1 -1
  78. package/dist/resources/workflow-resource.d.ts +6 -2
  79. package/dist/resources/workflow-resource.js +11 -6
  80. package/dist/resources/workflow-resource.js.map +1 -1
  81. package/dist/svg/percentage.js +7 -3
  82. package/dist/svg/percentage.js.map +1 -1
  83. package/dist/svg/scoreCard.js +10 -4
  84. package/dist/svg/scoreCard.js.map +1 -1
  85. package/dist/types/queries.d.ts +1 -1
  86. package/dist/utils/card-utils.d.ts +1 -1
  87. package/dist/utils/common-utils.d.ts +8 -0
  88. package/dist/utils/common-utils.js +14 -0
  89. package/dist/utils/common-utils.js.map +1 -1
  90. package/dist/utils/file-utils.d.ts +15 -3
  91. package/dist/utils/file-utils.js +48 -9
  92. package/dist/utils/file-utils.js.map +1 -1
  93. package/dist/utils/json.js +2 -2
  94. package/dist/utils/json.js.map +1 -1
  95. package/dist/utils/resource-utils.js +1 -0
  96. package/dist/utils/resource-utils.js.map +1 -1
  97. package/package.json +7 -5
  98. package/src/command-handler.ts +42 -2
  99. package/src/command-manager.ts +3 -0
  100. package/src/commands/create.ts +11 -0
  101. package/src/commands/migrate.ts +88 -0
  102. package/src/containers/project/card-cache.ts +18 -1
  103. package/src/containers/project/project-paths.ts +6 -2
  104. package/src/containers/project.ts +66 -1
  105. package/src/containers/template.ts +5 -0
  106. package/src/interfaces/command-options.ts +8 -0
  107. package/src/interfaces/project-interfaces.ts +4 -0
  108. package/src/macros/include/index.ts +3 -1
  109. package/src/macros/include/types.ts +6 -0
  110. package/src/migrations/index.ts +20 -0
  111. package/src/migrations/migration-executor.ts +478 -0
  112. package/src/migrations/migration-worker.ts +190 -0
  113. package/src/migrations/worker-executor.ts +185 -0
  114. package/src/project-settings.ts +7 -0
  115. package/src/resources/calculation-resource.ts +13 -2
  116. package/src/resources/card-type-resource.ts +19 -14
  117. package/src/resources/create-defaults.ts +1 -1
  118. package/src/resources/field-type-resource.ts +31 -13
  119. package/src/resources/file-resource.ts +25 -8
  120. package/src/resources/folder-resource.ts +21 -7
  121. package/src/resources/graph-model-resource.ts +6 -0
  122. package/src/resources/graph-view-resource.ts +6 -0
  123. package/src/resources/link-type-resource.ts +34 -0
  124. package/src/resources/report-resource.ts +7 -1
  125. package/src/resources/resource-object.ts +57 -18
  126. package/src/resources/template-resource.ts +6 -1
  127. package/src/resources/workflow-resource.ts +17 -7
  128. package/src/svg/percentage.ts +7 -3
  129. package/src/svg/scoreCard.ts +10 -4
  130. package/src/types/queries.ts +1 -1
  131. package/src/utils/common-utils.ts +15 -0
  132. package/src/utils/file-utils.ts +56 -12
  133. package/src/utils/json.ts +2 -6
  134. package/src/utils/resource-utils.ts +1 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2025
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { Worker } from 'node:worker_threads';
17
+
18
+ import { getChildLogger } from '../utils/log-utils.js';
19
+ import type {
20
+ MigrationContext,
21
+ MigrationStepResult,
22
+ } from '@cyberismo/migrations';
23
+ import type { WorkerMessage, WorkerResponse } from './migration-executor.js';
24
+
25
+ const CANCEL_PERIOD_MS = 100;
26
+ const logger = getChildLogger({ module: 'WorkerExecutor' });
27
+
28
+ /**
29
+ * Execute a migration step in a separate worker thread.
30
+ * The worker loads the migration program dynamically and executes the specified step.
31
+ *
32
+ * @param migrationPath Absolute path to the migration's 'index.js' file
33
+ * @param stepName The migration step to execute
34
+ * @param context Migration context
35
+ * @param timeoutMilliSeconds Timeout in milliseconds to wait for the step to complete
36
+ * @returns Migration step result
37
+ */
38
+ export async function executeStep(
39
+ migrationPath: string,
40
+ stepName: string,
41
+ context: MigrationContext,
42
+ timeoutMilliSeconds: number,
43
+ ): Promise<MigrationStepResult> {
44
+ // Always uses the compiled .js version from the dist directory.
45
+ function _workerPath() {
46
+ const currentFilePath = fileURLToPath(import.meta.url);
47
+ const currentDir = dirname(currentFilePath);
48
+ const srcMigrationsSegment = join('src', 'migrations');
49
+ const distMigrationsSegment = join('dist', 'migrations');
50
+ const distDir = currentDir.replace(
51
+ srcMigrationsSegment,
52
+ distMigrationsSegment,
53
+ );
54
+ return join(distDir, 'migration-worker.js');
55
+ }
56
+
57
+ return new Promise((resolve, reject) => {
58
+ const worker = new Worker(_workerPath(), {
59
+ execArgv: process.execArgv,
60
+ });
61
+ let timeoutId: NodeJS.Timeout | undefined;
62
+ let isResolved = false;
63
+
64
+ const cleanup = () => {
65
+ if (timeoutId) {
66
+ clearTimeout(timeoutId);
67
+ timeoutId = undefined;
68
+ }
69
+ };
70
+
71
+ const terminate = async (sendCancel: boolean = false): Promise<void> => {
72
+ if (sendCancel) {
73
+ try {
74
+ const cancelMessage: WorkerMessage = { type: 'cancel' };
75
+ worker.postMessage(cancelMessage);
76
+ await new Promise((_resolve) =>
77
+ setTimeout(_resolve, CANCEL_PERIOD_MS),
78
+ );
79
+ } catch {
80
+ // Ignore errors when sending cancel message
81
+ }
82
+ }
83
+
84
+ try {
85
+ await worker.terminate();
86
+ } catch (error) {
87
+ logger.debug({ error }, 'Error terminating worker');
88
+ }
89
+ };
90
+
91
+ timeoutId = setTimeout(() => {
92
+ if (!isResolved) {
93
+ isResolved = true;
94
+ cleanup();
95
+
96
+ void (async () => {
97
+ await terminate(true);
98
+ resolve({
99
+ success: false,
100
+ error: new Error(`Migration step '${stepName}' timeout`),
101
+ });
102
+ })();
103
+ }
104
+ }, timeoutMilliSeconds);
105
+
106
+ worker.on('message', (response: WorkerResponse) => {
107
+ if (isResolved) return;
108
+
109
+ isResolved = true;
110
+ cleanup();
111
+
112
+ void (async () => {
113
+ if (response.type === 'error') {
114
+ await terminate(false);
115
+ resolve({
116
+ success: false,
117
+ error: new Error(response.error || 'Unknown worker error'),
118
+ });
119
+ } else if (response.type === 'result' && response.result) {
120
+ await terminate(false);
121
+ resolve(response.result);
122
+ } else {
123
+ await terminate(false);
124
+ resolve({
125
+ success: false,
126
+ error: new Error('Invalid worker response'),
127
+ });
128
+ }
129
+ })();
130
+ });
131
+
132
+ worker.on('error', (error) => {
133
+ if (isResolved) return;
134
+
135
+ isResolved = true;
136
+ cleanup();
137
+
138
+ void (async () => {
139
+ await terminate(false);
140
+ resolve({
141
+ success: false,
142
+ error,
143
+ });
144
+ })();
145
+ });
146
+
147
+ worker.on('exit', (code) => {
148
+ if (isResolved) return;
149
+
150
+ isResolved = true;
151
+ cleanup();
152
+
153
+ if (code !== 0) {
154
+ resolve({
155
+ success: false,
156
+ error: new Error(`Worker exited with code ${code}`),
157
+ });
158
+ }
159
+ });
160
+
161
+ const migrationContext: MigrationContext = {
162
+ cardRootPath: context.cardRootPath,
163
+ cardsConfigPath: context.cardsConfigPath,
164
+ fromVersion: context.fromVersion,
165
+ toVersion: context.toVersion,
166
+ backupDir: context.backupDir,
167
+ };
168
+
169
+ const executeMessage: WorkerMessage = {
170
+ type: 'execute',
171
+ migrationPath,
172
+ stepName,
173
+ context: migrationContext,
174
+ };
175
+
176
+ try {
177
+ worker.postMessage(executeMessage);
178
+ } catch (error) {
179
+ isResolved = true;
180
+ cleanup();
181
+ void terminate(false);
182
+ reject(error);
183
+ }
184
+ });
185
+ }
@@ -35,6 +35,8 @@ export class ProjectConfiguration implements ProjectSettings {
35
35
  schemaVersion?: number;
36
36
  name: string;
37
37
  cardKeyPrefix: string;
38
+ category?: string;
39
+ description: string;
38
40
  modules: ModuleSetting[];
39
41
  hubs: HubSetting[];
40
42
  private logger = getChildLogger({ module: 'Project' });
@@ -45,6 +47,7 @@ export class ProjectConfiguration implements ProjectSettings {
45
47
  this.name = '';
46
48
  this.settingPath = path;
47
49
  this.cardKeyPrefix = '';
50
+ this.description = '';
48
51
  this.modules = [];
49
52
  this.hubs = [];
50
53
  this.autoSave = autoSave;
@@ -79,6 +82,8 @@ export class ProjectConfiguration implements ProjectSettings {
79
82
  this.schemaVersion = settings.schemaVersion;
80
83
  this.cardKeyPrefix = settings.cardKeyPrefix;
81
84
  this.name = settings.name;
85
+ this.category = settings.category;
86
+ this.description = settings.description || '';
82
87
  this.modules = settings.modules || [];
83
88
  this.hubs = settings.hubs || [];
84
89
  } else {
@@ -106,6 +111,8 @@ export class ProjectConfiguration implements ProjectSettings {
106
111
  schemaVersion: this.schemaVersion,
107
112
  cardKeyPrefix: this.cardKeyPrefix,
108
113
  name: this.name,
114
+ category: this.category,
115
+ description: this.description,
109
116
  modules: this.modules,
110
117
  hubs: this.hubs,
111
118
  };
@@ -34,6 +34,11 @@ export class CalculationResource extends FolderResource<
34
34
  CalculationMetadata,
35
35
  CalculationContent
36
36
  > {
37
+ /**
38
+ * Creates instance of CalculationResource
39
+ * @param project Project to use
40
+ * @param name Resource name
41
+ */
37
42
  constructor(project: Project, name: ResourceName) {
38
43
  super(project, name, 'calculations');
39
44
 
@@ -41,9 +46,15 @@ export class CalculationResource extends FolderResource<
41
46
  this.contentSchema = super.contentSchemaContent(this.contentSchemaId);
42
47
  }
43
48
 
44
- // When resource name changes
49
+ /**
50
+ * When resource name changes
51
+ * @param existingName Current resource name
52
+ */
45
53
  protected async onNameChange(existingName: string) {
46
- await super.updateCalculations(existingName, this.content.name);
54
+ await Promise.all([
55
+ super.updateCalculations(existingName, this.content.name),
56
+ super.updateCardContentReferences(existingName, this.content.name),
57
+ ]);
47
58
  await this.write();
48
59
  }
49
60
 
@@ -17,6 +17,7 @@ import { FileResource } from './file-resource.js';
17
17
  import { resourceName, resourceNameToString } from '../utils/resource-utils.js';
18
18
  import { ResourcesFrom } from '../containers/project.js';
19
19
  import { sortCards } from '../utils/card-utils.js';
20
+ import { removeValue } from '../utils/common-utils.js';
20
21
  import { Validate } from '../commands/validate.js';
21
22
 
22
23
  import type {
@@ -39,6 +40,11 @@ import type { ResourceName } from '../utils/resource-utils.js';
39
40
  * Card type resource class.
40
41
  */
41
42
  export class CardTypeResource extends FileResource<CardType> {
43
+ /**
44
+ * Creates instance of CardTypeResource
45
+ * @param project Project to use
46
+ * @param name Resource name
47
+ */
42
48
  constructor(project: Project, name: ResourceName) {
43
49
  super(project, name, 'cardTypes');
44
50
 
@@ -69,7 +75,10 @@ export class CardTypeResource extends FileResource<CardType> {
69
75
  if (op && op.name === 'rank') return;
70
76
 
71
77
  // Collect both project cards and template cards.
72
- const cards = await this.collectCards(this.content.name);
78
+ const cards = await this.collectCards(
79
+ this.content.name,
80
+ (card, cardTypeName) => card.metadata?.cardType === cardTypeName,
81
+ );
73
82
 
74
83
  if (op && op.name === 'change') {
75
84
  const from = (op as ChangeOperation<string>).target;
@@ -119,7 +128,10 @@ export class CardTypeResource extends FileResource<CardType> {
119
128
  op: ChangeOperation<Type>,
120
129
  ) {
121
130
  await this.verifyStateMapping(stateMapping, op);
122
- const cards = await this.collectCards(this.content.name);
131
+ const cards = await this.collectCards(
132
+ this.content.name,
133
+ (card, cardTypeName) => card.metadata?.cardType === cardTypeName,
134
+ );
123
135
 
124
136
  const unmappedStates: string[] = [];
125
137
 
@@ -179,15 +191,6 @@ export class CardTypeResource extends FileResource<CardType> {
179
191
  return references;
180
192
  }
181
193
 
182
- // Remove value from array.
183
- // todo: make it as generic and move to utils
184
- private removeValue(array: string[], value: string) {
185
- const index = array.findIndex((element) => element === value);
186
- if (index !== -1) {
187
- array.splice(index, 1);
188
- }
189
- }
190
-
191
194
  // If value from 'customFields' is removed, remove it also from 'optionallyVisible' and 'alwaysVisible' arrays.
192
195
  private removeValueFromOtherArrays<Type>(
193
196
  op: Operation<Type>,
@@ -201,8 +204,8 @@ export class CardTypeResource extends FileResource<CardType> {
201
204
  field = { name: target['name' as keyof Type] };
202
205
  }
203
206
  const fieldName = (field ? field.name : target) as string;
204
- this.removeValue(content.alwaysVisibleFields, fieldName);
205
- this.removeValue(content.optionallyVisibleFields, fieldName);
207
+ removeValue(content.alwaysVisibleFields, fieldName);
208
+ removeValue(content.optionallyVisibleFields, fieldName);
206
209
  }
207
210
 
208
211
  // Sets content container values to be either '[]' or with proper values.
@@ -385,6 +388,7 @@ export class CardTypeResource extends FileResource<CardType> {
385
388
  await Promise.all([
386
389
  super.updateHandleBars(existingName, this.content.name),
387
390
  super.updateCalculations(existingName, this.content.name),
391
+ super.updateCardContentReferences(existingName, this.content.name),
388
392
  this.updateLinkTypes(existingName),
389
393
  ]);
390
394
 
@@ -394,7 +398,8 @@ export class CardTypeResource extends FileResource<CardType> {
394
398
  /**
395
399
  * Creates a new card type object. Base class writes the object to disk automatically.
396
400
  * @param workflowName Workflow name that this card type uses.
397
- * @throws when workflow is empty, or does not exist in the project.
401
+ * @throws when workflow is empty, or
402
+ * when workflow does not exist in the project.
398
403
  */
399
404
  public async createCardType(workflowName: string) {
400
405
  if (!workflowName) {
@@ -115,7 +115,7 @@ export abstract class DefaultContent {
115
115
  displayName: '',
116
116
  dataType: dataType,
117
117
  } as FieldType;
118
- if (dataType === 'enum') {
118
+ if (dataType === 'enum' || dataType === 'list') {
119
119
  value.enumValues = [{ enumValue: 'value1' }, { enumValue: 'value2' }];
120
120
  }
121
121
  return value;
@@ -50,6 +50,11 @@ export class FieldTypeResource extends FileResource<FieldType> {
50
50
  private fromType: DataType = 'integer';
51
51
  private toType: DataType = 'integer';
52
52
 
53
+ /**
54
+ * Creates an instance of FieldTypeResource
55
+ * @param project Project to use
56
+ * @param name Resource name
57
+ */
53
58
  constructor(project: Project, name: ResourceName) {
54
59
  super(project, name, 'fieldTypes');
55
60
 
@@ -183,13 +188,16 @@ export class FieldTypeResource extends FileResource<FieldType> {
183
188
  );
184
189
  }
185
190
  const newValue = (op as ChangeOperation<EnumDefinition>).to;
186
- const foundTo = values.find(
187
- (item) => (item as EnumDefinition).enumValue === newValue.enumValue,
188
- );
189
- if (foundTo) {
190
- throw new Error(
191
- `Cannot perform operation on 'enumValues'. Enum with value '${(op.to as EnumDefinition).enumValue}' already exists`,
191
+ // Only check for duplicates if the enumValue itself is being changed
192
+ if (newValue.enumValue !== targetValue.enumValue) {
193
+ const foundTo = values.find(
194
+ (item) => (item as EnumDefinition).enumValue === newValue.enumValue,
192
195
  );
196
+ if (foundTo) {
197
+ throw new Error(
198
+ `Cannot perform operation on 'enumValues'. Enum with value '${(op.to as EnumDefinition).enumValue}' already exists`,
199
+ );
200
+ }
193
201
  }
194
202
  }
195
203
  // Return the whole object; caller can just provide 'enumValue'.
@@ -206,7 +214,12 @@ export class FieldTypeResource extends FileResource<FieldType> {
206
214
  const removedValue = (op.target as EnumDefinition).enumValue;
207
215
  const cardTypes = this.relevantCardTypes();
208
216
  const allCards = await Promise.all(
209
- cardTypes.map((cardType) => this.collectCards(cardType)),
217
+ cardTypes.map((cardType) =>
218
+ this.collectCards(
219
+ cardType,
220
+ (card, cardTypeName) => card.metadata?.cardType === cardTypeName,
221
+ ),
222
+ ),
210
223
  );
211
224
  const cardsToUpdate = allCards
212
225
  .flat()
@@ -283,6 +296,7 @@ export class FieldTypeResource extends FileResource<FieldType> {
283
296
  await Promise.all([
284
297
  super.updateHandleBars(existingName, this.content.name),
285
298
  super.updateCalculations(existingName, this.content.name),
299
+ super.updateCardContentReferences(existingName, this.content.name),
286
300
  this.updateCardTypes(existingName),
287
301
  ]);
288
302
  await this.write();
@@ -314,16 +328,16 @@ export class FieldTypeResource extends FileResource<FieldType> {
314
328
  */
315
329
  public static fieldDataTypes(): DataType[] {
316
330
  return [
317
- 'shortText',
318
- 'longText',
319
- 'number',
320
- 'integer',
321
331
  'boolean',
322
- 'enum',
323
- 'list',
324
332
  'date',
325
333
  'dateTime',
334
+ 'enum',
335
+ 'integer',
336
+ 'list',
337
+ 'longText',
338
+ 'number',
326
339
  'person',
340
+ 'shortText',
327
341
  ];
328
342
  }
329
343
 
@@ -420,6 +434,10 @@ export class FieldTypeResource extends FileResource<FieldType> {
420
434
  }
421
435
  content.dataType = super.handleScalar(op) as DataType;
422
436
  } else if (key === 'enumValues') {
437
+ // Initialize enumValues array if it doesn't exist
438
+ if (!content.enumValues) {
439
+ content.enumValues = [];
440
+ }
423
441
  if (op.name === 'add' || op.name === 'change' || op.name === 'remove') {
424
442
  const existingValue = this.enumValueExists<EnumDefinition>(
425
443
  op as Operation<EnumDefinition>,
@@ -35,23 +35,35 @@ export abstract class FileResource<
35
35
  super(project, name, type);
36
36
  this.initialize();
37
37
  }
38
- // Collects cards that are using the 'cardTypeName'.
39
- protected async collectCards(cardTypeName: string) {
40
- function filteredCards(cardSource: Card[], cardTypeName: string): Card[] {
41
- const cards = cardSource;
42
- return cards.filter((card) => card.metadata?.cardType === cardTypeName);
38
+ /**
39
+ * Collects cards that match the given filter function.
40
+ * @param resourceName The resource name to filter by
41
+ * @param filterFn Function that returns true for cards to include
42
+ * @returns Array of cards that match the filter
43
+ */
44
+ protected async collectCards(
45
+ resourceName: string,
46
+ filterFn: (card: Card, resourceName: string) => boolean,
47
+ ): Promise<Card[]> {
48
+ function filteredCards(
49
+ cardSource: Card[],
50
+ resourceName: string,
51
+ filterFn: (card: Card, resourceName: string) => boolean,
52
+ ): Card[] {
53
+ return cardSource.filter((card) => filterFn(card, resourceName));
43
54
  }
44
55
 
45
56
  // Collect both project cards ...
46
57
  const projectCards = filteredCards(
47
58
  this.project.cards(this.project.paths.cardRootFolder),
48
- cardTypeName,
59
+ resourceName,
60
+ filterFn,
49
61
  );
50
62
  // ... and cards from each template that would be affected.
51
63
  const templates = this.project.resources.templates(ResourcesFrom.localOnly);
52
64
  const templateCards = templates.map((template) => {
53
65
  const templateObject = template.templateObject();
54
- return filteredCards(templateObject.cards(), cardTypeName);
66
+ return filteredCards(templateObject.cards(), resourceName, filterFn);
55
67
  });
56
68
  // Return all affected cards
57
69
  const cards = [projectCards, ...templateCards].reduce(
@@ -67,7 +79,12 @@ export abstract class FileResource<
67
79
  */
68
80
  protected abstract onNameChange?(previousName: string): Promise<void>;
69
81
 
70
- // Updates resource key to a new prefix
82
+ /**
83
+ * Updates resource key to a new prefix
84
+ * @param name Resource name
85
+ * @param prefixes list of prefixes in the project
86
+ * @returns updated resource name
87
+ */
71
88
  protected updatePrefixInResourceName(name: string, prefixes: string[]) {
72
89
  const { identifier, prefix, type } = resourceName(name);
73
90
  if (this.moduleResource) {
@@ -19,6 +19,7 @@ import { isContentKey } from '../interfaces/resource-interfaces.js';
19
19
  import {
20
20
  filename,
21
21
  contentPropertyName,
22
+ ALL_FILE_MAPPINGS,
22
23
  } from '../interfaces/folder-content-interfaces.js';
23
24
  import { formatJson } from '../utils/json.js';
24
25
  import { VALID_FOLDER_RESOURCE_FILES } from '../utils/constants.js';
@@ -95,7 +96,7 @@ export abstract class FolderResource<
95
96
  for (const [fileName, fileContent] of contentFiles.entries()) {
96
97
  const key = contentPropertyName(fileName);
97
98
  if (key) {
98
- const isJson = key === 'schema';
99
+ const isJson = key === ALL_FILE_MAPPINGS['parameterSchema.json'];
99
100
  content[key] = isJson ? JSON.parse(fileContent) : fileContent;
100
101
  }
101
102
  }
@@ -143,15 +144,28 @@ export abstract class FolderResource<
143
144
  throw new Error(`File '${fileName}' is not allowed to be updated`);
144
145
  }
145
146
 
146
- await writeFileSafe(filePath, changedContent, { flag: 'w' });
147
+ // TODO: Updates should either use valid strings or allow for objects
148
+ const key = contentPropertyName(fileName);
149
+ const isJson = key === ALL_FILE_MAPPINGS['parameterSchema.json'];
150
+ let parsedContent: unknown = changedContent;
151
+ if (isJson) {
152
+ try {
153
+ parsedContent = JSON.parse(changedContent);
154
+ } catch (error) {
155
+ const message =
156
+ error instanceof Error ? error.message : 'Unknown error';
157
+ throw new Error(`Invalid JSON content for '${key}' update: ${message}`);
158
+ }
159
+ }
160
+ const contentToWrite = isJson
161
+ ? formatJson(parsedContent as object)
162
+ : changedContent;
163
+
164
+ await writeFileSafe(filePath, contentToWrite, { flag: 'w' });
147
165
 
148
166
  // Update this resource's content
149
- const key = contentPropertyName(fileName);
150
167
  if (key) {
151
- const isJson = key === 'schema';
152
- (this.resourceContent as Record<string, unknown>)[key] = isJson
153
- ? JSON.parse(changedContent)
154
- : changedContent;
168
+ (this.resourceContent as Record<string, unknown>)[key] = parsedContent;
155
169
  }
156
170
  }
157
171
 
@@ -38,6 +38,11 @@ export class GraphModelResource extends FolderResource<
38
38
  GraphModelMetadata,
39
39
  GraphModelContent
40
40
  > {
41
+ /**
42
+ * Creates an instance of GraphModelResource
43
+ * @param project Project to use
44
+ * @param name Resource name
45
+ */
41
46
  constructor(project: Project, name: ResourceName) {
42
47
  super(project, name, 'graphModels');
43
48
 
@@ -55,6 +60,7 @@ export class GraphModelResource extends FolderResource<
55
60
  join(this.internalFolder, CONTENT_FILES.model),
56
61
  ]),
57
62
  super.updateCalculations(existingName, this.content.name),
63
+ super.updateCardContentReferences(existingName, this.content.name),
58
64
  ]);
59
65
  // Finally, write updated content.
60
66
  await this.write();
@@ -39,6 +39,11 @@ export class GraphViewResource extends FolderResource<
39
39
  GraphViewMetadata,
40
40
  GraphViewContent
41
41
  > {
42
+ /**
43
+ * Creates instance of GraphViewResource
44
+ * @param project Project to use
45
+ * @param name Resource name
46
+ */
42
47
  constructor(project: Project, name: ResourceName) {
43
48
  super(project, name, 'graphViews');
44
49
 
@@ -56,6 +61,7 @@ export class GraphViewResource extends FolderResource<
56
61
  await this.handleBarFile(),
57
62
  ]),
58
63
  super.updateCalculations(existingName, this.content.name),
64
+ super.updateCardContentReferences(existingName, this.content.name),
59
65
  ]);
60
66
  await this.write();
61
67
  }
@@ -25,6 +25,11 @@ import type { ResourceName } from '../utils/resource-utils.js';
25
25
  * Link Type resource class.
26
26
  */
27
27
  export class LinkTypeResource extends FileResource<LinkType> {
28
+ /**
29
+ * Creates instance of LinkTypeResource
30
+ * @param project Project to use
31
+ * @param name Resource name
32
+ */
28
33
  constructor(project: Project, name: ResourceName) {
29
34
  super(project, name, 'linkTypes');
30
35
 
@@ -32,6 +37,33 @@ export class LinkTypeResource extends FileResource<LinkType> {
32
37
  this.contentSchema = super.contentSchemaContent(this.contentSchemaId);
33
38
  }
34
39
 
40
+ // Update card metadata links when link type is renamed
41
+ private async updateCardLinks(from: string, to: string) {
42
+ const cards = await this.collectCards(
43
+ from,
44
+ (card, linkTypeName) =>
45
+ card.metadata?.links?.some((link) => link.linkType === linkTypeName) ??
46
+ false,
47
+ );
48
+ if (cards.length === 0) {
49
+ return;
50
+ }
51
+
52
+ await Promise.all(
53
+ cards.map(async (card) => {
54
+ if (card.metadata?.links) {
55
+ card.metadata.links = card.metadata.links.map((link) => {
56
+ if (link.linkType === from) {
57
+ return { ...link, linkType: to };
58
+ }
59
+ return link;
60
+ });
61
+ await this.project.updateCardMetadata(card, card.metadata);
62
+ }
63
+ }),
64
+ );
65
+ }
66
+
35
67
  /**
36
68
  * When resource name changes.
37
69
  * @param existingName Current resource name.
@@ -52,6 +84,8 @@ export class LinkTypeResource extends FileResource<LinkType> {
52
84
  await Promise.all([
53
85
  super.updateHandleBars(existingName, this.content.name),
54
86
  super.updateCalculations(existingName, this.content.name),
87
+ super.updateCardContentReferences(existingName, this.content.name),
88
+ this.updateCardLinks(existingName, this.content.name),
55
89
  ]);
56
90
  // Finally, write updated content.
57
91
  await this.write();
@@ -42,6 +42,11 @@ export class ReportResource extends FolderResource<
42
42
  ReportMetadata,
43
43
  ReportContent
44
44
  > {
45
+ /**
46
+ * Creates instance of ReportResource
47
+ * @param project Project to use
48
+ * @param name Resource name
49
+ */
45
50
  constructor(project: Project, name: ResourceName) {
46
51
  super(project, name, 'reports');
47
52
 
@@ -68,6 +73,7 @@ export class ReportResource extends FolderResource<
68
73
  await this.handleBarFiles(),
69
74
  ),
70
75
  super.updateCalculations(existingName, this.content.name),
76
+ super.updateCardContentReferences(existingName, this.content.name),
71
77
  ]);
72
78
  // Finally, write updated content.
73
79
  await this.write();
@@ -152,9 +158,9 @@ export class ReportResource extends FolderResource<
152
158
 
153
159
  /**
154
160
  * Validates report.
155
- * @throws when there are validation errors.
156
161
  * @param content Content to be validated.
157
162
  * @note If content is not provided, base class validation will use resource's current content.
163
+ * @throws when there are validation errors.
158
164
  */
159
165
  public async validate(content?: object) {
160
166
  const resourceContent = this.contentData();