@cyberismo/data-handler 0.0.2

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 (306) hide show
  1. package/LICENSE +702 -0
  2. package/dist/card-metadata-updater.d.ts +33 -0
  3. package/dist/card-metadata-updater.js +121 -0
  4. package/dist/card-metadata-updater.js.map +1 -0
  5. package/dist/command-handler.d.ts +96 -0
  6. package/dist/command-handler.js +557 -0
  7. package/dist/command-handler.js.map +1 -0
  8. package/dist/command-manager.d.ts +43 -0
  9. package/dist/command-manager.js +73 -0
  10. package/dist/command-manager.js.map +1 -0
  11. package/dist/commands/calculate.d.ts +86 -0
  12. package/dist/commands/calculate.js +444 -0
  13. package/dist/commands/calculate.js.map +1 -0
  14. package/dist/commands/create.d.ts +114 -0
  15. package/dist/commands/create.js +389 -0
  16. package/dist/commands/create.js.map +1 -0
  17. package/dist/commands/edit.d.ts +37 -0
  18. package/dist/commands/edit.js +99 -0
  19. package/dist/commands/edit.js.map +1 -0
  20. package/dist/commands/export-site.d.ts +45 -0
  21. package/dist/commands/export-site.js +301 -0
  22. package/dist/commands/export-site.js.map +1 -0
  23. package/dist/commands/export.d.ts +53 -0
  24. package/dist/commands/export.js +251 -0
  25. package/dist/commands/export.js.map +1 -0
  26. package/dist/commands/import.d.ts +53 -0
  27. package/dist/commands/import.js +133 -0
  28. package/dist/commands/import.js.map +1 -0
  29. package/dist/commands/index.d.ts +26 -0
  30. package/dist/commands/index.js +27 -0
  31. package/dist/commands/index.js.map +1 -0
  32. package/dist/commands/move.d.ts +55 -0
  33. package/dist/commands/move.js +341 -0
  34. package/dist/commands/move.js.map +1 -0
  35. package/dist/commands/remove.d.ts +38 -0
  36. package/dist/commands/remove.js +192 -0
  37. package/dist/commands/remove.js.map +1 -0
  38. package/dist/commands/rename.d.ts +46 -0
  39. package/dist/commands/rename.js +289 -0
  40. package/dist/commands/rename.js.map +1 -0
  41. package/dist/commands/show.d.ts +124 -0
  42. package/dist/commands/show.js +345 -0
  43. package/dist/commands/show.js.map +1 -0
  44. package/dist/commands/transition.d.ts +27 -0
  45. package/dist/commands/transition.js +92 -0
  46. package/dist/commands/transition.js.map +1 -0
  47. package/dist/commands/update.d.ts +29 -0
  48. package/dist/commands/update.js +64 -0
  49. package/dist/commands/update.js.map +1 -0
  50. package/dist/commands/validate.d.ts +143 -0
  51. package/dist/commands/validate.js +689 -0
  52. package/dist/commands/validate.js.map +1 -0
  53. package/dist/containers/card-container.d.ts +44 -0
  54. package/dist/containers/card-container.js +282 -0
  55. package/dist/containers/card-container.js.map +1 -0
  56. package/dist/containers/project/project-paths.d.ts +46 -0
  57. package/dist/containers/project/project-paths.js +105 -0
  58. package/dist/containers/project/project-paths.js.map +1 -0
  59. package/dist/containers/project/resource-collector.d.ts +86 -0
  60. package/dist/containers/project/resource-collector.js +331 -0
  61. package/dist/containers/project/resource-collector.js.map +1 -0
  62. package/dist/containers/project.d.ts +351 -0
  63. package/dist/containers/project.js +896 -0
  64. package/dist/containers/project.js.map +1 -0
  65. package/dist/containers/template.d.ts +108 -0
  66. package/dist/containers/template.js +433 -0
  67. package/dist/containers/template.js.map +1 -0
  68. package/dist/exceptions/index.d.ts +19 -0
  69. package/dist/exceptions/index.js +26 -0
  70. package/dist/exceptions/index.js.map +1 -0
  71. package/dist/index.d.ts +16 -0
  72. package/dist/index.js +15 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/interfaces/adoc.d.ts +12 -0
  75. package/dist/interfaces/adoc.js +13 -0
  76. package/dist/interfaces/adoc.js.map +1 -0
  77. package/dist/interfaces/macros.d.ts +45 -0
  78. package/dist/interfaces/macros.js +13 -0
  79. package/dist/interfaces/macros.js.map +1 -0
  80. package/dist/interfaces/project-interfaces.d.ts +121 -0
  81. package/dist/interfaces/project-interfaces.js +21 -0
  82. package/dist/interfaces/project-interfaces.js.map +1 -0
  83. package/dist/interfaces/request-status-interfaces.d.ts +28 -0
  84. package/dist/interfaces/request-status-interfaces.js +20 -0
  85. package/dist/interfaces/request-status-interfaces.js.map +1 -0
  86. package/dist/interfaces/resource-interfaces.d.ts +117 -0
  87. package/dist/interfaces/resource-interfaces.js +20 -0
  88. package/dist/interfaces/resource-interfaces.js.map +1 -0
  89. package/dist/macros/base-macro.d.ts +31 -0
  90. package/dist/macros/base-macro.js +126 -0
  91. package/dist/macros/base-macro.js.map +1 -0
  92. package/dist/macros/common.d.ts +17 -0
  93. package/dist/macros/common.js +23 -0
  94. package/dist/macros/common.js.map +1 -0
  95. package/dist/macros/createCards/index.d.ts +36 -0
  96. package/dist/macros/createCards/index.js +35 -0
  97. package/dist/macros/createCards/index.js.map +1 -0
  98. package/dist/macros/createCards/metadata.d.ts +14 -0
  99. package/dist/macros/createCards/metadata.js +18 -0
  100. package/dist/macros/createCards/metadata.js.map +1 -0
  101. package/dist/macros/graph/index.d.ts +29 -0
  102. package/dist/macros/graph/index.js +91 -0
  103. package/dist/macros/graph/index.js.map +1 -0
  104. package/dist/macros/graph/metadata.d.ts +14 -0
  105. package/dist/macros/graph/metadata.js +18 -0
  106. package/dist/macros/graph/metadata.js.map +1 -0
  107. package/dist/macros/index.d.ts +93 -0
  108. package/dist/macros/index.js +237 -0
  109. package/dist/macros/index.js.map +1 -0
  110. package/dist/macros/report/index.d.ts +26 -0
  111. package/dist/macros/report/index.js +70 -0
  112. package/dist/macros/report/index.js.map +1 -0
  113. package/dist/macros/report/metadata.d.ts +14 -0
  114. package/dist/macros/report/metadata.js +18 -0
  115. package/dist/macros/report/metadata.js.map +1 -0
  116. package/dist/macros/scoreCard/index.d.ts +30 -0
  117. package/dist/macros/scoreCard/index.js +38 -0
  118. package/dist/macros/scoreCard/index.js.map +1 -0
  119. package/dist/macros/scoreCard/metadata.d.ts +14 -0
  120. package/dist/macros/scoreCard/metadata.js +18 -0
  121. package/dist/macros/scoreCard/metadata.js.map +1 -0
  122. package/dist/macros/task-queue.d.ts +46 -0
  123. package/dist/macros/task-queue.js +69 -0
  124. package/dist/macros/task-queue.js.map +1 -0
  125. package/dist/module-manager.d.ts +62 -0
  126. package/dist/module-manager.js +350 -0
  127. package/dist/module-manager.js.map +1 -0
  128. package/dist/permissions/action-guard.d.ts +28 -0
  129. package/dist/permissions/action-guard.js +61 -0
  130. package/dist/permissions/action-guard.js.map +1 -0
  131. package/dist/project-settings.d.ts +42 -0
  132. package/dist/project-settings.js +120 -0
  133. package/dist/project-settings.js.map +1 -0
  134. package/dist/resources/array-handler.d.ts +28 -0
  135. package/dist/resources/array-handler.js +116 -0
  136. package/dist/resources/array-handler.js.map +1 -0
  137. package/dist/resources/card-type-resource.d.ts +72 -0
  138. package/dist/resources/card-type-resource.js +334 -0
  139. package/dist/resources/card-type-resource.js.map +1 -0
  140. package/dist/resources/create-defaults.d.ts +81 -0
  141. package/dist/resources/create-defaults.js +184 -0
  142. package/dist/resources/create-defaults.js.map +1 -0
  143. package/dist/resources/field-type-resource.d.ts +88 -0
  144. package/dist/resources/field-type-resource.js +411 -0
  145. package/dist/resources/field-type-resource.js.map +1 -0
  146. package/dist/resources/file-resource.d.ts +50 -0
  147. package/dist/resources/file-resource.js +301 -0
  148. package/dist/resources/file-resource.js.map +1 -0
  149. package/dist/resources/folder-resource.d.ts +66 -0
  150. package/dist/resources/folder-resource.js +100 -0
  151. package/dist/resources/folder-resource.js.map +1 -0
  152. package/dist/resources/graph-model-resource.d.ts +78 -0
  153. package/dist/resources/graph-model-resource.js +164 -0
  154. package/dist/resources/graph-model-resource.js.map +1 -0
  155. package/dist/resources/graph-view-resource.d.ts +78 -0
  156. package/dist/resources/graph-view-resource.js +163 -0
  157. package/dist/resources/graph-view-resource.js.map +1 -0
  158. package/dist/resources/link-type-resource.d.ts +62 -0
  159. package/dist/resources/link-type-resource.js +150 -0
  160. package/dist/resources/link-type-resource.js.map +1 -0
  161. package/dist/resources/report-resource.d.ts +77 -0
  162. package/dist/resources/report-resource.js +171 -0
  163. package/dist/resources/report-resource.js.map +1 -0
  164. package/dist/resources/resource-object.d.ts +108 -0
  165. package/dist/resources/resource-object.js +147 -0
  166. package/dist/resources/resource-object.js.map +1 -0
  167. package/dist/resources/template-resource.d.ts +82 -0
  168. package/dist/resources/template-resource.js +173 -0
  169. package/dist/resources/template-resource.js.map +1 -0
  170. package/dist/resources/workflow-resource.d.ts +67 -0
  171. package/dist/resources/workflow-resource.js +156 -0
  172. package/dist/resources/workflow-resource.js.map +1 -0
  173. package/dist/types/queries.d.ts +142 -0
  174. package/dist/types/queries.js +16 -0
  175. package/dist/types/queries.js.map +1 -0
  176. package/dist/utils/card-utils.d.ts +34 -0
  177. package/dist/utils/card-utils.js +78 -0
  178. package/dist/utils/card-utils.js.map +1 -0
  179. package/dist/utils/clingo-fact-builder.d.ts +58 -0
  180. package/dist/utils/clingo-fact-builder.js +126 -0
  181. package/dist/utils/clingo-fact-builder.js.map +1 -0
  182. package/dist/utils/clingo-facts.d.ts +97 -0
  183. package/dist/utils/clingo-facts.js +352 -0
  184. package/dist/utils/clingo-facts.js.map +1 -0
  185. package/dist/utils/clingo-parser.d.ts +59 -0
  186. package/dist/utils/clingo-parser.js +403 -0
  187. package/dist/utils/clingo-parser.js.map +1 -0
  188. package/dist/utils/clingo-program-builder.d.ts +39 -0
  189. package/dist/utils/clingo-program-builder.js +57 -0
  190. package/dist/utils/clingo-program-builder.js.map +1 -0
  191. package/dist/utils/common-utils.d.ts +24 -0
  192. package/dist/utils/common-utils.js +47 -0
  193. package/dist/utils/common-utils.js.map +1 -0
  194. package/dist/utils/constants.d.ts +18 -0
  195. package/dist/utils/constants.js +27 -0
  196. package/dist/utils/constants.js.map +1 -0
  197. package/dist/utils/csv.d.ts +18 -0
  198. package/dist/utils/csv.js +45 -0
  199. package/dist/utils/csv.js.map +1 -0
  200. package/dist/utils/file-utils.d.ts +69 -0
  201. package/dist/utils/file-utils.js +158 -0
  202. package/dist/utils/file-utils.js.map +1 -0
  203. package/dist/utils/json.d.ts +61 -0
  204. package/dist/utils/json.js +108 -0
  205. package/dist/utils/json.js.map +1 -0
  206. package/dist/utils/lexorank.d.ts +59 -0
  207. package/dist/utils/lexorank.js +159 -0
  208. package/dist/utils/lexorank.js.map +1 -0
  209. package/dist/utils/log-utils.d.ts +40 -0
  210. package/dist/utils/log-utils.js +109 -0
  211. package/dist/utils/log-utils.js.map +1 -0
  212. package/dist/utils/random.d.ts +19 -0
  213. package/dist/utils/random.js +34 -0
  214. package/dist/utils/random.js.map +1 -0
  215. package/dist/utils/resource-utils.d.ts +45 -0
  216. package/dist/utils/resource-utils.js +137 -0
  217. package/dist/utils/resource-utils.js.map +1 -0
  218. package/dist/utils/sanitize-svg.d.ts +18 -0
  219. package/dist/utils/sanitize-svg.js +38 -0
  220. package/dist/utils/sanitize-svg.js.map +1 -0
  221. package/dist/utils/user-preferences.d.ts +64 -0
  222. package/dist/utils/user-preferences.js +106 -0
  223. package/dist/utils/user-preferences.js.map +1 -0
  224. package/dist/utils/validate.d.ts +26 -0
  225. package/dist/utils/validate.js +53 -0
  226. package/dist/utils/validate.js.map +1 -0
  227. package/dist/utils/value-utils.d.ts +58 -0
  228. package/dist/utils/value-utils.js +181 -0
  229. package/dist/utils/value-utils.js.map +1 -0
  230. package/package.json +67 -0
  231. package/src/card-metadata-updater.ts +182 -0
  232. package/src/command-handler.ts +686 -0
  233. package/src/command-manager.ts +99 -0
  234. package/src/commands/calculate.ts +591 -0
  235. package/src/commands/create.ts +559 -0
  236. package/src/commands/edit.ts +123 -0
  237. package/src/commands/export-site.ts +356 -0
  238. package/src/commands/export.ts +315 -0
  239. package/src/commands/import.ts +169 -0
  240. package/src/commands/index.ts +42 -0
  241. package/src/commands/move.ts +451 -0
  242. package/src/commands/remove.ts +244 -0
  243. package/src/commands/rename.ts +378 -0
  244. package/src/commands/show.ts +442 -0
  245. package/src/commands/transition.ts +127 -0
  246. package/src/commands/update.ts +76 -0
  247. package/src/commands/validate.ts +962 -0
  248. package/src/containers/card-container.ts +378 -0
  249. package/src/containers/project/project-paths.ts +127 -0
  250. package/src/containers/project/resource-collector.ts +379 -0
  251. package/src/containers/project.ts +1135 -0
  252. package/src/containers/template.ts +573 -0
  253. package/src/exceptions/index.ts +29 -0
  254. package/src/index.ts +33 -0
  255. package/src/interfaces/adoc.ts +18 -0
  256. package/src/interfaces/macros.ts +54 -0
  257. package/src/interfaces/project-interfaces.ts +208 -0
  258. package/src/interfaces/request-status-interfaces.ts +30 -0
  259. package/src/interfaces/resource-interfaces.ts +179 -0
  260. package/src/macros/base-macro.ts +176 -0
  261. package/src/macros/common.ts +24 -0
  262. package/src/macros/createCards/index.ts +57 -0
  263. package/src/macros/createCards/metadata.ts +21 -0
  264. package/src/macros/graph/index.ts +130 -0
  265. package/src/macros/graph/metadata.ts +21 -0
  266. package/src/macros/index.ts +321 -0
  267. package/src/macros/report/index.ts +88 -0
  268. package/src/macros/report/metadata.ts +21 -0
  269. package/src/macros/scoreCard/index.ts +55 -0
  270. package/src/macros/scoreCard/metadata.ts +21 -0
  271. package/src/macros/task-queue.ts +79 -0
  272. package/src/module-manager.ts +443 -0
  273. package/src/permissions/action-guard.ts +77 -0
  274. package/src/project-settings.ts +140 -0
  275. package/src/resources/array-handler.ts +141 -0
  276. package/src/resources/card-type-resource.ts +455 -0
  277. package/src/resources/create-defaults.ts +216 -0
  278. package/src/resources/field-type-resource.ts +533 -0
  279. package/src/resources/file-resource.ts +433 -0
  280. package/src/resources/folder-resource.ts +140 -0
  281. package/src/resources/graph-model-resource.ts +205 -0
  282. package/src/resources/graph-view-resource.ts +199 -0
  283. package/src/resources/link-type-resource.ts +191 -0
  284. package/src/resources/report-resource.ts +224 -0
  285. package/src/resources/resource-object.ts +246 -0
  286. package/src/resources/template-resource.ts +210 -0
  287. package/src/resources/workflow-resource.ts +205 -0
  288. package/src/types/queries.ts +149 -0
  289. package/src/utils/card-utils.ts +83 -0
  290. package/src/utils/clingo-fact-builder.ts +167 -0
  291. package/src/utils/clingo-facts.ts +550 -0
  292. package/src/utils/clingo-parser.ts +519 -0
  293. package/src/utils/clingo-program-builder.ts +71 -0
  294. package/src/utils/common-utils.ts +54 -0
  295. package/src/utils/constants.ts +32 -0
  296. package/src/utils/csv.ts +53 -0
  297. package/src/utils/file-utils.ts +182 -0
  298. package/src/utils/json.ts +118 -0
  299. package/src/utils/lexorank.ts +180 -0
  300. package/src/utils/log-utils.ts +127 -0
  301. package/src/utils/random.ts +37 -0
  302. package/src/utils/resource-utils.ts +180 -0
  303. package/src/utils/sanitize-svg.ts +46 -0
  304. package/src/utils/user-preferences.ts +126 -0
  305. package/src/utils/validate.ts +66 -0
  306. package/src/utils/value-utils.ts +189 -0
@@ -0,0 +1,962 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2024
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
+ // node
15
+ import { type Dirent } from 'node:fs';
16
+ import { basename, dirname, extname, join, parse, resolve } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { readdir } from 'node:fs/promises';
19
+
20
+ // dependencies
21
+ import { Validator as JSONValidator, type Schema } from 'jsonschema';
22
+ import { Validator as DirectoryValidator } from 'directory-schema-validator';
23
+ import { parentSchema, schemas } from '@cyberismo/resources';
24
+
25
+ // data-handler
26
+ import type {
27
+ Card,
28
+ DotSchemaContent,
29
+ ProjectSettings,
30
+ ResourceTypes,
31
+ } from '../interfaces/project-interfaces.js';
32
+ import type {
33
+ CardType,
34
+ CustomField,
35
+ FieldType,
36
+ ReportMetadata,
37
+ ResourceContent,
38
+ Workflow,
39
+ } from '../interfaces/resource-interfaces.js';
40
+ import { errorFunction } from '../utils/log-utils.js';
41
+ import { isTemplateCard } from '../utils/card-utils.js';
42
+ import { pathExists } from '../utils/file-utils.js';
43
+ import { Project } from '../containers/project.js';
44
+ import { readJsonFile } from '../utils/json.js';
45
+ import { resourceName } from '../utils/resource-utils.js';
46
+
47
+ const invalidNames = new RegExp(
48
+ '[<>:"/\\|?*\x00-\x1F]|^(?:aux|con|clock$|nul|prn|com[1-9]|lpt[1-9])$', // eslint-disable-line no-control-regex
49
+ );
50
+
51
+ const SHORT_TEXT_MAX_LENGTH = 80;
52
+
53
+ import * as EmailValidator from 'email-validator';
54
+ import { evaluateMacros } from '../macros/index.js';
55
+
56
+ const baseDir = dirname(fileURLToPath(import.meta.url));
57
+ const subFoldersToValidate = ['.cards', 'cardRoot'];
58
+
59
+ export interface LengthProvider {
60
+ length: number;
61
+ }
62
+
63
+ /**
64
+ * Validates content.
65
+ */
66
+ export class Validate {
67
+ private static instance: Validate;
68
+
69
+ validator: JSONValidator;
70
+ directoryValidator: DirectoryValidator;
71
+
72
+ private parentSchema: Schema;
73
+
74
+ private validatedCardTypes: Map<string, CardType>;
75
+ private validatedWorkflows: Map<string, Workflow>;
76
+ private validatedFieldTypes: Map<string, FieldType>;
77
+
78
+ static baseFolder: string;
79
+ static jsonFileExtension = '.json';
80
+ static parentSchemaFile: string;
81
+ static schemaConfigurationFile = '.schema';
82
+ static projectConfigurationFile = 'cardsConfig.json';
83
+ static cardMetadataFile = 'index.json';
84
+ static dotSchemaSchemaId = '/dotSchema';
85
+ static parameterSchemaFile = 'parameterSchema.json';
86
+
87
+ constructor() {
88
+ Validate.baseFolder = pathExists(
89
+ join(process.cwd(), '../../schema', 'cardTreeDirectorySchema.json'),
90
+ )
91
+ ? join(process.cwd(), '../../schema')
92
+ : join(baseDir, '../../../schema');
93
+ Validate.parentSchemaFile = join(
94
+ Validate.baseFolder,
95
+ 'cardTreeDirectorySchema.json',
96
+ );
97
+ this.validator = new JSONValidator();
98
+ this.directoryValidator = new DirectoryValidator();
99
+ this.parentSchema = parentSchema;
100
+ this.addChildSchemas();
101
+ this.validatedFieldTypes = new Map();
102
+ this.validatedWorkflows = new Map();
103
+ this.validatedCardTypes = new Map();
104
+ }
105
+
106
+ // Helper to get length from types when needed.
107
+ private length<T extends LengthProvider>(item: T): number {
108
+ return item.length;
109
+ }
110
+
111
+ // Loads child schemas to validator.
112
+ private addChildSchemas() {
113
+ schemas.forEach((schema) => {
114
+ this.validator.addSchema(schema, schema.$id);
115
+ });
116
+ }
117
+
118
+ // Validates that 'name' in resources matches filename, location and project prefix.
119
+ private checkResourceName(
120
+ file: Dirent,
121
+ content:
122
+ | ResourceContent
123
+ | CustomField
124
+ | DotSchemaContent
125
+ | ProjectSettings
126
+ | ReportMetadata,
127
+ projectPrefixes: string[],
128
+ ): string[] {
129
+ const errors: string[] = [];
130
+ const fullFileNameWithPath = this.fullPath(file);
131
+ // Exclude cardsConfig.json, .schemas and resource specific JSON files.
132
+ if (
133
+ file.name !== Validate.projectConfigurationFile &&
134
+ file.name !== Validate.cardMetadataFile &&
135
+ file.name !== Validate.dotSchemaSchemaId &&
136
+ file.name !== Validate.parameterSchemaFile
137
+ ) {
138
+ const namedContent = content as ResourceContent | ReportMetadata;
139
+ if (!namedContent.name) {
140
+ errors.push(
141
+ `File '${file.name}' does not contain 'name' property. Cannot validate resource's 'name'.`,
142
+ );
143
+ return errors;
144
+ }
145
+ const { identifier, prefix, type } = resourceName(namedContent.name);
146
+ const filenameWithoutExtension = parse(file.name).name;
147
+
148
+ if (!projectPrefixes.includes(prefix)) {
149
+ errors.push(
150
+ `Wrong prefix in resource '${namedContent.name}'. Project prefixes are '[${projectPrefixes.join(', ')}]'`,
151
+ );
152
+ }
153
+ if (identifier !== filenameWithoutExtension) {
154
+ errors.push(
155
+ `Resource 'name' ${namedContent.name} mismatch with file path '${fullFileNameWithPath}'`,
156
+ );
157
+ }
158
+ if (!fullFileNameWithPath.includes(type)) {
159
+ errors.push(
160
+ `Wrong type name in resource '${namedContent.name}'. Should match filename path: '${fullFileNameWithPath}'`,
161
+ );
162
+ }
163
+ }
164
+ return errors;
165
+ }
166
+
167
+ // Return full path and filename.
168
+ private fullPath(file: Dirent): string {
169
+ return join(file.parentPath, file.name);
170
+ }
171
+
172
+ // Puts resource to a local cache if found and returns the resource.
173
+ // If value is already cached, returns from cache.
174
+ private async getAndCacheResource<Type>(
175
+ project: Project,
176
+ cachedValues: Map<string, Type>,
177
+ valueName: string,
178
+ ): Promise<Type | undefined> {
179
+ return (
180
+ cachedValues.get(valueName) ||
181
+ project.resource<Type>(valueName).then((resource) => {
182
+ if (!resource) {
183
+ return undefined;
184
+ }
185
+ cachedValues.set(valueName, resource);
186
+ return resource;
187
+ })
188
+ );
189
+ }
190
+
191
+ private parseValidatorMessage(errorObject: object[]): string {
192
+ let parsedErrorMessage = '';
193
+ // todo: get schema name here?
194
+ for (const error of errorObject) {
195
+ let instancePath = '';
196
+ let params = '';
197
+ let message = '';
198
+ let fileError = false;
199
+ if (Object.prototype.hasOwnProperty.call(error, 'instancePath')) {
200
+ const temp = Object(error)['instancePath'];
201
+ if (temp.endsWith('files')) fileError = true;
202
+ instancePath = temp;
203
+ instancePath = instancePath.replace(/\/directories/g, '');
204
+ instancePath = instancePath.replace(/\/files/g, '');
205
+ if (instancePath === '') {
206
+ instancePath = 'project root';
207
+ }
208
+ if (instancePath[0] === '/') {
209
+ instancePath = instancePath.slice(1);
210
+ }
211
+ }
212
+ if (Object.prototype.hasOwnProperty.call(error, 'params')) {
213
+ params = Object(error)['params']['additionalProperty'];
214
+ }
215
+ if (Object.prototype.hasOwnProperty.call(error, 'message')) {
216
+ message = Object(error)['message'];
217
+ message = message.replace(
218
+ 'must have required property',
219
+ fileError ? 'must have file' : 'must have subdirectory',
220
+ );
221
+ if (message === 'must NOT have additional properties') {
222
+ message = message.replace(
223
+ 'must NOT have additional properties',
224
+ 'non-allowed additional property',
225
+ );
226
+ message = message + `: ${params}`;
227
+ }
228
+ }
229
+ parsedErrorMessage += `\nAt '${instancePath}' ${message}`;
230
+ }
231
+ return parsedErrorMessage;
232
+ }
233
+
234
+ // Handles reading and validating 'contentSchema' in a directory.
235
+ private async readAndValidateContentFiles(
236
+ project: Project,
237
+ path: string,
238
+ ): Promise<string[]> {
239
+ const message: string[] = [];
240
+ try {
241
+ const prefixes = await project.projectPrefixes();
242
+ const files = await readdir(path, {
243
+ withFileTypes: true,
244
+ });
245
+
246
+ const foldersToValidate = files.filter(
247
+ (dirent) =>
248
+ dirent.isDirectory() && subFoldersToValidate.includes(dirent.name),
249
+ );
250
+
251
+ // Validate subfolders parallel.
252
+ const promises: Promise<string[]>[] = [];
253
+ foldersToValidate.forEach((folder) => {
254
+ promises.push(this.validateFolder(prefixes, folder));
255
+ });
256
+ const result = await Promise.all(promises);
257
+ message.push(...result.flat(1));
258
+ } catch (error) {
259
+ throw new Error(errorFunction(error));
260
+ }
261
+ return message;
262
+ }
263
+
264
+ // Removes same items from an array.
265
+ private removeDuplicateEntries(
266
+ value: string,
267
+ index: number,
268
+ array: string[],
269
+ ) {
270
+ return array.indexOf(value) === index;
271
+ }
272
+
273
+ // Validate one subfolder.
274
+ private async validateFolder(prefixes: string[], path: Dirent) {
275
+ const messages: string[] = [];
276
+ const files = await readdir(this.fullPath(path), {
277
+ withFileTypes: true,
278
+ recursive: true,
279
+ });
280
+ const schemaFiles = files.filter(
281
+ (dirent) =>
282
+ dirent.isFile() && dirent.name === Validate.schemaConfigurationFile,
283
+ );
284
+
285
+ messages.push(...(await this.validateSchemaFiles(schemaFiles)));
286
+
287
+ // no point in validating contents if .schema files are not valid
288
+ if (messages.length !== 0) {
289
+ return messages;
290
+ }
291
+
292
+ const schemaConfigs = (
293
+ await Promise.all(
294
+ schemaFiles.map(async (dirent) => ({
295
+ dirent,
296
+ content: await readJsonFile(this.fullPath(dirent)),
297
+ })),
298
+ )
299
+ ).reduce<Record<string, DotSchemaContent>>((acc, { dirent, content }) => {
300
+ acc[dirent.parentPath] = content;
301
+ return acc;
302
+ }, {});
303
+
304
+ // Fetches nearest parent's .schema file.
305
+ function schemaConfigFile(
306
+ path: string,
307
+ schemaConfigs: Record<string, DotSchemaContent>,
308
+ ) {
309
+ let schemas = schemaConfigs[path];
310
+ let parentPath = path;
311
+ while (!schemas) {
312
+ parentPath = resolve(parentPath, '..');
313
+ if (dirname(parentPath) === parentPath) {
314
+ break;
315
+ }
316
+ schemas = schemaConfigs[parentPath];
317
+ }
318
+ return schemas;
319
+ }
320
+
321
+ // Go through every file
322
+ for (const file of files.filter(
323
+ (dirent) =>
324
+ dirent.isFile() &&
325
+ dirent.name !== Validate.schemaConfigurationFile &&
326
+ extname(dirent.name) === Validate.jsonFileExtension,
327
+ )) {
328
+ const fullPath = this.fullPath(file);
329
+ const content = await readJsonFile(fullPath);
330
+ const nameErrors = this.checkResourceName(file, content, prefixes);
331
+
332
+ if (nameErrors) {
333
+ messages.push(...nameErrors);
334
+ }
335
+ const schemas = schemaConfigFile(file.parentPath, schemaConfigs);
336
+ // if schema is not defined for the directory, skip it
337
+ if (!schemas) {
338
+ continue;
339
+ }
340
+ const fileSchema = schemas.find(
341
+ (schema) =>
342
+ schema.file === file.name || (schemas.length === 1 && !schema.file),
343
+ );
344
+ if (!fileSchema) {
345
+ continue;
346
+ }
347
+
348
+ if (!fileSchema.id.startsWith('/')) {
349
+ fileSchema.id = '/' + fileSchema.id;
350
+ }
351
+
352
+ const schema = this.validator.schemas[fileSchema.id];
353
+ if (!schema) {
354
+ throw new Error(`Unknown schema name '${fileSchema.id}', aborting.`);
355
+ }
356
+ const result = this.validator.validate(content, schema);
357
+ for (const error of result.errors) {
358
+ const msg = `Validation error from '${fullPath}': ${error.message}.`;
359
+ messages.push(msg);
360
+ }
361
+ }
362
+ return messages;
363
+ }
364
+
365
+ // Handles validating .schema files
366
+ private async validateSchemaFiles(files: Dirent[]) {
367
+ const schema = this.validator.schemas[Validate.dotSchemaSchemaId];
368
+
369
+ if (!schema) {
370
+ throw new Error(`'${Validate.dotSchemaSchemaId}' schema not found`);
371
+ }
372
+
373
+ const message: string[] = [];
374
+
375
+ for (const file of files) {
376
+ const fullPath = this.fullPath(file);
377
+
378
+ const result = this.validator.validate(
379
+ await readJsonFile(fullPath),
380
+ schema,
381
+ );
382
+ for (const error of result.errors) {
383
+ const msg = `Validation error from '${fullPath}': ${error.message}.`;
384
+ message.push(msg);
385
+ }
386
+ }
387
+ return message;
388
+ }
389
+
390
+ // Validate array of custom field names
391
+ private async validateArrayOfFields(
392
+ project: Project,
393
+ cardType: CardType,
394
+ fieldArray: string[],
395
+ nameOfArray: string,
396
+ ) {
397
+ const errors: string[] = [];
398
+ if (cardType && fieldArray) {
399
+ const validationPromises = fieldArray.map(async (field) => {
400
+ const fieldType = await this.getAndCacheResource(
401
+ project,
402
+ this.validatedFieldTypes,
403
+ field,
404
+ );
405
+ if (!fieldType) {
406
+ return `Card type '${cardType.name}' has invalid reference to unknown ${nameOfArray} '${field}'`;
407
+ }
408
+ return null;
409
+ });
410
+
411
+ const results = await Promise.all(validationPromises);
412
+ errors.push(
413
+ ...results.filter((result): result is string => result !== null),
414
+ );
415
+ }
416
+ return errors;
417
+ }
418
+
419
+ // Validates that arrays have only string elements.
420
+ private validateListValues(list: string[]): boolean {
421
+ let valid = true;
422
+ list.forEach((value) => {
423
+ if (typeof value !== 'string') {
424
+ valid = false;
425
+ }
426
+ });
427
+ return valid;
428
+ }
429
+
430
+ // Validates that card's dataType can be used with JS types.
431
+ private validType<T>(value: T, fieldType: FieldType): boolean {
432
+ const field = fieldType.dataType;
433
+ const typeOfValue = typeof value;
434
+
435
+ // Nulls are always accepted.
436
+ if (typeOfValue === 'object' && value === null) {
437
+ return true;
438
+ }
439
+
440
+ if (field === 'date' || field === 'dateTime') {
441
+ return !isNaN(Date.parse(<string>value));
442
+ }
443
+ if (field === 'list') {
444
+ return Array.isArray(value) && this.validateListValues(<string[]>value);
445
+ }
446
+ if (field === 'boolean' || field === 'number') {
447
+ return typeOfValue === field;
448
+ }
449
+ if (field === 'shortText') {
450
+ return (
451
+ typeOfValue === 'string' &&
452
+ this.length(<string>value) <= SHORT_TEXT_MAX_LENGTH
453
+ );
454
+ }
455
+ if (field === 'longText') {
456
+ return typeOfValue === 'string';
457
+ }
458
+ if (field === 'integer') {
459
+ return typeOfValue === 'number' && Number.isInteger(value);
460
+ }
461
+ if (field === 'person') {
462
+ // Accept empty names
463
+ return (
464
+ value === undefined ||
465
+ EmailValidator.validate(<string>value) ||
466
+ this.length(<string>value) === 0
467
+ );
468
+ }
469
+ if (field === 'enum') {
470
+ const found = fieldType.enumValues?.find(
471
+ (item) => item.enumValue === value,
472
+ );
473
+ return found ? true : false;
474
+ }
475
+ console.error(`Type ${field} is not supported`);
476
+ return false;
477
+ }
478
+
479
+ /**
480
+ * Validates that new identifier of a resource is according to naming convention.
481
+ * @param identifier: resource identifier
482
+ * returns true if identifier is valid, and false otherwise.
483
+ */
484
+ public static isValidIdentifierName(identifier: string): boolean {
485
+ const validIdentifier = new RegExp('^[A-Za-z0-9 ._-]+$');
486
+ const contentValidated = validIdentifier.test(identifier);
487
+ const lengthValidated = identifier.length > 0 && identifier.length < 256;
488
+ const notInvalidIdentifier = !invalidNames.test(identifier);
489
+ return contentValidated && lengthValidated && notInvalidIdentifier;
490
+ }
491
+
492
+ /**
493
+ * Validates that 'name' can be used as a project name.
494
+ * @param name project name
495
+ * @returns true if name is valid, and false otherwise.
496
+ * @note that on Windows, if path + filename is longer than 256 characters, some file operations
497
+ * are not possible. Thus, setting the maximum length of project name to 64 characters.
498
+ * The 192 characters usually should be enough for the path.
499
+ */
500
+ public static isValidProjectName(name: string): boolean {
501
+ const validName = new RegExp('^[A-Za-z ._-]+$');
502
+ const contentValidated = validName.test(name);
503
+ const lengthValidated = name.length > 0 && name.length < 64;
504
+ const notInvalidName = !invalidNames.test(name);
505
+ return contentValidated && lengthValidated && notInvalidName;
506
+ }
507
+
508
+ /**
509
+ * Validates that 'name' can be used as label name.
510
+ * Labels are less restricted than other names, as they are never file names.
511
+ * @param name label name
512
+ * @returns true if name is valid, and false otherwise.
513
+ */
514
+ public static isValidLabelName(name: string): boolean {
515
+ const validName = new RegExp('^[-a-zA-Z0-9._-]+(?: [a-zA-Z0-9._-]+)*$');
516
+ const contentValidated = validName.test(name);
517
+ const lengthValidated = name.length > 0 && name.length < 256;
518
+ return contentValidated && lengthValidated;
519
+ }
520
+
521
+ /**
522
+ * Validates that a given directory path (and its children) conform to a JSON schema.
523
+ * @note Validates also content in the directory tree, if .schema file is found.
524
+ * @param projectPath path to validate.
525
+ * @returns string containing all validation errors
526
+ */
527
+ public async validate(projectPath: string): Promise<string> {
528
+ let validationErrors = '';
529
+ this.validatedFieldTypes.clear();
530
+ this.validatedWorkflows.clear();
531
+ this.validatedCardTypes.clear();
532
+
533
+ try {
534
+ // First, validate that the directory content conforms to the schema.
535
+ const valid = this.directoryValidator.validate(
536
+ this.parentSchema,
537
+ projectPath,
538
+ );
539
+ if (!valid && this.directoryValidator.errors) {
540
+ const errorMsg = this.parseValidatorMessage(
541
+ this.directoryValidator.errors,
542
+ );
543
+ if (errorMsg) {
544
+ validationErrors = errorMsg;
545
+ }
546
+ return validationErrors;
547
+ } else {
548
+ const errorMsg: string[] = [];
549
+ const project = new Project(projectPath);
550
+
551
+ // Then, validate that each 'contentSchema' children as well.
552
+ const result = await this.readAndValidateContentFiles(
553
+ project,
554
+ projectPath,
555
+ );
556
+ if (result.length > 0) {
557
+ errorMsg.push(...result);
558
+ }
559
+
560
+ // Finally, validate that each card is correct
561
+ const cards = await project.cards();
562
+ cards.push(...(await project.allTemplateCards()));
563
+
564
+ for (const card of cards) {
565
+ if (card.metadata) {
566
+ // validate card's workflow
567
+ if (!isTemplateCard(card)) {
568
+ const validWorkflow = await this.validateWorkflowState(
569
+ project,
570
+ card,
571
+ );
572
+ if (validWorkflow.length !== 0) {
573
+ errorMsg.push(validWorkflow);
574
+ }
575
+ }
576
+ }
577
+
578
+ const validCustomFields = await this.validateCustomFields(
579
+ project,
580
+ card,
581
+ );
582
+ if (validCustomFields.length !== 0) {
583
+ errorMsg.push(validCustomFields);
584
+ }
585
+
586
+ const validLabels = await this.validateCardLabels(card);
587
+ if (validLabels.length > 0) {
588
+ errorMsg.push(validLabels);
589
+ }
590
+
591
+ // Validate macros in content
592
+ if (card.content) {
593
+ await evaluateMacros(card.content, {
594
+ mode: 'validate',
595
+ projectPath,
596
+ cardKey: card.key,
597
+ });
598
+ }
599
+ }
600
+ if (errorMsg.length) {
601
+ validationErrors += errorMsg
602
+ .filter(this.removeDuplicateEntries)
603
+ .join('\n');
604
+ }
605
+ }
606
+ } catch (error) {
607
+ validationErrors += errorFunction(error);
608
+ }
609
+ return validationErrors;
610
+ }
611
+
612
+ /**
613
+ * Validates folder name.
614
+ * @todo: This should check that the path is resolvable and can be used as a folder name in various operating systems.
615
+ * @param path path to a folder
616
+ * @returns true, if the path is valid and can be used; false otherwise.
617
+ */
618
+ public static validateFolder(path: string): boolean {
619
+ if (path === '' || path === '.' || path === '..') {
620
+ return false;
621
+ }
622
+ return !invalidNames.test(basename(path));
623
+ }
624
+
625
+ /**
626
+ * Validates that 'object' conforms to JSON schema 'schemaId'.
627
+ * @param content Object to validate.
628
+ * @param schemaId Schema ID to identify a JSON schema.
629
+ * @returns string containing all validation errors
630
+ */
631
+ public validateJson(content: object, schemaId: string): string {
632
+ const validationErrors: string[] = [];
633
+ if (!schemaId.startsWith('/')) {
634
+ schemaId = '/' + schemaId;
635
+ }
636
+ if (this.validator.schemas[schemaId] === undefined) {
637
+ validationErrors.push(`Unknown schema ${schemaId}`);
638
+ } else {
639
+ const result = this.validator.validate(
640
+ content,
641
+ this.validator.schemas[schemaId],
642
+ );
643
+ for (const error of result.errors) {
644
+ const msg = `Schema '${schemaId}' validation Error: ${error.message}\n`;
645
+ validationErrors.push(msg);
646
+ }
647
+ }
648
+ return validationErrors.join('\n');
649
+ }
650
+
651
+ /**
652
+ * Validate that resource names and identifiers are valid.
653
+ * @param resourceType Type of resource
654
+ * @param name Name of resource
655
+ * @param prefixes currently used project prefixes
656
+ * @returns resource name as valid resource name; throws in error cases.
657
+ */
658
+ public async validResourceName(
659
+ resourceType: ResourceTypes,
660
+ name: string,
661
+ prefixes: string[],
662
+ ): Promise<string> {
663
+ const resource = resourceName(name);
664
+ resource.type = resource.type ? resource.type : resourceType;
665
+ // a bit shaky way to ensure that prefix is set; first of the project prefixes should be the actual project prefix.
666
+ if (resource.prefix === '') {
667
+ resource.prefix = prefixes.length > 0 ? prefixes.at(0) || '' : '';
668
+ if (resource.prefix === '') {
669
+ throw new Error(`Project prefix cannot be empty string`);
670
+ }
671
+ }
672
+ if (!prefixes.includes(resource.prefix)) {
673
+ throw new Error(
674
+ `Resource name can only refer to project that it is part of. Prefix '${resource.prefix}' is not included in '[${prefixes.join(',')}]'`,
675
+ );
676
+ }
677
+ if (resourceType !== resource.type) {
678
+ throw new Error(
679
+ `Resource name must match the resource type. Type '${resource.type}' does not match '${resourceType}'`,
680
+ );
681
+ }
682
+ if (!Validate.isValidIdentifierName(resource.identifier)) {
683
+ throw new Error(
684
+ `Resource identifier must follow naming rules. Identifier '${resource.identifier}' is invalid`,
685
+ );
686
+ }
687
+ return `${resource.prefix}/${resourceType}/${resource.identifier}`;
688
+ }
689
+
690
+ /**
691
+ * Validates that 'prefix' is valid project prefix.
692
+ * @param prefix project prefix
693
+ * @returns true, if prefix can be used as project prefix, false otherwise.
694
+ */
695
+ public static validatePrefix(prefix: string): boolean {
696
+ const validPrefix = new RegExp('^[a-z]+$');
697
+ const contentValidated = validPrefix.test(prefix);
698
+ const lengthValidated = prefix.length > 2 && prefix.length < 11;
699
+ return contentValidated && lengthValidated;
700
+ }
701
+
702
+ /**
703
+ * Validate schema that matches schemaId from path.
704
+ * @param projectPath path to schema
705
+ * @param schemaId schema's id
706
+ * @returns string containing all validation errors
707
+ * @todo - unused; remove?
708
+ */
709
+ public async validateSchema(
710
+ projectPath: string,
711
+ schemaId: string,
712
+ ): Promise<string> {
713
+ const validationErrors: string[] = [];
714
+ if (!schemaId.startsWith('/')) {
715
+ schemaId = '/' + schemaId;
716
+ }
717
+ const activeJsonSchema = this.validator.schemas[schemaId];
718
+ if (activeJsonSchema === undefined) {
719
+ throw new Error(`Unknown schema '${schemaId}'`);
720
+ } else {
721
+ let contentFile = '';
722
+ try {
723
+ contentFile = await readJsonFile(projectPath);
724
+ } catch {
725
+ throw new Error(`Path is not valid ${projectPath}`);
726
+ }
727
+
728
+ const result = this.validator.validate(contentFile, activeJsonSchema);
729
+ for (const error of result.errors) {
730
+ const msg = `Schema '${schemaId}' validation Error: ${error.message}\n`;
731
+ validationErrors.push(msg);
732
+ }
733
+ }
734
+ return validationErrors.join('\n');
735
+ }
736
+
737
+ /**
738
+ * Validates that card's custom fields are according to schema and have correct data in them.
739
+ * @param project currently used Project
740
+ * @param card specific card
741
+ * @returns string containing all validation errors
742
+ */
743
+ public async validateCustomFields(
744
+ project: Project,
745
+ card: Card,
746
+ ): Promise<string> {
747
+ const validationErrors: string[] = [];
748
+
749
+ if (!card.metadata) {
750
+ throw new Error(
751
+ `Card '${card.key}' has no metadata. Card object needs to be instantiated with '{metadata: true}'`,
752
+ );
753
+ }
754
+
755
+ const cardType = await this.getAndCacheResource(
756
+ project,
757
+ this.validatedCardTypes,
758
+ card.metadata?.cardType,
759
+ );
760
+
761
+ if (!cardType) {
762
+ validationErrors.push(
763
+ `Card '${card.key}' has invalid card type '${card.metadata?.cardType}'`,
764
+ );
765
+ return validationErrors.join('\n');
766
+ }
767
+
768
+ // Check that arrays of field types refer to existing fields.
769
+ let fieldErrors = await this.validateArrayOfFields(
770
+ project,
771
+ cardType,
772
+ cardType.optionallyVisibleFields,
773
+ 'optionally visible fields',
774
+ );
775
+ validationErrors.push(...fieldErrors);
776
+ fieldErrors = await this.validateArrayOfFields(
777
+ project,
778
+ cardType,
779
+ cardType.alwaysVisibleFields,
780
+ 'always visible fields',
781
+ );
782
+ validationErrors.push(...fieldErrors);
783
+
784
+ for (const field of cardType.customFields) {
785
+ const found = await project.resourceExists('fieldTypes', field.name);
786
+ if (!found) {
787
+ validationErrors.push(
788
+ `Custom field '${field.name}' from card type '${cardType.name}' not found from project`,
789
+ );
790
+ }
791
+ if (field.isCalculated) {
792
+ if (card.metadata[field.name] !== undefined) {
793
+ validationErrors.push(
794
+ `Card '${card.key}' not allowed to have a value in a calculated field '${field.name}'`,
795
+ );
796
+ }
797
+ continue;
798
+ } else {
799
+ if (card.metadata[field.name] === undefined) {
800
+ validationErrors.push(
801
+ `Card '${card.key}' is missing custom field '${field.name}'`,
802
+ );
803
+ continue;
804
+ }
805
+ }
806
+
807
+ const fieldType = await this.getAndCacheResource(
808
+ project,
809
+ this.validatedFieldTypes,
810
+ field.name,
811
+ );
812
+
813
+ if (!fieldType) {
814
+ validationErrors.push(
815
+ `In card '${card.key}' field '${field.name}' is missing from project\n`,
816
+ );
817
+ continue;
818
+ }
819
+
820
+ if (!this.validType(card.metadata[field.name], fieldType)) {
821
+ const typeOfValue = typeof card.metadata[field.name];
822
+ let fieldValue = card.metadata[field.name];
823
+ if (typeOfValue === 'string') {
824
+ fieldValue = card.metadata[field.name]
825
+ ? `"${card.metadata[field.name]}"`
826
+ : '""';
827
+ }
828
+ if (fieldType.dataType === 'enum') {
829
+ const listOfEnumValues = fieldType.enumValues?.map(
830
+ (item) => item.enumValue,
831
+ );
832
+ validationErrors.push(
833
+ `In card '${card.key}' field '${field.name}' is defined as '${fieldType.dataType}', possible enumerations are: ${listOfEnumValues?.join(', ')}\n`,
834
+ );
835
+ continue;
836
+ }
837
+ if (fieldType.dataType === 'person') {
838
+ validationErrors.push(
839
+ `In card '${card.key}' field '${field.name}' value '${card.metadata[field.name]}' cannot be used as '${fieldType.dataType}'. Not a valid email address.'`,
840
+ );
841
+ continue;
842
+ }
843
+ validationErrors.push(
844
+ `In card '${card.key}' field '${field.name}' is defined as '${fieldType.dataType}', but it is '${typeOfValue}' with value of ${fieldValue}\n`,
845
+ );
846
+ }
847
+ }
848
+
849
+ return validationErrors.join('\n');
850
+ }
851
+
852
+ /**
853
+ * Validates the labels of a card
854
+ * @param card card to validate. Card must have metadata.
855
+ */
856
+ public async validateCardLabels(card: Card): Promise<string> {
857
+ const validationErrors: string[] = [];
858
+ if (!card.metadata) {
859
+ validationErrors.push(
860
+ `Card '${card.key}' has no metadata. Card object needs to be instantiated with '{metadata: true}'`,
861
+ );
862
+ }
863
+ // labels are not mandatory
864
+ if (card.metadata?.labels) {
865
+ if (!Array.isArray(card.metadata?.labels)) {
866
+ validationErrors.push(
867
+ `In card '${card.key}' expected labels to be an array of strings, but instead got ${card.metadata.labels}`,
868
+ );
869
+ } else {
870
+ for (const label of card.metadata.labels) {
871
+ // labels follow same name guidance as resource names
872
+ if (!Validate.isValidLabelName(label)) {
873
+ validationErrors.push(
874
+ `In card '${card.key}' label '${label}' does not follow naming rules`,
875
+ );
876
+ }
877
+ }
878
+ }
879
+ }
880
+ return validationErrors.join('\n');
881
+ }
882
+
883
+ /**
884
+ * Checks if card's current workflow state matches workflow that card's card type is using.
885
+ * Template cards are expected to have empty workflow state.
886
+ * @param project Project object.
887
+ * @param card Card object to validate
888
+ * @returns string containing all validation errors
889
+ */
890
+ public async validateWorkflowState(
891
+ project: Project,
892
+ card: Card,
893
+ ): Promise<string> {
894
+ const validationErrors: string[] = [];
895
+
896
+ if (!card.metadata) {
897
+ validationErrors.push(
898
+ `Card '${card.key}' has no metadata. Card object needs to be instantiated with '{metadata: true}'`,
899
+ );
900
+ }
901
+
902
+ // Use caches for cardTypes and workflows, to avoid re-reading the same JSON files multiple times.
903
+ const cardType = await this.getAndCacheResource(
904
+ project,
905
+ this.validatedCardTypes,
906
+ card.metadata?.cardType || '',
907
+ );
908
+ if (!cardType) {
909
+ validationErrors.push(
910
+ `Card '${card.key}' has invalid card type '${card.metadata?.cardType}'`,
911
+ );
912
+ return validationErrors.join('\n');
913
+ }
914
+ if (!cardType.workflow) {
915
+ validationErrors.push(
916
+ `Card type '${card.metadata?.cardType}' does not have 'workflow'`,
917
+ );
918
+ return validationErrors.join('\n');
919
+ }
920
+
921
+ const workflow = await this.getAndCacheResource(
922
+ project,
923
+ this.validatedWorkflows,
924
+ cardType.workflow,
925
+ );
926
+
927
+ if (!workflow) {
928
+ validationErrors.push(
929
+ `Workflow of '${cardType.workflow}' card type '${card.metadata?.cardType}' does not exist in the project`,
930
+ );
931
+ return validationErrors.join('\n');
932
+ }
933
+
934
+ const cardState = card.metadata?.workflowState;
935
+ if (!isTemplateCard(card)) {
936
+ const found = workflow.states.find((item) => item.name === cardState);
937
+ if (!found) {
938
+ validationErrors.push(
939
+ `Card '${card.key}' has invalid state '${cardState}'`,
940
+ );
941
+ }
942
+ } else {
943
+ if (cardState) {
944
+ validationErrors.push(
945
+ `Template card ${card.key} must have empty "workflowState"`,
946
+ );
947
+ }
948
+ }
949
+ return validationErrors.join('\n');
950
+ }
951
+
952
+ /**
953
+ * Possibly creates (if no instance exists) and returns an instance of Validate command.
954
+ * @returns instance of Validate command.
955
+ */
956
+ public static getInstance(): Validate {
957
+ if (!Validate.instance) {
958
+ Validate.instance = new Validate();
959
+ }
960
+ return Validate.instance;
961
+ }
962
+ }