cassproject 5.0.17 → 5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassproject",
3
- "version": "5.0.17",
3
+ "version": "5.0.18",
4
4
  "description": "Competency and Skills Service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -191,13 +191,13 @@
191
191
  "cypress-fail-fast": "^8.1.0",
192
192
  "eslint": "^10.8.0",
193
193
  "fake-indexeddb": "^6.2.5",
194
- "mocha": "^11.7.6",
194
+ "mocha": "^11.8.0",
195
195
  "node-polyfill-webpack-plugin": "^4.1.0",
196
196
  "nodemon": "^3.1.14",
197
197
  "nyc": "^18.0.0",
198
198
  "sinon": "^21.1.2",
199
199
  "wait-on": "^9.1.0",
200
- "webpack": "^5.109.1"
200
+ "webpack": "^5.109.2"
201
201
  },
202
202
  "packageManager": "npm@11.3.0+sha512.96eb611483f49c55f7fa74df61b588de9e213f80a256728e6798ddc67176c7b07e4a1cfc7de8922422cbce02543714367037536955221fa451b0c4fefaf20c66"
203
203
  }
@@ -207,6 +207,20 @@ module.exports = class CTDLASNCSVConceptImport {
207
207
  continue;
208
208
  }
209
209
  }
210
+ // Validate registry URL fields (only if url rules provided)
211
+ if (validationRules && validationRules.urlRules) {
212
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
213
+ pretranslatedE,
214
+ "skos:ConceptScheme",
215
+ each + 2,
216
+ validationRules.urlRules
217
+ );
218
+ if (urlErrors) {
219
+ errors.push(...urlErrors);
220
+ rowsWithErrors.add(each);
221
+ continue;
222
+ }
223
+ }
210
224
  let translator = new EcLinkedData(null, null);
211
225
  translator.copyFrom(pretranslatedE);
212
226
  CTDLASNCSVImport.cleanUpTranslator(
@@ -294,6 +308,20 @@ module.exports = class CTDLASNCSVConceptImport {
294
308
  continue;
295
309
  }
296
310
  }
311
+ // Validate registry URL fields (only if url rules provided)
312
+ if (validationRules && validationRules.urlRules) {
313
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
314
+ pretranslatedE,
315
+ "skos:Concept",
316
+ each + 2,
317
+ validationRules.urlRules
318
+ );
319
+ if (urlErrors) {
320
+ errors.push(...urlErrors);
321
+ rowsWithErrors.add(each);
322
+ continue;
323
+ }
324
+ }
297
325
  let translator = new EcLinkedData(null, null);
298
326
  translator.copyFrom(pretranslatedE);
299
327
  CTDLASNCSVImport.cleanUpTranslator(
@@ -583,6 +611,20 @@ module.exports = class CTDLASNCSVConceptImport {
583
611
  continue;
584
612
  }
585
613
  }
614
+ // Validate registry URL fields (only if url rules provided)
615
+ if (validationRules && validationRules.urlRules) {
616
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
617
+ pretranslatedE,
618
+ "asn:ProgressionModel",
619
+ each + 2,
620
+ validationRules.urlRules
621
+ );
622
+ if (urlErrors) {
623
+ errors.push(...urlErrors);
624
+ rowsWithErrors.add(each);
625
+ continue;
626
+ }
627
+ }
586
628
  let translator = new EcLinkedData(null, null);
587
629
  translator.copyFrom(pretranslatedE);
588
630
  CTDLASNCSVImport.cleanUpTranslator(
@@ -666,6 +708,20 @@ module.exports = class CTDLASNCSVConceptImport {
666
708
  continue;
667
709
  }
668
710
  }
711
+ // Validate registry URL fields (only if url rules provided)
712
+ if (validationRules && validationRules.urlRules) {
713
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
714
+ pretranslatedE,
715
+ "asn:ProgressionLevel",
716
+ each + 2,
717
+ validationRules.urlRules
718
+ );
719
+ if (urlErrors) {
720
+ errors.push(...urlErrors);
721
+ rowsWithErrors.add(each);
722
+ continue;
723
+ }
724
+ }
669
725
  let translator = new EcLinkedData(null, null);
670
726
  translator.copyFrom(pretranslatedE);
671
727
  CTDLASNCSVImport.cleanUpTranslator(
@@ -255,6 +255,20 @@ module.exports = class CTDLASNCSVImport {
255
255
  continue;
256
256
  }
257
257
  }
258
+ // Validate registry URL fields (only if url rules provided)
259
+ if (validationRules && validationRules.urlRules) {
260
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
261
+ pretranslatedE,
262
+ "ceasn:CompetencyFramework",
263
+ i + 2,
264
+ validationRules.urlRules
265
+ );
266
+ if (urlErrors) {
267
+ errors.push(...urlErrors);
268
+ rowsWithErrors.add(i);
269
+ continue;
270
+ }
271
+ }
258
272
 
259
273
  let translator = new EcLinkedData(null, null);
260
274
  translator.copyFrom(pretranslatedE);
@@ -404,6 +418,20 @@ module.exports = class CTDLASNCSVImport {
404
418
  continue;
405
419
  }
406
420
  }
421
+ // Validate registry URL fields (only if url rules provided)
422
+ if (validationRules && validationRules.urlRules) {
423
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
424
+ pretranslatedE,
425
+ "ceasn:Competency",
426
+ i + 2,
427
+ validationRules.urlRules
428
+ );
429
+ if (urlErrors) {
430
+ errors.push(...urlErrors);
431
+ rowsWithErrors.add(i);
432
+ continue;
433
+ }
434
+ }
407
435
 
408
436
  let translator = new EcLinkedData(null, null);
409
437
  translator.copyFrom(pretranslatedE);
@@ -850,6 +878,20 @@ module.exports = class CTDLASNCSVImport {
850
878
  continue;
851
879
  }
852
880
  }
881
+ // Validate registry URL fields (only if url rules provided)
882
+ if (validationRules && validationRules.urlRules) {
883
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
884
+ pretranslatedE,
885
+ "ceterms:Collection",
886
+ i + 2,
887
+ validationRules.urlRules
888
+ );
889
+ if (urlErrors) {
890
+ errors.push(...urlErrors);
891
+ rowsWithErrors.add(i);
892
+ continue;
893
+ }
894
+ }
853
895
  let translator = new EcLinkedData(null, null);
854
896
  translator.copyFrom(pretranslatedE);
855
897
  CTDLASNCSVImport.cleanUpTranslator(
@@ -939,6 +981,20 @@ module.exports = class CTDLASNCSVImport {
939
981
  continue;
940
982
  }
941
983
  }
984
+ // Validate registry URL fields (only if url rules provided)
985
+ if (validationRules && validationRules.urlRules) {
986
+ const urlErrors = CTDLASNCSVImport.validateRegistryUrls(
987
+ pretranslatedE,
988
+ "ceasn:Competency",
989
+ i + 2,
990
+ validationRules.urlRules
991
+ );
992
+ if (urlErrors) {
993
+ errors.push(...urlErrors);
994
+ rowsWithErrors.add(i);
995
+ continue;
996
+ }
997
+ }
942
998
  let translator = new EcLinkedData(null, null);
943
999
  translator.copyFrom(pretranslatedE);
944
1000
  CTDLASNCSVImport.cleanUpTranslator(
@@ -1447,6 +1503,48 @@ module.exports = class CTDLASNCSVImport {
1447
1503
  return null;
1448
1504
  }
1449
1505
 
1506
+ /*
1507
+ * Validates that reference fields contain either a CTID (ce-...) or a
1508
+ * credential registry URL whose origin is allowed for this environment.
1509
+ * CTIDs are resolved by the CTID pipeline (getIdFromCtid); any other value
1510
+ * must be an http(s) URL with an origin listed in urlRules.allowedOrigins.
1511
+ *
1512
+ * urlRules = {
1513
+ * fields: { "<@type>": ["ceasn:creator", ...], ... },
1514
+ * allowedOrigins: ["https://sandbox.credentialengineregistry.org", ...]
1515
+ * }
1516
+ *
1517
+ * Returns an array of error strings, or null when everything passes.
1518
+ */
1519
+ static validateRegistryUrls(obj, type, rowIndex, urlRules) {
1520
+ if (!urlRules) return null;
1521
+ const fields = urlRules.fields ? urlRules.fields[type] : null;
1522
+ const allowedOrigins = urlRules.allowedOrigins;
1523
+ if (!fields || !allowedOrigins || allowedOrigins.length === 0) return null;
1524
+
1525
+ const ctidPattern = /^ce-[0-9a-fA-F-]+$/;
1526
+ const errors = [];
1527
+ for (const field of fields) {
1528
+ const raw = obj[field];
1529
+ if (raw == null) continue;
1530
+ const values = Array.isArray(raw) ? raw : String(raw).split("|");
1531
+ for (let value of values) {
1532
+ value = String(value).trim();
1533
+ if (value === "" || ctidPattern.test(value)) continue;
1534
+ let origin = null;
1535
+ try {
1536
+ origin = new URL(value).origin;
1537
+ } catch (e) {
1538
+ // Not parseable as a URL; falls through to the error below.
1539
+ }
1540
+ if (origin == null || !allowedOrigins.includes(origin)) {
1541
+ errors.push(`Row ${rowIndex}: ${field} must be a CTID or a credential registry URL for this environment (got: ${value})`);
1542
+ }
1543
+ }
1544
+ }
1545
+ return errors.length > 0 ? errors : null;
1546
+ }
1547
+
1450
1548
  static validateFrameworkHierarchy(tabularData, hierarchyRules) {
1451
1549
  // Use provided hierarchy rules or fall back to defaults
1452
1550
  if (!hierarchyRules) {
@@ -0,0 +1,100 @@
1
+ let chai = require("chai");
2
+ const CTDLASNCSVImport = require("../org/cass/importer/CTDLASNCSVImport.js");
3
+
4
+ let expect = chai.expect;
5
+
6
+ // Unit tests for the registry URL validation added for cassproject/cass-editor#1423:
7
+ // CSV reference fields must contain either a CTID (ce-...) or a credential
8
+ // registry URL whose origin matches the configured environment.
9
+ describe("CTDLASNCSVImport.validateRegistryUrls", () => {
10
+ const urlRules = {
11
+ fields: {
12
+ "ceasn:CompetencyFramework": ["ceasn:creator", "ceasn:publisher", "asn:hasProgressionModel", "ceasn:hasTopChild"],
13
+ "ceasn:Competency": ["ceasn:creator", "ceterms:hasTask"]
14
+ },
15
+ allowedOrigins: ["https://sandbox.credentialengineregistry.org"]
16
+ };
17
+ const type = "ceasn:CompetencyFramework";
18
+
19
+ it("accepts a CTID value", () => {
20
+ const obj = { "ceasn:publisher": "ce-11111111-2222-3333-4444-555555555555" };
21
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 2, urlRules)).to.equal(null);
22
+ });
23
+
24
+ it("accepts a registry URL with an allowed origin", () => {
25
+ const obj = { "ceasn:publisher": "https://sandbox.credentialengineregistry.org/resources/ce-1111" };
26
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 2, urlRules)).to.equal(null);
27
+ });
28
+
29
+ it("rejects a URL from a different origin", () => {
30
+ const obj = { "ceasn:publisher": "https://credentialengineregistry.org/resources/ce-1111" };
31
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 2, urlRules);
32
+ expect(errors).to.be.an("array").with.lengthOf(1);
33
+ expect(errors[0]).to.contain("Row 2");
34
+ expect(errors[0]).to.contain("ceasn:publisher");
35
+ expect(errors[0]).to.contain("https://credentialengineregistry.org/resources/ce-1111");
36
+ });
37
+
38
+ it("rejects an arbitrary non-registry URL", () => {
39
+ const obj = { "ceasn:creator": "https://example.com/x" };
40
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 3, urlRules);
41
+ expect(errors).to.be.an("array").with.lengthOf(1);
42
+ expect(errors[0]).to.contain("ceasn:creator");
43
+ });
44
+
45
+ it("rejects a value that is neither CTID nor URL", () => {
46
+ const obj = { "ceasn:publisher": "Some Publisher Name" };
47
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 4, urlRules);
48
+ expect(errors).to.be.an("array").with.lengthOf(1);
49
+ expect(errors[0]).to.contain("Some Publisher Name");
50
+ });
51
+
52
+ it("reports only the bad part of a pipe-delimited value", () => {
53
+ const obj = { "ceasn:hasTopChild": "ce-11111111-2222-3333-4444-555555555555|https://evil.example.com/resources/ce-2222" };
54
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 5, urlRules);
55
+ expect(errors).to.be.an("array").with.lengthOf(1);
56
+ expect(errors[0]).to.contain("https://evil.example.com/resources/ce-2222");
57
+ });
58
+
59
+ it("validates array values", () => {
60
+ const obj = { "ceasn:creator": ["https://sandbox.credentialengineregistry.org/resources/ce-1", "https://example.org/nope"] };
61
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 6, urlRules);
62
+ expect(errors).to.be.an("array").with.lengthOf(1);
63
+ expect(errors[0]).to.contain("https://example.org/nope");
64
+ });
65
+
66
+ it("collects errors across multiple fields", () => {
67
+ const obj = {
68
+ "ceasn:creator": "https://example.com/a",
69
+ "ceasn:publisher": "https://example.com/b"
70
+ };
71
+ const errors = CTDLASNCSVImport.validateRegistryUrls(obj, type, 7, urlRules);
72
+ expect(errors).to.be.an("array").with.lengthOf(2);
73
+ });
74
+
75
+ it("ignores fields not configured for the type", () => {
76
+ const obj = { "ceasn:description": "https://example.com/not-checked" };
77
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 8, urlRules)).to.equal(null);
78
+ });
79
+
80
+ it("is inert for types with no configured fields", () => {
81
+ const obj = { "ceasn:publisher": "https://example.com/x" };
82
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, "skos:Concept", 9, urlRules)).to.equal(null);
83
+ });
84
+
85
+ it("is inert when urlRules is absent", () => {
86
+ const obj = { "ceasn:publisher": "https://example.com/x" };
87
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 10, null)).to.equal(null);
88
+ });
89
+
90
+ it("is inert when allowedOrigins is empty", () => {
91
+ const obj = { "ceasn:publisher": "https://example.com/x" };
92
+ const rules = { fields: urlRules.fields, allowedOrigins: [] };
93
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 11, rules)).to.equal(null);
94
+ });
95
+
96
+ it("skips empty and null values", () => {
97
+ const obj = { "ceasn:publisher": "", "ceasn:creator": null };
98
+ expect(CTDLASNCSVImport.validateRegistryUrls(obj, type, 12, urlRules)).to.equal(null);
99
+ });
100
+ });