cassproject 5.0.2 → 5.0.3

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.
package/README.md CHANGED
@@ -50,6 +50,10 @@ Development unit tests presume you have a CaSS Repository running on `localhost:
50
50
 
51
51
  # Changelog
52
52
 
53
+ ## 5.0.3
54
+ * Updates to CEASN import/export.
55
+ * Security updates.
56
+
53
57
  ## 5.0.2
54
58
  * Cosmetic and security updates (to dev dependencies)
55
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassproject",
3
- "version": "5.0.2",
3
+ "version": "5.0.3",
4
4
  "description": "Competency and Skills Service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,12 +10,12 @@
10
10
  "multitest": "concurrently --kill-others-on-fail \"npm run test15\" \"npm run test14\" \"npm run test13\" \"npm run test12\"",
11
11
  "testCassTest": "npm run testkill && docker run -d --platform linux/amd64 --network cass-net --name cass-test -p80:80 -e CASS_LOOPBACK cass-test && wait-on http://localhost/api/ping && npm run testNode18 && npm run testNode18Fips && npm run testNode16 && npm run testNode15 && npm run testNode14 && npm run testNode13 && npm run testNode12 && npm run testCypressEdge && npm run testCypress && npm run testkill",
12
12
  "testDevHttps": "npm run testkill && docker run -d --platform linux/amd64 --network cass-net --name cass-test -p443:80 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:dev && wait-on https://localhost/api/ping && npm run testNode18HttpsFips && npm run testNode18Https && npm run testNode16Https && npm run testNode15Https && npm run testNode14Https && npm run testNode13Https && npm run testNode12Https && npm run testCypressEdgeHttps && npm run testCypressHttps",
13
- "test16HttpsFips": "export CASS_LOOPBACK=https://cass-testsf16/api/|| set CASS_LOOPBACK=https://cass-testsf16/api/&& npm run testkillsf16 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf16 -p450:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.6.4 && wait-on https://localhost:450/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode24HttpsForceFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf16",
14
- "test16Https11Fips": "export CASS_LOOPBACK=https://cass-testsf1116/api/|| set CASS_LOOPBACK=https://cass-testsf1116/api/&& npm run testkillsf1116 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf1116 -p449:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.6.4 && wait-on https://localhost:449/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode24HttpsForceFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf1116",
15
- "test16Https": "export CASS_LOOPBACK=https://cass-tests16/api/|| set CASS_LOOPBACK=https://cass-tests16/api/&& npm run testkills16 && docker run -d --platform linux/amd64 --network cass-net --name cass-tests16 -p448:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.6.4 && wait-on https://localhost:448/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode18Https\" && npm run testkills16",
16
- "test16Https11": "export CASS_LOOPBACK=https://cass-tests1116/api/|| set CASS_LOOPBACK=https://cass-tests1116/api/&& npm run testkills1116 && docker run -d --platform linux/amd64 --network cass-net --name cass-tests1116 -p451:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.6.4 && wait-on https://localhost:451/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode18Https\" && npm run testkills1116",
17
- "test16Fips": "export CASS_LOOPBACK=http://cass-testf16/api/|| set CASS_LOOPBACK=http://cass-testf16/api/&& npm run testkillf16 && docker run -d --platform linux/amd64 --network cass-net -e CASS_LOOPBACK --name cass-testf16 -p83:80 cassproject/cass:1.6.4 && wait-on http://localhost:83/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode24\" \"npm run testNode24Fips\" \"npm run testNode24ForceFips\" \"npm run testNode22\" \"npm run testNode22Fips\" \"npm run testNode22ForceFips\" \"npm run testNode20\" \"npm run testNode20Fips\" \"npm run testNode20ForceFips\" \"npm run testNode18\" && npm run testkillf16",
18
- "test16": "export CASS_LOOPBACK=http://cass-test16/api/|| set CASS_LOOPBACK=http://cass-test16/api/&& npm run testkill16 && docker run -d --platform linux/amd64 --network cass-net -e CASS_LOOPBACK --name cass-test16 -p82:80 cassproject/cass:1.6.4 && wait-on http://localhost:82/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode24\" \"npm run testNode24Fips\" \"npm run testNode22\" \"npm run testNode22Fips\" \"npm run testNode20\" \"npm run testNode20Fips\" \"npm run testNode18\" && npm run testkill16",
13
+ "test16HttpsFips": "export CASS_LOOPBACK=https://cass-testsf16/api/|| set CASS_LOOPBACK=https://cass-testsf16/api/&& npm run testkillsf16 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf16 -p450:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.6.6 && wait-on https://localhost:450/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode24HttpsForceFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf16",
14
+ "test16Https11Fips": "export CASS_LOOPBACK=https://cass-testsf1116/api/|| set CASS_LOOPBACK=https://cass-testsf1116/api/&& npm run testkillsf1116 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf1116 -p449:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.6.6 && wait-on https://localhost:449/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode24HttpsForceFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf1116",
15
+ "test16Https": "export CASS_LOOPBACK=https://cass-tests16/api/|| set CASS_LOOPBACK=https://cass-tests16/api/&& npm run testkills16 && docker run -d --platform linux/amd64 --network cass-net --name cass-tests16 -p448:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.6.6 && wait-on https://localhost:448/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode18Https\" && npm run testkills16",
16
+ "test16Https11": "export CASS_LOOPBACK=https://cass-tests1116/api/|| set CASS_LOOPBACK=https://cass-tests1116/api/&& npm run testkills1116 && docker run -d --platform linux/amd64 --network cass-net --name cass-tests1116 -p451:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.6.6 && wait-on https://localhost:451/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode18Https\" && npm run testkills1116",
17
+ "test16Fips": "export CASS_LOOPBACK=http://cass-testf16/api/|| set CASS_LOOPBACK=http://cass-testf16/api/&& npm run testkillf16 && docker run -d --platform linux/amd64 --network cass-net -e CASS_LOOPBACK --name cass-testf16 -p83:80 cassproject/cass:1.6.6 && wait-on http://localhost:83/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode24\" \"npm run testNode24Fips\" \"npm run testNode24ForceFips\" \"npm run testNode22\" \"npm run testNode22Fips\" \"npm run testNode22ForceFips\" \"npm run testNode20\" \"npm run testNode20Fips\" \"npm run testNode20ForceFips\" \"npm run testNode18\" && npm run testkillf16",
18
+ "test16": "export CASS_LOOPBACK=http://cass-test16/api/|| set CASS_LOOPBACK=http://cass-test16/api/&& npm run testkill16 && docker run -d --platform linux/amd64 --network cass-net -e CASS_LOOPBACK --name cass-test16 -p82:80 cassproject/cass:1.6.6 && wait-on http://localhost:82/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode24\" \"npm run testNode24Fips\" \"npm run testNode22\" \"npm run testNode22Fips\" \"npm run testNode20\" \"npm run testNode20Fips\" \"npm run testNode18\" && npm run testkill16",
19
19
  "test15HttpsFips": "export TESTLEVEL=15|| set TESTLEVEL=15&& export CASS_LOOPBACK=https://cass-testsf15/api/|| set CASS_LOOPBACK=https://cass-testsf15/api/&& npm run testkillsf15 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf15 -p446:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.5.75 && wait-on https://localhost:446/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf15",
20
20
  "test15Https11Fips": "export TESTLEVEL=15|| set TESTLEVEL=15&& export CASS_LOOPBACK=https://cass-testsf1115/api/|| set CASS_LOOPBACK=https://cass-testsf1115/api/&& npm run testkillsf1115 && docker run -d --platform linux/amd64 --network cass-net --name cass-testsf1115 -p447:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.5.75 && wait-on https://localhost:447/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode24HttpsForceFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode22HttpsForceFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode20HttpsForceFips\" \"npm run testNode18Https\" && npm run testkillsf1115",
21
21
  "test15Https": "export TESTLEVEL=15|| set TESTLEVEL=15&& export CASS_LOOPBACK=https://cass-tests15/api/|| set CASS_LOOPBACK=https://cass-tests15/api/&& npm run testkills15 && docker run -d --platform linux/amd64 --network cass-net --name cass-tests15 -p444:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.5.75 && wait-on https://localhost:444/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode24HttpsFips\" \"npm run testNode22Https\" \"npm run testNode22HttpsFips\" \"npm run testNode20Https\" \"npm run testNode20HttpsFips\" \"npm run testNode18Https\" && npm run testkills15",
@@ -188,10 +188,10 @@
188
188
  "chai": "4.5.0",
189
189
  "concurrently": "^9.2.1",
190
190
  "convert-hrtime": "^5.0.0",
191
- "cypress": "^15.2.0",
191
+ "cypress": "^15.4.0",
192
192
  "cypress-fail-fast": "^7.1.1",
193
- "eslint": "^9.35.0",
194
- "fake-indexeddb": "^6.2.2",
193
+ "eslint": "^9.37.0",
194
+ "fake-indexeddb": "^6.2.3",
195
195
  "mocha": "^10.8.2",
196
196
  "node-polyfill-webpack-plugin": "^4.1.0",
197
197
  "nodemon": "^3.1.10",
@@ -199,7 +199,7 @@
199
199
  "sinon": "^19.0.5",
200
200
  "url-polyfill": "^1.1.14",
201
201
  "wait-on": "^8.0.5",
202
- "webpack": "^5.101.3",
202
+ "webpack": "^5.102.1",
203
203
  "webpack-cli": "^5.1.4"
204
204
  },
205
205
  "packageManager": "npm@11.3.0+sha512.96eb611483f49c55f7fa74df61b588de9e213f80a256728e6798ddc67176c7b07e4a1cfc7de8922422cbce02543714367037536955221fa451b0c4fefaf20c66"
@@ -23,6 +23,7 @@ module.exports = class CSVExport extends Exporter {
23
23
  compExport.downloadCSV(fileName);
24
24
  }
25
25
  static exportCTDLASN(json, name) {
26
+ console.log('export CTDL');
26
27
  let objects = [];
27
28
  CSVExport.findGraphs(json, objects);
28
29
  CSVExport.exportObjects(objects, name + ".csv", true);
@@ -202,7 +203,16 @@ module.exports = class CSVExport extends Exporter {
202
203
  display += props2[prop2][prop3] + "|";
203
204
  }
204
205
  } else {
205
- display += props2[prop2] + "|";
206
+ if (prop === 'ceterms:versionIdentifier') {
207
+ if (props2[prop2]['ceterms:identifierValueCode'])
208
+ display += props2[prop2]['ceterms:identifierValueCode'] + "~";
209
+ if (props2[prop2]['ceterms:identifierTypeName'])
210
+ display += Object.values(props2[prop2]['ceterms:identifierTypeName'])[0] + "~";
211
+ if (props2[prop2]['ceterms:identifierType'])
212
+ display += props2[prop2]['ceterms:identifierType'] + "|";
213
+ } else {
214
+ display += props2[prop2] + "|";
215
+ }
206
216
  }
207
217
  }
208
218
  display = display.substring(0, display.length - 1);
@@ -109,7 +109,8 @@ module.exports = class CTDLASNCSVConceptImport {
109
109
  ceo,
110
110
  endpoint,
111
111
  eim,
112
- progressionsFlag
112
+ progressionsFlag,
113
+ validationRules
113
114
  ) {
114
115
  if (eim === undefined || eim == null)
115
116
  eim = EcIdentityManager.default;
@@ -123,7 +124,7 @@ module.exports = class CTDLASNCSVConceptImport {
123
124
  failure("Invalid file type");
124
125
  }
125
126
  if (progressionsFlag) {
126
- return this.importProgressions(repo, file, success, failure, ceo, endpoint, eim);
127
+ return this.importProgressions(repo, file, success, failure, ceo, endpoint, eim, validationRules);
127
128
  }
128
129
  Papa.parse(file, {
129
130
  header: true,
@@ -139,21 +140,50 @@ module.exports = class CTDLASNCSVConceptImport {
139
140
  } catch (e) {
140
141
  console.error('Error trimming data', e);
141
142
  }
143
+
144
+ // Validate hierarchy before processing (only if validation rules provided)
145
+ if (validationRules && validationRules.validateHierarchy !== false) {
146
+ const hierarchyError = CTDLASNCSVConceptImport.validateConceptHierarchy(
147
+ tabularData,
148
+ validationRules.hierarchyRules
149
+ );
150
+ if (hierarchyError) {
151
+ failure(hierarchyError);
152
+ return;
153
+ }
154
+ }
142
155
  const terms = JSON.parse(JSON.stringify((await EcRemote.getExpectingObject("https://schema.cassproject.org/0.4/jsonld1.1/ceasn2cassConceptsTerms"))));
143
156
  let schemeArray = [];
144
157
  let concepts = [];
145
158
  for (let each = 0; each < tabularData.length; each++) {
146
159
  let pretranslatedE = tabularData[each];
147
- // Probably an empty newline, skip
148
- if (!pretranslatedE["@id"]) {
160
+ // Skip extra lines if found in file
161
+ if (!pretranslatedE || !pretranslatedE["@type"]) {
149
162
  continue;
150
163
  }
164
+ if (!pretranslatedE["@id"]) {
165
+ failure(`Row ${each + 2}: is missing an @id`);
166
+ return;
167
+ }
151
168
  if (
152
169
  pretranslatedE["@type"].toLowerCase().startsWith('sample') || pretranslatedE["@type"].toLowerCase().startsWith('instruction')
153
170
  ) {
154
171
  continue;
155
172
  }
156
173
  if (pretranslatedE["@type"] == "skos:ConceptScheme") {
174
+ // Validate required properties (only if validation rules provided)
175
+ if (validationRules && validationRules.requiredProps) {
176
+ const validationError = CTDLASNCSVConceptImport.validateRequiredProperties(
177
+ pretranslatedE,
178
+ "skos:ConceptScheme",
179
+ each + 2,
180
+ validationRules.requiredProps
181
+ );
182
+ if (validationError) {
183
+ failure(validationError);
184
+ return;
185
+ }
186
+ }
157
187
  let translator = new EcLinkedData(null, null);
158
188
  translator.copyFrom(pretranslatedE);
159
189
  CTDLASNCSVImport.cleanUpTranslator(
@@ -223,6 +253,19 @@ module.exports = class CTDLASNCSVConceptImport {
223
253
  setVersionIdentifier(f);
224
254
  schemeArray.push(f);
225
255
  } else if (pretranslatedE["@type"] == "skos:Concept") {
256
+ // Validate required properties (only if validation rules provided)
257
+ if (validationRules && validationRules.requiredProps) {
258
+ const validationError = CTDLASNCSVConceptImport.validateRequiredProperties(
259
+ pretranslatedE,
260
+ "skos:Concept",
261
+ each + 2,
262
+ validationRules.requiredProps
263
+ );
264
+ if (validationError) {
265
+ failure(validationError);
266
+ return;
267
+ }
268
+ }
226
269
  let translator = new EcLinkedData(null, null);
227
270
  translator.copyFrom(pretranslatedE);
228
271
  CTDLASNCSVImport.cleanUpTranslator(
@@ -251,6 +294,7 @@ module.exports = class CTDLASNCSVConceptImport {
251
294
  let f = new EcConcept();
252
295
  f.copyFrom(e);
253
296
  if (e["id"] == null) {
297
+ failure(`Row ${each + 2}: Concept is missing an id`);
254
298
  return;
255
299
  }
256
300
  if (
@@ -364,10 +408,11 @@ module.exports = class CTDLASNCSVConceptImport {
364
408
  pretranslatedE["@type"] == null ||
365
409
  pretranslatedE["@type"] == ""
366
410
  ) {
411
+ failure(`Row ${each + 2}: Missing or empty @type`);
367
412
  return;
368
413
  } else {
369
- this.error(
370
- "Found unknown type:" + pretranslatedE["@type"]
414
+ failure(
415
+ `Row ${each + 2}: Found unknown type: ` + pretranslatedE["@type"]
371
416
  );
372
417
  return;
373
418
  }
@@ -384,7 +429,8 @@ module.exports = class CTDLASNCSVConceptImport {
384
429
  failure,
385
430
  ceo,
386
431
  endpoint,
387
- eim
432
+ eim,
433
+ validationRules
388
434
  ) {
389
435
  Papa.parse(file, {
390
436
  header: true,
@@ -400,17 +446,50 @@ module.exports = class CTDLASNCSVConceptImport {
400
446
  } catch (e) {
401
447
  console.error('Error trimming data', e);
402
448
  }
449
+
450
+ // Validate hierarchy before processing (only if validation rules provided)
451
+ if (validationRules && validationRules.validateHierarchy !== false) {
452
+ const hierarchyError = CTDLASNCSVConceptImport.validateConceptHierarchy(
453
+ tabularData,
454
+ validationRules.hierarchyRules
455
+ );
456
+ if (hierarchyError) {
457
+ failure(hierarchyError);
458
+ return;
459
+ }
460
+ }
403
461
  const terms = JSON.parse(JSON.stringify((await EcRemote.getExpectingObject("https://schema.cassproject.org/0.4/jsonld1.1/ceasn2cassConceptsTerms"))));
404
462
  let schemeArray = [];
405
463
  let concepts = [];
406
464
  for (let each = 0; each < tabularData.length; each++) {
407
465
  let pretranslatedE = tabularData[each];
466
+ // Skip extra lines if found in file
467
+ if (!pretranslatedE || !pretranslatedE["@type"]) {
468
+ continue;
469
+ }
470
+ if (!pretranslatedE["@id"]) {
471
+ failure(`Row ${each + 2}: is missing an @id`);
472
+ return;
473
+ }
408
474
  if (
409
475
  pretranslatedE["@type"].toLowerCase().startsWith('sample') || pretranslatedE["@type"].toLowerCase().startsWith('instruction')
410
476
  ) {
411
477
  continue;
412
478
  }
413
479
  if (pretranslatedE["@type"] == "asn:ProgressionModel") {
480
+ // Validate required properties (only if validation rules provided)
481
+ if (validationRules && validationRules.requiredProps) {
482
+ const validationError = CTDLASNCSVConceptImport.validateRequiredProperties(
483
+ pretranslatedE,
484
+ "asn:ProgressionModel",
485
+ each + 2,
486
+ validationRules.requiredProps
487
+ );
488
+ if (validationError) {
489
+ failure(validationError);
490
+ return;
491
+ }
492
+ }
414
493
  let translator = new EcLinkedData(null, null);
415
494
  translator.copyFrom(pretranslatedE);
416
495
  CTDLASNCSVImport.cleanUpTranslator(
@@ -476,6 +555,19 @@ module.exports = class CTDLASNCSVConceptImport {
476
555
  f.subType = "Progression";
477
556
  schemeArray.push(f);
478
557
  } else if (pretranslatedE["@type"] == "asn:ProgressionLevel") {
558
+ // Validate required properties (only if validation rules provided)
559
+ if (validationRules && validationRules.requiredProps) {
560
+ const validationError = CTDLASNCSVConceptImport.validateRequiredProperties(
561
+ pretranslatedE,
562
+ "asn:ProgressionLevel",
563
+ each + 2,
564
+ validationRules.requiredProps
565
+ );
566
+ if (validationError) {
567
+ failure(validationError);
568
+ return;
569
+ }
570
+ }
479
571
  let translator = new EcLinkedData(null, null);
480
572
  translator.copyFrom(pretranslatedE);
481
573
  CTDLASNCSVImport.cleanUpTranslator(
@@ -504,6 +596,7 @@ module.exports = class CTDLASNCSVConceptImport {
504
596
  let f = new EcConcept();
505
597
  f.copyFrom(e);
506
598
  if (e["id"] == null) {
599
+ failure(`Row ${each + 2}: Concept is missing an id`);
507
600
  return;
508
601
  }
509
602
  if (
@@ -583,10 +676,11 @@ module.exports = class CTDLASNCSVConceptImport {
583
676
  pretranslatedE["@type"] == null ||
584
677
  pretranslatedE["@type"] == ""
585
678
  ) {
679
+ failure(`Row ${each + 2}: Missing or empty @type`);
586
680
  return;
587
681
  } else {
588
- this.error(
589
- "Found unknown type:" + pretranslatedE["@type"]
682
+ failure(
683
+ `Row ${each + 2}: Found unknown type: ` + pretranslatedE["@type"]
590
684
  );
591
685
  return;
592
686
  }
@@ -596,4 +690,75 @@ module.exports = class CTDLASNCSVConceptImport {
596
690
  error: failure
597
691
  });
598
692
  }
693
+
694
+ static validateRequiredProperties(obj, type, rowIndex, requiredProps) {
695
+ // Use provided validation rules or fall back to defaults
696
+ if (!requiredProps) {
697
+ return 'Required properties not defined.'
698
+ }
699
+
700
+ const props = requiredProps[type];
701
+ if (!props) return null;
702
+
703
+ const missing = [];
704
+ for (const prop of props) {
705
+ if (!obj[prop] || (typeof obj[prop] === 'string' && obj[prop].trim() === '')) {
706
+ missing.push(prop);
707
+ }
708
+ }
709
+
710
+ if (missing.length > 0) {
711
+ return `Row ${rowIndex}: Missing required properties for ${type}: ${missing.join(', ')}`;
712
+ }
713
+ return null;
714
+ }
715
+
716
+ static validateConceptHierarchy(tabularData, hierarchyRules) {
717
+ // Use provided hierarchy rules or fall back to defaults
718
+ if (!hierarchyRules) {
719
+ return 'Hierarchy rules not defined.'
720
+ }
721
+
722
+ // Check each type defined in hierarchy rules
723
+ for (const [type, rules] of Object.entries(hierarchyRules)) {
724
+ const hasType = tabularData.some(r => r && r["@type"] === type);
725
+ if (!hasType) continue;
726
+
727
+ let hasRequiredProp = false;
728
+ let hasChildProp = false;
729
+
730
+ for (const row of tabularData) {
731
+ if (!row || !row["@type"]) continue;
732
+
733
+ // Check if this row has any of the required properties
734
+ if (row["@type"] === type && rules.requiredProperties) {
735
+ for (const prop of rules.requiredProperties) {
736
+ if (row[prop]) {
737
+ hasRequiredProp = true;
738
+ break;
739
+ }
740
+ }
741
+ }
742
+
743
+ // Check if any row has child properties
744
+ if (rules.childProperties) {
745
+ for (const prop of rules.childProperties) {
746
+ if (row[prop]) {
747
+ hasChildProp = true;
748
+ break;
749
+ }
750
+ }
751
+ }
752
+
753
+ if (hasRequiredProp && hasChildProp) break;
754
+ }
755
+
756
+ // Validate that at least one of the hierarchy properties exists
757
+ if (!hasRequiredProp && !hasChildProp) {
758
+ return rules.errorMessage || `CSV must contain hierarchy properties for ${type}`;
759
+ }
760
+ }
761
+
762
+ return null;
763
+ }
599
764
  };
@@ -133,7 +133,8 @@ module.exports = class CTDLASNCSVImport {
133
133
  endpoint,
134
134
  eim,
135
135
  collectionsFlag,
136
- skip
136
+ skip,
137
+ validationRules
137
138
  ) {
138
139
  if (eim === undefined || eim == null)
139
140
  eim = EcIdentityManager.default;
@@ -147,7 +148,7 @@ module.exports = class CTDLASNCSVImport {
147
148
  failure("Invalid file type");
148
149
  }
149
150
  if (collectionsFlag) {
150
- return this.importCollectionsAndCompetencies(repo, file, success, failure, ceo, endpoint, eim, skip);
151
+ return this.importCollectionsAndCompetencies(repo, file, success, failure, ceo, endpoint, eim, skip, validationRules);
151
152
  }
152
153
  Papa.parse(file, {
153
154
  header: true,
@@ -163,7 +164,19 @@ module.exports = class CTDLASNCSVImport {
163
164
  } catch (e) {
164
165
  console.error('Error trimming data', e);
165
166
  }
166
-
167
+
168
+ // Validate hierarchy before processing (only if validation rules provided)
169
+ if (validationRules && validationRules.validateHierarchy !== false) {
170
+ const hierarchyError = CTDLASNCSVImport.validateFrameworkHierarchy(
171
+ tabularData,
172
+ validationRules.hierarchyRules
173
+ );
174
+ if (hierarchyError) {
175
+ failure(hierarchyError);
176
+ return;
177
+ }
178
+ }
179
+
167
180
  const terms = JSON.parse(JSON.stringify((await EcRemote.getExpectingObject("https://schema.cassproject.org/0.4/jsonld1.1/ceasn2cassTerms"))));
168
181
  let frameworks = {};
169
182
  let frameworkArray = [];
@@ -206,6 +219,20 @@ module.exports = class CTDLASNCSVImport {
206
219
  pretranslatedE["@type"] ==
207
220
  "ceasn:CompetencyFramework"
208
221
  ) {
222
+ // Validate required properties (only if validation rules provided)
223
+ if (validationRules && validationRules.requiredProps) {
224
+ const validationError = CTDLASNCSVImport.validateRequiredProperties(
225
+ pretranslatedE,
226
+ "ceasn:CompetencyFramework",
227
+ i + 2,
228
+ validationRules.requiredProps
229
+ );
230
+ if (validationError) {
231
+ failure(validationError);
232
+ return;
233
+ }
234
+ }
235
+
209
236
  let translator = new EcLinkedData(null, null);
210
237
  translator.copyFrom(pretranslatedE);
211
238
  CTDLASNCSVImport.cleanUpTranslator(
@@ -318,9 +345,9 @@ module.exports = class CTDLASNCSVImport {
318
345
  // Remove skipped competencies
319
346
  if (skip && Array.isArray(skip) && skip.length > 0 && f.competency) {
320
347
  skip.forEach((element) => {
321
- const id = (element.ctid ? element.ctid : element).replace('ce-', '');
348
+ const id = (element.ctid ? element.ctid : element).replace('ce-', '');
322
349
  const index = f.competency.findIndex((comp) => comp.includes(id));
323
- if (index) {
350
+ if (index >= 0) {
324
351
  f.competency.splice(index, 1);
325
352
  }
326
353
  });
@@ -334,6 +361,20 @@ module.exports = class CTDLASNCSVImport {
334
361
  } else if (
335
362
  pretranslatedE["@type"] == "ceasn:Competency"
336
363
  ) {
364
+ // Validate required properties (only if validation rules provided)
365
+ if (validationRules && validationRules.requiredProps) {
366
+ const validationError = CTDLASNCSVImport.validateRequiredProperties(
367
+ pretranslatedE,
368
+ "ceasn:Competency",
369
+ i + 2,
370
+ validationRules.requiredProps
371
+ );
372
+ if (validationError) {
373
+ failure(validationError);
374
+ return;
375
+ }
376
+ }
377
+
337
378
  let translator = new EcLinkedData(null, null);
338
379
  translator.copyFrom(pretranslatedE);
339
380
  CTDLASNCSVImport.cleanUpTranslator(
@@ -369,6 +410,7 @@ module.exports = class CTDLASNCSVImport {
369
410
  let f = new EcCompetency();
370
411
  f.copyFrom(e);
371
412
  if (e["id"] == null) {
413
+ failure(`Row ${i+2}: Competency is missing an id`);
372
414
  return;
373
415
  }
374
416
  if (e["ceasn:isPartOf"] != null) {
@@ -632,6 +674,7 @@ module.exports = class CTDLASNCSVImport {
632
674
  pretranslatedE["@type"] == null ||
633
675
  pretranslatedE["@type"] == ""
634
676
  ) {
677
+ failure(`Row ${i+2}: Missing or empty @type`);
635
678
  return;
636
679
  } else {
637
680
  failure(
@@ -653,7 +696,8 @@ module.exports = class CTDLASNCSVImport {
653
696
  ceo,
654
697
  endpoint,
655
698
  eim,
656
- skip
699
+ skip,
700
+ validationRules
657
701
  ) {
658
702
  Papa.parse(file, {
659
703
  header: true,
@@ -670,6 +714,18 @@ module.exports = class CTDLASNCSVImport {
670
714
  console.error('Error trimming data', e);
671
715
  }
672
716
 
717
+ // Validate hierarchy before processing (only if validation rules provided)
718
+ if (validationRules && validationRules.validateHierarchy !== false) {
719
+ const hierarchyError = CTDLASNCSVImport.validateFrameworkHierarchy(
720
+ tabularData,
721
+ validationRules.hierarchyRules
722
+ );
723
+ if (hierarchyError) {
724
+ failure(hierarchyError);
725
+ return;
726
+ }
727
+ }
728
+
673
729
  const terms = JSON.parse(JSON.stringify((await EcRemote.getExpectingObject("https://schema.cassproject.org/0.4/jsonld1.1/ceasn2cassTerms"))));
674
730
  let frameworks = {};
675
731
  let frameworkArray = [];
@@ -705,6 +761,19 @@ module.exports = class CTDLASNCSVImport {
705
761
  pretranslatedE["@type"] ==
706
762
  "ceterms:Collection"
707
763
  ) {
764
+ // Validate required properties (only if validation rules provided)
765
+ if (validationRules && validationRules.requiredProps) {
766
+ const validationError = CTDLASNCSVImport.validateRequiredProperties(
767
+ pretranslatedE,
768
+ "ceterms:Collection",
769
+ i + 2,
770
+ validationRules.requiredProps
771
+ );
772
+ if (validationError) {
773
+ failure(validationError);
774
+ return;
775
+ }
776
+ }
708
777
  let translator = new EcLinkedData(null, null);
709
778
  translator.copyFrom(pretranslatedE);
710
779
  CTDLASNCSVImport.cleanUpTranslator(
@@ -762,9 +831,9 @@ module.exports = class CTDLASNCSVImport {
762
831
  // Remove skipped competencies
763
832
  if (skip && Array.isArray(skip) && skip.length > 0 && f.competency) {
764
833
  skip.forEach((element) => {
765
- const id = (element.ctid ? element.ctid : element).replace('ce-', '');
834
+ const id = (element.ctid ? element.ctid : element).replace('ce-', '');
766
835
  const index = f.competency.findIndex((comp) => comp.includes(id));
767
- if (index) {
836
+ if (index >= 0) {
768
837
  f.competency.splice(index, 1);
769
838
  }
770
839
  });
@@ -776,6 +845,19 @@ module.exports = class CTDLASNCSVImport {
776
845
  } else if (
777
846
  pretranslatedE["@type"] == "ceasn:Competency"
778
847
  ) {
848
+ // Validate required properties (only if validation rules provided)
849
+ if (validationRules && validationRules.requiredProps) {
850
+ const validationError = CTDLASNCSVImport.validateRequiredProperties(
851
+ pretranslatedE,
852
+ "ceterms:Collection:Competency",
853
+ i + 2,
854
+ validationRules.requiredProps
855
+ );
856
+ if (validationError) {
857
+ failure(validationError);
858
+ return;
859
+ }
860
+ }
779
861
  let translator = new EcLinkedData(null, null);
780
862
  translator.copyFrom(pretranslatedE);
781
863
  CTDLASNCSVImport.cleanUpTranslator(
@@ -799,6 +881,7 @@ module.exports = class CTDLASNCSVImport {
799
881
  let f = new EcCompetency();
800
882
  f.copyFrom(e);
801
883
  if (e["id"] == null) {
884
+ failure(`Row ${i+2}: Competency is missing an id`);
802
885
  return;
803
886
  }
804
887
  if (e["ceterms:isMemberOf"] != null) {
@@ -992,6 +1075,7 @@ module.exports = class CTDLASNCSVImport {
992
1075
  pretranslatedE["@type"] == null ||
993
1076
  pretranslatedE["@type"] == ""
994
1077
  ) {
1078
+ failure(`Row ${i+2}: Missing or empty @type`);
995
1079
  return;
996
1080
  } else {
997
1081
  failure(
@@ -1244,6 +1328,77 @@ module.exports = class CTDLASNCSVImport {
1244
1328
  frameworks[EcRemoteLinkedData.trimVersionFromUrl(e["ceasn:isPartOf"])].relation.push(r.shortId());
1245
1329
  }
1246
1330
  }
1331
+ static validateRequiredProperties(obj, type, rowIndex, requiredProps) {
1332
+ // Use provided validation rules or fall back to defaults
1333
+ if (!requiredProps) {
1334
+ return 'Required properties not defined.'
1335
+ }
1336
+
1337
+ const props = requiredProps[type];
1338
+ if (!props) return null;
1339
+
1340
+ const missing = [];
1341
+ for (const prop of props) {
1342
+ if (!obj[prop] || (typeof obj[prop] === 'string' && obj[prop].trim() === '')) {
1343
+ missing.push(prop);
1344
+ }
1345
+ }
1346
+
1347
+ if (missing.length > 0) {
1348
+ return `Row ${rowIndex}: Missing required properties for ${type}: ${missing.join(', ')}`;
1349
+ }
1350
+ return null;
1351
+ }
1352
+
1353
+ static validateFrameworkHierarchy(tabularData, hierarchyRules) {
1354
+ // Use provided hierarchy rules or fall back to defaults
1355
+ if (!hierarchyRules) {
1356
+ return 'Hierarchy rules not defined.'
1357
+ }
1358
+
1359
+ // Check each type defined in hierarchy rules
1360
+ for (const [type, rules] of Object.entries(hierarchyRules)) {
1361
+ const hasType = tabularData.some(r => r && r["@type"] === type);
1362
+ if (!hasType) continue;
1363
+
1364
+ let hasRequiredProp = false;
1365
+ let hasChildProp = false;
1366
+
1367
+ for (const row of tabularData) {
1368
+ if (!row || !row["@type"]) continue;
1369
+
1370
+ // Check if this row has any of the required properties
1371
+ if (row["@type"] === type && rules.requiredProperties) {
1372
+ for (const prop of rules.requiredProperties) {
1373
+ if (row[prop]) {
1374
+ hasRequiredProp = true;
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ // Check if any row has child properties
1381
+ if (rules.childProperties) {
1382
+ for (const prop of rules.childProperties) {
1383
+ if (row[prop]) {
1384
+ hasChildProp = true;
1385
+ break;
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ if (hasRequiredProp && hasChildProp) break;
1391
+ }
1392
+
1393
+ // Validate that at least one of the hierarchy properties exists
1394
+ if (!hasRequiredProp && !hasChildProp) {
1395
+ return rules.errorMessage || `CSV must contain hierarchy properties for ${type}`;
1396
+ }
1397
+ }
1398
+
1399
+ return null;
1400
+ }
1401
+
1247
1402
  static setDateCreated(importObject, object) {
1248
1403
  if (
1249
1404
  importObject["ceasn:dateCreated"] == null &&