@cyberismo/data-handler 0.0.16 → 0.0.18

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 (94) hide show
  1. package/dist/command-handler.js +5 -7
  2. package/dist/command-handler.js.map +1 -1
  3. package/dist/command-manager.js +4 -4
  4. package/dist/command-manager.js.map +1 -1
  5. package/dist/commands/create.js +1 -1
  6. package/dist/commands/create.js.map +1 -1
  7. package/dist/commands/fetch.d.ts +8 -0
  8. package/dist/commands/fetch.js +101 -23
  9. package/dist/commands/fetch.js.map +1 -1
  10. package/dist/commands/import.d.ts +5 -2
  11. package/dist/commands/import.js +12 -3
  12. package/dist/commands/import.js.map +1 -1
  13. package/dist/commands/remove.d.ts +3 -1
  14. package/dist/commands/remove.js +7 -1
  15. package/dist/commands/remove.js.map +1 -1
  16. package/dist/commands/rename.js +5 -0
  17. package/dist/commands/rename.js.map +1 -1
  18. package/dist/commands/show.d.ts +4 -2
  19. package/dist/commands/show.js +8 -2
  20. package/dist/commands/show.js.map +1 -1
  21. package/dist/commands/validate.js +3 -5
  22. package/dist/commands/validate.js.map +1 -1
  23. package/dist/containers/card-container.d.ts +7 -5
  24. package/dist/containers/card-container.js +30 -5
  25. package/dist/containers/card-container.js.map +1 -1
  26. package/dist/containers/project/project-paths.d.ts +2 -0
  27. package/dist/containers/project/project-paths.js +6 -0
  28. package/dist/containers/project/project-paths.js.map +1 -1
  29. package/dist/containers/project/resource-cache.js +9 -7
  30. package/dist/containers/project/resource-cache.js.map +1 -1
  31. package/dist/containers/project.d.ts +11 -2
  32. package/dist/containers/project.js +54 -8
  33. package/dist/containers/project.js.map +1 -1
  34. package/dist/containers/template.js +4 -4
  35. package/dist/containers/template.js.map +1 -1
  36. package/dist/interfaces/command-options.d.ts +3 -1
  37. package/dist/interfaces/project-interfaces.d.ts +5 -5
  38. package/dist/interfaces/project-interfaces.js.map +1 -1
  39. package/dist/project-settings.d.ts +5 -0
  40. package/dist/project-settings.js +12 -0
  41. package/dist/project-settings.js.map +1 -1
  42. package/dist/resources/card-type-resource.d.ts +6 -2
  43. package/dist/resources/card-type-resource.js +68 -86
  44. package/dist/resources/card-type-resource.js.map +1 -1
  45. package/dist/resources/field-type-resource.d.ts +5 -1
  46. package/dist/resources/field-type-resource.js +49 -54
  47. package/dist/resources/field-type-resource.js.map +1 -1
  48. package/dist/resources/file-resource.d.ts +14 -1
  49. package/dist/resources/file-resource.js +29 -0
  50. package/dist/resources/file-resource.js.map +1 -1
  51. package/dist/resources/link-type-resource.d.ts +5 -1
  52. package/dist/resources/link-type-resource.js +28 -30
  53. package/dist/resources/link-type-resource.js.map +1 -1
  54. package/dist/resources/resource-object.d.ts +1 -0
  55. package/dist/resources/resource-object.js +52 -1
  56. package/dist/resources/resource-object.js.map +1 -1
  57. package/dist/resources/template-resource.js +5 -26
  58. package/dist/resources/template-resource.js.map +1 -1
  59. package/dist/resources/workflow-resource.d.ts +1 -1
  60. package/dist/resources/workflow-resource.js +79 -76
  61. package/dist/resources/workflow-resource.js.map +1 -1
  62. package/dist/utils/configuration-logger.d.ts +91 -0
  63. package/dist/utils/configuration-logger.js +151 -0
  64. package/dist/utils/configuration-logger.js.map +1 -0
  65. package/dist/utils/constants.d.ts +1 -1
  66. package/dist/utils/constants.js +5 -3
  67. package/dist/utils/constants.js.map +1 -1
  68. package/package.json +4 -4
  69. package/src/command-handler.ts +17 -9
  70. package/src/command-manager.ts +4 -4
  71. package/src/commands/create.ts +1 -1
  72. package/src/commands/fetch.ts +143 -34
  73. package/src/commands/import.ts +13 -1
  74. package/src/commands/remove.ts +10 -1
  75. package/src/commands/rename.ts +15 -0
  76. package/src/commands/show.ts +11 -3
  77. package/src/commands/validate.ts +3 -7
  78. package/src/containers/card-container.ts +37 -5
  79. package/src/containers/project/project-paths.ts +8 -0
  80. package/src/containers/project/resource-cache.ts +12 -9
  81. package/src/containers/project.ts +76 -9
  82. package/src/containers/template.ts +4 -4
  83. package/src/interfaces/command-options.ts +3 -1
  84. package/src/interfaces/project-interfaces.ts +5 -5
  85. package/src/project-settings.ts +13 -0
  86. package/src/resources/card-type-resource.ts +91 -109
  87. package/src/resources/field-type-resource.ts +60 -64
  88. package/src/resources/file-resource.ts +43 -1
  89. package/src/resources/link-type-resource.ts +32 -36
  90. package/src/resources/resource-object.ts +73 -1
  91. package/src/resources/template-resource.ts +6 -26
  92. package/src/resources/workflow-resource.ts +102 -93
  93. package/src/utils/configuration-logger.ts +206 -0
  94. package/src/utils/constants.ts +5 -3
@@ -21,6 +21,7 @@ import {
21
21
  unlink,
22
22
  writeFile,
23
23
  } from 'node:fs/promises';
24
+ import { readdirSync } from 'node:fs';
24
25
 
25
26
  // base class
26
27
  import { CardContainer } from './card-container.js';
@@ -59,6 +60,11 @@ import type { Template } from './template.js';
59
60
 
60
61
  import { ROOT } from '../utils/constants.js';
61
62
 
63
+ import {
64
+ ConfigurationLogger,
65
+ ConfigurationOperation,
66
+ } from '../utils/configuration-logger.js';
67
+
62
68
  // Re-export this, so that classes that use Project do not need to have separate import.
63
69
  export { ResourcesFrom };
64
70
 
@@ -83,6 +89,7 @@ export class Project extends CardContainer {
83
89
  private resourceWatcher: ContentWatcher | undefined;
84
90
  private settings: ProjectConfiguration;
85
91
  private validator: Validate;
92
+ private cachedAllModulePrefixes: string[] = [];
86
93
 
87
94
  constructor(
88
95
  path: string,
@@ -95,7 +102,7 @@ export class Project extends CardContainer {
95
102
  join(path, '.cards', 'local', Project.projectConfigFileName),
96
103
  options.autoSave ?? true,
97
104
  );
98
- super(path, settings.cardKeyPrefix, '');
105
+ super(path, settings.cardKeyPrefix);
99
106
  this.settings = settings;
100
107
 
101
108
  this.logger.info({ path }, 'Initializing project');
@@ -103,16 +110,16 @@ export class Project extends CardContainer {
103
110
  this.calculationEngine = new CalculationEngine(this);
104
111
  this.projectPaths = new ProjectPaths(path);
105
112
  this.resourceHandler = new ResourceHandler(this);
106
-
107
- this.containerName = this.settings.name;
108
113
  // todo: implement project validation
109
114
  this.validator = Validate.getInstance();
110
115
 
111
116
  this.logger.info(
112
- { name: this.containerName },
117
+ { name: this.settings.name },
113
118
  'Project initialization complete',
114
119
  );
115
120
 
121
+ this.refreshAllModulePrefixes();
122
+
116
123
  const ignoreRenameFileChanges = true;
117
124
 
118
125
  // Watch changes in .cards if there are multiple instances of Project being
@@ -217,12 +224,34 @@ export class Project extends CardContainer {
217
224
  }
218
225
  }
219
226
 
227
+ // Refreshes the cached list of all module prefixes.
228
+ // This includes both direct and transient module dependencies.
229
+ private refreshAllModulePrefixes(): void {
230
+ const prefixes: string[] = [this.projectPrefix];
231
+
232
+ try {
233
+ const modules = readdirSync(this.paths.modulesFolder, {
234
+ withFileTypes: true,
235
+ })
236
+ .filter((item) => item.isDirectory())
237
+ .map((item) => item.name);
238
+
239
+ prefixes.push(...modules);
240
+ } catch {
241
+ // If modules folder doesn't exist, fall back to configuration modules only
242
+ const moduleNames = this.configuration.modules.map((item) => item.name);
243
+ prefixes.push(...moduleNames);
244
+ }
245
+
246
+ this.cachedAllModulePrefixes = prefixes;
247
+ }
248
+
220
249
  // Validates that card's data is valid.
221
250
  private async validateCard(card: Card): Promise<string> {
222
251
  const invalidCustomData = await this.validator.validateCustomFields(
223
252
  this,
224
253
  card,
225
- this.projectPrefixes(),
254
+ this.allModulePrefixes(),
226
255
  );
227
256
  const invalidWorkFlow = await this.validator.validateWorkflowState(
228
257
  this,
@@ -256,7 +285,7 @@ export class Project extends CardContainer {
256
285
  protected async populateTemplateCards(): Promise<void> {
257
286
  try {
258
287
  const templateResources = this.resources.templates();
259
- const prefixes = this.projectPrefixes();
288
+ const prefixes = this.allModulePrefixes();
260
289
  const loadPromises = templateResources.map(async (template) => {
261
290
  try {
262
291
  this.validator.validResourceName(
@@ -609,12 +638,30 @@ export class Project extends CardContainer {
609
638
  /**
610
639
  * Adds a module from project.
611
640
  * @param module Module to add
641
+ * @param skipMigrationLog If true, skip logging to migration log. Used during project creation.
612
642
  */
613
- public async importModule(module: ModuleSetting) {
643
+ public async importModule(module: ModuleSetting, skipMigrationLog = false) {
614
644
  // Add module as a dependency.
615
645
  await this.configuration.addModule(module);
616
646
  this.resources.changedModules();
647
+ this.refreshAllModulePrefixes();
617
648
  await this.populateTemplateCards();
649
+
650
+ // Log configuration change
651
+ if (!skipMigrationLog) {
652
+ await ConfigurationLogger.log(
653
+ this.basePath,
654
+ ConfigurationOperation.MODULE_ADD,
655
+ module.name,
656
+ {
657
+ parameters: {
658
+ location: module.location,
659
+ branch: module.branch,
660
+ private: module.private,
661
+ },
662
+ },
663
+ );
664
+ }
618
665
  this.logger.info(`Imported module '${module.name}'`);
619
666
  }
620
667
 
@@ -831,9 +878,18 @@ export class Project extends CardContainer {
831
878
  }
832
879
 
833
880
  /**
834
- * Returns all prefixes used in the project (project's own plus all from imported modules).
881
+ * Returns all prefixes used in the project.
882
+ * This includes both direct dependencies and transient dependencies.
835
883
  * @returns all prefixes used in the project.
836
884
  */
885
+ public allModulePrefixes(): string[] {
886
+ return this.cachedAllModulePrefixes;
887
+ }
888
+
889
+ /**
890
+ * Returns prefixes for direct module dependencies only (from cardsConfig.json).
891
+ * @returns prefixes for direct module dependencies.
892
+ */
837
893
  public projectPrefixes(): string[] {
838
894
  const prefixes: string[] = [this.projectPrefix];
839
895
  const moduleNames = this.configuration.modules.map((item) => item.name);
@@ -892,6 +948,17 @@ export class Project extends CardContainer {
892
948
  // Finally, remove module from project configuration
893
949
  await this.configuration.removeModule(moduleName);
894
950
 
951
+ // Refresh cached module prefixes after removal
952
+ this.refreshAllModulePrefixes();
953
+
954
+ // Log configuration change
955
+ await ConfigurationLogger.log(
956
+ this.basePath,
957
+ ConfigurationOperation.MODULE_REMOVE,
958
+ moduleName,
959
+ {},
960
+ );
961
+
895
962
  this.logger.info(`Removed module '${moduleName}'`);
896
963
  }
897
964
 
@@ -909,7 +976,7 @@ export class Project extends CardContainer {
909
976
  */
910
977
  public async show(): Promise<ProjectMetadata> {
911
978
  return {
912
- name: this.containerName,
979
+ name: this.settings.name,
913
980
  path: this.basePath,
914
981
  prefix: this.projectPrefix,
915
982
  hubs: this.configuration.hubs,
@@ -65,7 +65,7 @@ export class Template extends CardContainer {
65
65
  constructor(project: Project, template: Resource) {
66
66
  // Templates might come from modules. Remove module name from template name.
67
67
  const templateName = stripExtension(basename(template.name));
68
- super(template.path, project.projectPrefix, templateName);
68
+ super(template.path, project.projectPrefix);
69
69
  this.templateName = templateName;
70
70
  this.fullTemplateName = template.name;
71
71
 
@@ -399,7 +399,7 @@ export class Template extends CardContainer {
399
399
  try {
400
400
  // todo: to use cache instead of file access
401
401
  if (!pathExists(this.templateFolder())) {
402
- throw new Error(`Template '${this.containerName}' does not exist`);
402
+ throw new Error(`Template '${this.templateName}' does not exist`);
403
403
  }
404
404
  const cardType = this.project.resources
405
405
  .byType(cardTypeName, 'cardTypes')
@@ -407,7 +407,7 @@ export class Template extends CardContainer {
407
407
 
408
408
  if (parentCard && !this.hasTemplateCard(parentCard.key)) {
409
409
  throw new Error(
410
- `Card '${parentCard.key}' does not exist in template '${this.containerName}'`,
410
+ `Card '${parentCard.key}' does not exist in template '${this.templateName}'`,
411
411
  );
412
412
  }
413
413
 
@@ -496,7 +496,7 @@ export class Template extends CardContainer {
496
496
  const cards = this.cards();
497
497
  if (cards.length === 0) {
498
498
  throw new Error(
499
- `No cards in template '${this.containerName}'. Please add template cards with 'add' command first.`,
499
+ `No cards in template '${this.templateName}'. Please add template cards with 'add' command first.`,
500
500
  );
501
501
  }
502
502
  return this.doCreateCards(cards, parentCard);
@@ -56,7 +56,9 @@ export interface ExportCommandOptions extends BaseCommandOptions {
56
56
  export type FetchCommandOptions = BaseCommandOptions;
57
57
 
58
58
  // Options for 'import' command
59
- export type ImportCommandOptions = BaseCommandOptions;
59
+ export interface ImportCommandOptions extends BaseCommandOptions {
60
+ skipMigrationLog?: boolean;
61
+ }
60
62
 
61
63
  // Options for 'move' command
62
64
  export type MoveCommandOptions = BaseCommandOptions;
@@ -47,13 +47,15 @@ export interface CardListContainer {
47
47
  }
48
48
  // Remember to add all these keys to utils/constants.ts
49
49
  export interface PredefinedCardMetadata {
50
- title: string;
51
50
  cardType: string;
52
- workflowState: string;
53
- rank: string;
51
+ labels?: string[];
54
52
  lastTransitioned?: string;
55
53
  lastUpdated?: string;
54
+ links: Link[];
55
+ rank: string;
56
56
  templateCardKey?: string;
57
+ title: string;
58
+ workflowState: string;
57
59
  }
58
60
 
59
61
  // todo: do we need in the future separation between module-template-cards and local template-cards
@@ -65,8 +67,6 @@ export enum CardLocation {
65
67
 
66
68
  // Card's index.json file content.
67
69
  export interface CardMetadata extends PredefinedCardMetadata {
68
- labels?: string[];
69
- links: Link[];
70
70
  [key: string]: MetadataContent;
71
71
  }
72
72
 
@@ -265,4 +265,17 @@ export class ProjectConfiguration implements ProjectSettings {
265
265
  `Prefix '${newPrefix}' is not valid prefix. Prefix should be in lowercase and contain letters from a to z (max length 10).`,
266
266
  );
267
267
  }
268
+
269
+ /**
270
+ * Changes project name.
271
+ * @param newName New project name
272
+ */
273
+ public async setProjectName(newName: string) {
274
+ const isValid = Validate.isValidProjectName(newName);
275
+ if (isValid) {
276
+ this.name = newName;
277
+ return this.save();
278
+ }
279
+ throw new Error(`Project name '${newName}' is not valid.`);
280
+ }
268
281
  }
@@ -102,38 +102,6 @@ export class CardTypeResource extends FileResource<CardType> {
102
102
  }
103
103
  }
104
104
 
105
- // When resource name changes.
106
- private async handleNameChange(existingName: string) {
107
- const current = this.content;
108
- const prefixes = this.project.projectPrefixes();
109
- if (current.customFields) {
110
- current.customFields.map(
111
- (field) =>
112
- (field.name = this.updatePrefixInResourceName(field.name, prefixes)),
113
- );
114
- }
115
- if (current.alwaysVisibleFields) {
116
- current.alwaysVisibleFields = current.alwaysVisibleFields.map((item) =>
117
- this.updatePrefixInResourceName(item, prefixes),
118
- );
119
- }
120
- if (current.optionallyVisibleFields) {
121
- current.optionallyVisibleFields = current.optionallyVisibleFields.map(
122
- (item) => this.updatePrefixInResourceName(item, prefixes),
123
- );
124
- }
125
- current.workflow = this.updatePrefixInResourceName(
126
- current.workflow,
127
- prefixes,
128
- );
129
- await Promise.all([
130
- super.updateHandleBars(existingName, this.content.name),
131
- super.updateCalculations(existingName, this.content.name),
132
- ]);
133
- // Finally, write updated content.
134
- await this.write();
135
- }
136
-
137
105
  // When a field is removed, remove it from all affected cards.
138
106
  private async handleRemoveField(cards: Card[], item: CustomField) {
139
107
  for (const card of cards) {
@@ -189,15 +157,6 @@ export class CardTypeResource extends FileResource<CardType> {
189
157
  );
190
158
  }
191
159
 
192
- // Remove value from array.
193
- // todo: make it as generic and move to utils
194
- private removeValue(array: string[], value: string) {
195
- const index = array.findIndex((element) => element === value);
196
- if (index !== -1) {
197
- array.splice(index, 1);
198
- }
199
- }
200
-
201
160
  // Return link types that use this card type.
202
161
  private relevantLinkTypes(): string[] {
203
162
  const resourceName = resourceNameToString(this.resourceName);
@@ -220,6 +179,15 @@ export class CardTypeResource extends FileResource<CardType> {
220
179
  return references;
221
180
  }
222
181
 
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
+
223
191
  // If value from 'customFields' is removed, remove it also from 'optionallyVisible' and 'alwaysVisible' arrays.
224
192
  private removeValueFromOtherArrays<Type>(
225
193
  op: Operation<Type>,
@@ -246,21 +214,8 @@ export class CardTypeResource extends FileResource<CardType> {
246
214
  if (item.isCalculated === undefined) {
247
215
  item.isCalculated = false;
248
216
  }
249
- // Fetch "displayName" from field type if it is missing.
250
- if (item.name && item.displayName === undefined) {
251
- const fieldType = this.project.resources.byType(
252
- item.name,
253
- 'fieldTypes',
254
- );
255
- const fieldTypeContent = fieldType.data;
256
- if (fieldTypeContent) {
257
- item.displayName = fieldTypeContent.displayName;
258
- }
259
- } else if (!item.name) {
260
- console.error(
261
- `Custom field '${item.name}' is missing mandatory 'name' in card type '${content.name}'`,
262
- );
263
- return undefined;
217
+ if (!item.name) {
218
+ continue;
264
219
  }
265
220
  }
266
221
  } else {
@@ -400,6 +355,42 @@ export class CardTypeResource extends FileResource<CardType> {
400
355
  }
401
356
  }
402
357
 
358
+ /**
359
+ * When resource name changes
360
+ * @param existingName Current resource name
361
+ */
362
+ protected async onNameChange(existingName: string) {
363
+ const current = this.content;
364
+ const prefixes = this.project.projectPrefixes();
365
+ if (current.customFields) {
366
+ current.customFields.map(
367
+ (field) =>
368
+ (field.name = this.updatePrefixInResourceName(field.name, prefixes)),
369
+ );
370
+ }
371
+ if (current.alwaysVisibleFields) {
372
+ current.alwaysVisibleFields = current.alwaysVisibleFields.map((item) =>
373
+ this.updatePrefixInResourceName(item, prefixes),
374
+ );
375
+ }
376
+ if (current.optionallyVisibleFields) {
377
+ current.optionallyVisibleFields = current.optionallyVisibleFields.map(
378
+ (item) => this.updatePrefixInResourceName(item, prefixes),
379
+ );
380
+ }
381
+ current.workflow = this.updatePrefixInResourceName(
382
+ current.workflow,
383
+ prefixes,
384
+ );
385
+ await Promise.all([
386
+ super.updateHandleBars(existingName, this.content.name),
387
+ super.updateCalculations(existingName, this.content.name),
388
+ this.updateLinkTypes(existingName),
389
+ ]);
390
+
391
+ await this.write();
392
+ }
393
+
403
394
  /**
404
395
  * Creates a new card type object. Base class writes the object to disk automatically.
405
396
  * @param workflowName Workflow name that this card type uses.
@@ -438,7 +429,7 @@ export class CardTypeResource extends FileResource<CardType> {
438
429
  public async rename(newName: ResourceName) {
439
430
  const existingName = this.content.name;
440
431
  await super.rename(newName);
441
- return this.handleNameChange(existingName);
432
+ return this.onNameChange(existingName);
442
433
  }
443
434
 
444
435
  /**
@@ -451,60 +442,51 @@ export class CardTypeResource extends FileResource<CardType> {
451
442
  op: Operation<Type>,
452
443
  ) {
453
444
  const { key } = updateKey;
454
- const nameChange = key === 'name';
455
- const customFieldsChange = key === 'customFields';
456
- const existingName = this.content.name;
457
- await super.update(updateKey, op);
458
-
459
- const content = structuredClone(this.content);
460
- if (key === 'name') {
461
- content.name = super.handleScalar(op) as string;
462
- } else if (key === 'alwaysVisibleFields') {
463
- await this.validateFieldType(key, op);
464
- content.alwaysVisibleFields = super.handleArray(
465
- op,
466
- key,
467
- content.alwaysVisibleFields as Type[],
468
- ) as string[];
469
- } else if (key === 'optionallyVisibleFields') {
470
- await this.validateFieldType(key, op);
471
- content.optionallyVisibleFields = super.handleArray(
472
- op,
473
- key,
474
- content.optionallyVisibleFields as Type[],
475
- ) as string[];
476
- } else if (key === 'workflow') {
477
- const changeOp = op as ChangeOperation<string>;
478
- const stateMapping = changeOp.mappingTable?.stateMapping || {};
479
- content.workflow = super.handleScalar(op) as string;
480
- if (Object.keys(stateMapping).length > 0) {
481
- await this.handleWorkflowChange(stateMapping, changeOp);
445
+
446
+ if (key === 'name' || key === 'description' || key === 'displayName') {
447
+ await super.update(updateKey, op);
448
+ } else {
449
+ const content = structuredClone(this.content);
450
+ const customFieldsChange = key === 'customFields';
451
+ if (key === 'alwaysVisibleFields') {
452
+ await this.validateFieldType(key, op);
453
+ content.alwaysVisibleFields = super.handleArray(
454
+ op,
455
+ key,
456
+ content.alwaysVisibleFields as Type[],
457
+ ) as string[];
458
+ } else if (key === 'optionallyVisibleFields') {
459
+ await this.validateFieldType(key, op);
460
+ content.optionallyVisibleFields = super.handleArray(
461
+ op,
462
+ key,
463
+ content.optionallyVisibleFields as Type[],
464
+ ) as string[];
465
+ } else if (key === 'workflow') {
466
+ const changeOp = op as ChangeOperation<string>;
467
+ const stateMapping = changeOp.mappingTable?.stateMapping || {};
468
+ content.workflow = super.handleScalar(op) as string;
469
+ if (Object.keys(stateMapping).length > 0) {
470
+ await this.handleWorkflowChange(stateMapping, changeOp);
471
+ }
472
+ } else if (key === 'customFields') {
473
+ await this.validateFieldType(key, op);
474
+ content.customFields = super.handleArray(
475
+ op,
476
+ key,
477
+ content.customFields as Type[],
478
+ ) as CustomField[];
479
+ if (op.name === 'remove') {
480
+ this.removeValueFromOtherArrays(op, content);
481
+ }
482
+ } else {
483
+ throw new Error(`Unknown property '${key}' for CardType`);
482
484
  }
483
- } else if (key === 'customFields') {
484
- await this.validateFieldType(key, op);
485
- content.customFields = super.handleArray(
486
- op,
487
- key,
488
- content.customFields as Type[],
489
- ) as CustomField[];
490
- if (op.name === 'remove') {
491
- this.removeValueFromOtherArrays(op, content);
485
+ await super.postUpdate(content, updateKey, op);
486
+
487
+ if (customFieldsChange) {
488
+ return this.handleCustomFieldsChange(op as ChangeOperation<string>);
492
489
  }
493
- } else if (key === 'description') {
494
- content.description = super.handleScalar(op) as string;
495
- } else if (key === 'displayName') {
496
- content.displayName = super.handleScalar(op) as string;
497
- } else {
498
- throw new Error(`Unknown property '${key}' for CardType`);
499
- }
500
- await super.postUpdate(content, updateKey, op);
501
-
502
- // Renaming this card type causes that references to its name must be updated.
503
- if (nameChange) {
504
- await this.handleNameChange(existingName);
505
- await this.updateLinkTypes(existingName);
506
- } else if (customFieldsChange) {
507
- return this.handleCustomFieldsChange(op as ChangeOperation<string>);
508
490
  }
509
491
  }
510
492
 
@@ -223,15 +223,6 @@ export class FieldTypeResource extends FileResource<FieldType> {
223
223
  );
224
224
  }
225
225
 
226
- // When resource name changes.
227
- private async handleNameChange(existingName: string) {
228
- await Promise.all([
229
- super.updateHandleBars(existingName, this.content.name),
230
- super.updateCalculations(existingName, this.content.name),
231
- ]);
232
- await this.write();
233
- }
234
-
235
226
  // Checks if value 'from' can be converted 'to' value.
236
227
  private isConversionValid(from: DataType, to: DataType) {
237
228
  // Set helpers to avoid dragging 'Operation' object everywhere.
@@ -284,6 +275,19 @@ export class FieldTypeResource extends FileResource<FieldType> {
284
275
  }
285
276
  }
286
277
 
278
+ /**
279
+ * When resource name changes.
280
+ * @param existingName Current resource name.
281
+ */
282
+ protected async onNameChange(existingName: string) {
283
+ await Promise.all([
284
+ super.updateHandleBars(existingName, this.content.name),
285
+ super.updateCalculations(existingName, this.content.name),
286
+ this.updateCardTypes(existingName),
287
+ ]);
288
+ await this.write();
289
+ }
290
+
287
291
  /**
288
292
  * Creates a new field type object. Base class writes the object to disk automatically.
289
293
  * @param dataType Type for the new field type.
@@ -372,7 +376,7 @@ export class FieldTypeResource extends FileResource<FieldType> {
372
376
  public async rename(newName: ResourceName) {
373
377
  const existingName = this.content.name;
374
378
  await super.rename(newName);
375
- return this.handleNameChange(existingName);
379
+ return this.onNameChange(existingName);
376
380
  }
377
381
 
378
382
  /**
@@ -389,64 +393,56 @@ export class FieldTypeResource extends FileResource<FieldType> {
389
393
  op: Operation<Type>,
390
394
  ) {
391
395
  const { key } = updateKey;
392
- const nameChange = key === 'name';
393
- const typeChange = key === 'dataType';
394
- const enumChange = key === 'enumValues';
395
- const existingName = this.content.name;
396
- const existingType = this.content.dataType;
397
396
 
398
- await super.update(updateKey, op);
399
-
400
- const content = structuredClone(this.content);
401
- if (key === 'name') {
402
- content.name = super.handleScalar(op) as string;
403
- } else if (key === 'dataType') {
404
- const toType = op as ChangeOperation<DataType>;
405
- if (!FieldTypeResource.fieldDataTypes().includes(toType.to)) {
406
- throw new Error(
407
- `Cannot change '${key}' to unknown type '${toType.to}'`,
408
- );
409
- }
410
- if (existingType === toType.to) {
411
- throw new Error(`'${key}' is already '${toType.to}'`);
412
- }
413
- if (!this.isConversionValid(content.dataType, toType.to)) {
414
- throw new Error(
415
- `Cannot change data type from '${content.dataType}' to '${toType.to}'`,
416
- );
417
- }
418
- content.dataType = super.handleScalar(op) as DataType;
419
- } else if (key === 'displayName') {
420
- content.displayName = super.handleScalar(op) as string;
421
- } else if (key === 'enumValues') {
422
- if (op.name === 'add' || op.name === 'change' || op.name === 'remove') {
423
- const existingValue = this.enumValueExists<EnumDefinition>(
424
- op as Operation<EnumDefinition>,
425
- content.enumValues as EnumDefinition[],
426
- ) as Type;
427
- op.target = existingValue ?? op.target;
428
- }
429
- content.enumValues = super.handleArray(
430
- op,
431
- key,
432
- content.enumValues as Type[],
433
- ) as EnumDefinition[];
434
- } else if (key === 'description') {
435
- content.description = super.handleScalar(op) as string;
397
+ if (key === 'name' || key === 'description' || key === 'displayName') {
398
+ await super.update(updateKey, op);
436
399
  } else {
437
- throw new Error(`Unknown property '${key}' for FieldType`);
438
- }
400
+ const content = structuredClone(this.content);
401
+ const typeChange = key === 'dataType';
402
+ const enumChange = key === 'enumValues';
403
+ const existingType = this.content.dataType;
404
+ if (key === 'name') {
405
+ content.name = super.handleScalar(op) as string;
406
+ } else if (key === 'dataType') {
407
+ const toType = op as ChangeOperation<DataType>;
408
+ if (!FieldTypeResource.fieldDataTypes().includes(toType.to)) {
409
+ throw new Error(
410
+ `Cannot change '${key}' to unknown type '${toType.to}'`,
411
+ );
412
+ }
413
+ if (existingType === toType.to) {
414
+ throw new Error(`'${key}' is already '${toType.to}'`);
415
+ }
416
+ if (!this.isConversionValid(content.dataType, toType.to)) {
417
+ throw new Error(
418
+ `Cannot change data type from '${content.dataType}' to '${toType.to}'`,
419
+ );
420
+ }
421
+ content.dataType = super.handleScalar(op) as DataType;
422
+ } else if (key === 'enumValues') {
423
+ if (op.name === 'add' || op.name === 'change' || op.name === 'remove') {
424
+ const existingValue = this.enumValueExists<EnumDefinition>(
425
+ op as Operation<EnumDefinition>,
426
+ content.enumValues as EnumDefinition[],
427
+ ) as Type;
428
+ op.target = existingValue ?? op.target;
429
+ }
430
+ content.enumValues = super.handleArray(
431
+ op,
432
+ key,
433
+ content.enumValues as Type[],
434
+ ) as EnumDefinition[];
435
+ } else {
436
+ throw new Error(`Unknown property '${key}' for FieldType`);
437
+ }
439
438
 
440
- await super.postUpdate(content, updateKey, op);
439
+ await super.postUpdate(content, updateKey, op);
441
440
 
442
- if (nameChange) {
443
- // Renaming this field type causes that references to its name must be updated.
444
- await this.handleNameChange(existingName);
445
- await this.updateCardTypes(existingName);
446
- } else if (typeChange) {
447
- await this.dataTypeChanged();
448
- } else if (enumChange && op.name === 'remove') {
449
- await this.handleEnumValueReplacements(op);
441
+ if (typeChange) {
442
+ await this.dataTypeChanged();
443
+ } else if (enumChange && op.name === 'remove') {
444
+ await this.handleEnumValueReplacements(op);
445
+ }
450
446
  }
451
447
  }
452
448