cassproject 5.0.16 → 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 +23 -8
- package/src/org/cass/importer/CTDLASNCSVConceptImport.js +56 -0
- package/src/org/cass/importer/CTDLASNCSVImport.js +98 -0
- package/src/org/cassproject/ebac/repository/EcRepository.js +17 -0
- package/src/test/1.CTDLASNCSVImport.validateRegistryUrls.test.js +100 -0
- package/src/test/1.EcRepository.cache.test.js +179 -0
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cassproject",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.18",
|
|
4
4
|
"description": "Competency and Skills Service",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "concurrently --maxProcesses=1 --kill-others-on-fail \"npm run test16HttpsFips\" \"npm run test16Https11Fips\" \"npm run test16Https\" \"npm run test16Https11\" \"npm run test16Fips\" \"npm run test16\" \"npm run test15HttpsFips\" \"npm run test15Https11Fips\" \"npm run test15Https\" \"npm run test15Https11\" \"npm run test15Fips\" \"npm run test15\"",
|
|
7
|
+
"test": "concurrently --maxProcesses=1 --kill-others-on-fail \"npm run test17HttpsFips\" \"npm run test17Https11Fips\" \"npm run test17Https\" \"npm run test17Https11\" \"npm run test17Fips\" \"npm run test17\" \"npm run test16HttpsFips\" \"npm run test16Https11Fips\" \"npm run test16Https\" \"npm run test16Https11\" \"npm run test16Fips\" \"npm run test16\" \"npm run test15HttpsFips\" \"npm run test15Https11Fips\" \"npm run test15Https\" \"npm run test15Https11\" \"npm run test15Fips\" \"npm run test15\"",
|
|
8
8
|
"testActionCypress": "npm run testkill && export CASS_LOOPBACK=http://cass-test15/api/|| set CASS_LOOPBACK=http://cass-test15/api/&& npm run testkill15 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test15 -p80:80 cassproject/cass:1.5.75 && wait-on http://localhost/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\"",
|
|
9
9
|
"testCustom": "npm run test14 && npm run test13 && npm run test12",
|
|
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 --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 testCypressEdge && npm run testCypress && npm run testkill",
|
|
12
12
|
"testDevHttps": "npm run testkill && docker run -d --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 testCypressEdgeHttps && npm run testCypressHttps",
|
|
13
|
+
"test17HttpsFips": "export CASS_LOOPBACK=https://cass-testsf17/api/|| set CASS_LOOPBACK=https://cass-testsf17/api/&& npm run testkillsf17 && docker run -d --network cass-net --name cass-testsf17 -p454:443 -e CASS_LOOPBACK -e HTTPS=true --pull always cassproject/cass:1.7 && wait-on https://localhost:454/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 testkillsf17",
|
|
14
|
+
"test17Https11Fips": "export CASS_LOOPBACK=https://cass-testsf1117/api/|| set CASS_LOOPBACK=https://cass-testsf1117/api/&& npm run testkillsf1117 && docker run -d --network cass-net --name cass-testsf1117 -p453:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.7 && wait-on https://localhost:453/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 testkillsf1117",
|
|
15
|
+
"test17Https": "export CASS_LOOPBACK=https://cass-tests17/api/|| set CASS_LOOPBACK=https://cass-tests17/api/&& npm run testkills17 && docker run -d --network cass-net --name cass-tests17 -p452:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.7 && wait-on https://localhost:452/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 testkills17",
|
|
16
|
+
"test17HttpsNoFips": "export CASS_LOOPBACK=https://cass-tests17/api/|| set CASS_LOOPBACK=https://cass-tests17/api/&& npm run testkills17 && docker run -d --network cass-net --name cass-tests17 -p452:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.7 && wait-on https://localhost:452/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode22Https\" \"npm run testNode20Https\" \"npm run testNode18Https\" && npm run testkills17",
|
|
17
|
+
"test17Https11": "export CASS_LOOPBACK=https://cass-tests1117/api/|| set CASS_LOOPBACK=https://cass-tests1117/api/&& npm run testkills1117 && docker run -d --network cass-net --name cass-tests1117 -p455:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.7 && wait-on https://localhost:455/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 testkills1117",
|
|
18
|
+
"test17Https11NoFips": "export CASS_LOOPBACK=https://cass-tests1117/api/|| set CASS_LOOPBACK=https://cass-tests1117/api/&& npm run testkills1117 && docker run -d --network cass-net --name cass-tests1117 -p455:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.7 && wait-on https://localhost:455/api/ping && concurrently --kill-others-on-fail \"npm run testCypressHttps\" \"npm run testNode24Https\" \"npm run testNode22Https\" \"npm run testNode20Https\" \"npm run testNode18Https\" && npm run testkills1117",
|
|
19
|
+
"test17Fips": "export CASS_LOOPBACK=http://cass-testf17/api/|| set CASS_LOOPBACK=http://cass-testf17/api/&& npm run testkillf17 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-testf17 -p85:80 cassproject/cass:1.7 && wait-on http://localhost:85/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 testkillf17",
|
|
20
|
+
"test17": "export CASS_LOOPBACK=http://cass-test17/api/|| set CASS_LOOPBACK=http://cass-test17/api/&& npm run testkill17 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test17 -p84:80 cassproject/cass:1.7 && wait-on http://localhost:84/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 testkill17",
|
|
21
|
+
"test17NoFips": "export CASS_LOOPBACK=http://cass-test17/api/|| set CASS_LOOPBACK=http://cass-test17/api/&& npm run testkill17 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test17 -p84:80 cassproject/cass:1.7 && wait-on http://localhost:84/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode24\" \"npm run testNode22\" \"npm run testNode20\" \"npm run testNode18\" && npm run testkill17",
|
|
13
22
|
"test16HttpsFips": "export CASS_LOOPBACK=https://cass-testsf16/api/|| set CASS_LOOPBACK=https://cass-testsf16/api/&& npm run testkillsf16 && docker run -d --network cass-net --name cass-testsf16 -p450:443 -e CASS_LOOPBACK -e HTTPS=true --pull always cassproject/cass:1.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
23
|
"test16Https11Fips": "export CASS_LOOPBACK=https://cass-testsf1116/api/|| set CASS_LOOPBACK=https://cass-testsf1116/api/&& npm run testkillsf1116 && docker run -d --network cass-net --name cass-testsf1116 -p449:443 -e CASS_LOOPBACK -e HTTPS=true -e HTTP2=false cassproject/cass:1.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
24
|
"test16Https": "export CASS_LOOPBACK=https://cass-tests16/api/|| set CASS_LOOPBACK=https://cass-tests16/api/&& npm run testkills16 && docker run -d --network cass-net --name cass-tests16 -p448:443 -e CASS_LOOPBACK -e HTTPS=true cassproject/cass:1.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",
|
|
@@ -31,6 +40,12 @@
|
|
|
31
40
|
"test14": "npm run testkill14 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test14 -p80:80 cassproject/cass:1.4.4 && wait-on http://localhost/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode16\" && npm run testkill14",
|
|
32
41
|
"test13": "npm run testkill13 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test13 -p80:80 cassproject/cass:1.3.18 && wait-on http://localhost/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode16\" && npm run testkill13",
|
|
33
42
|
"test12": "npm run testkill12 && docker run -d --network cass-net -e CASS_LOOPBACK --name cass-test12 -p80:80 cassproject/cass:1.2.44 && wait-on http://localhost/api/ping && concurrently --kill-others-on-fail \"npm run testCypress\" \"npm run testNode16\" && npm run testkill12",
|
|
43
|
+
"testkillsf17": "docker kill cass-testsf17 | exit 0 && docker rm cass-testsf17 | exit 0 && docker network create cass-net | exit 0",
|
|
44
|
+
"testkillsf1117": "(docker kill cass-testsf1117 | exit 0) && (docker rm cass-testsf1117 | exit 0) && docker network create cass-net | exit 0",
|
|
45
|
+
"testkills17": "docker kill cass-tests17 | exit 0 && docker rm cass-tests17 | exit 0 && docker network create cass-net | exit 0",
|
|
46
|
+
"testkills1117": "docker kill cass-tests1117 | exit 0 && docker rm cass-tests1117 | exit 0 && docker network create cass-net | exit 0",
|
|
47
|
+
"testkillf17": "docker kill cass-testf17 | exit 0 && docker rm cass-testf17 | exit 0 && docker network create cass-net | exit 0",
|
|
48
|
+
"testkill17": "docker kill cass-test17 | exit 0 && docker rm cass-test17 | exit 0 && docker network create cass-net | exit 0",
|
|
34
49
|
"testkillsf16": "docker kill cass-testsf16 | exit 0 && docker rm cass-testsf16 | exit 0 && docker network create cass-net | exit 0",
|
|
35
50
|
"testkillsf1116": "(docker kill cass-testsf1116 | exit 0) && (docker rm cass-testsf1116 | exit 0) && docker network create cass-net | exit 0",
|
|
36
51
|
"testkills16": "docker kill cass-tests16 | exit 0 && docker rm cass-tests16 | exit 0 && docker network create cass-net | exit 0",
|
|
@@ -171,18 +186,18 @@
|
|
|
171
186
|
"devDependencies": {
|
|
172
187
|
"@cypress/webpack-preprocessor": "^7.1.1",
|
|
173
188
|
"chai": "^4.5.0",
|
|
174
|
-
"concurrently": "^10.0.
|
|
175
|
-
"cypress": "
|
|
189
|
+
"concurrently": "^10.0.4",
|
|
190
|
+
"cypress": "15.19.0",
|
|
176
191
|
"cypress-fail-fast": "^8.1.0",
|
|
177
|
-
"eslint": "^10.
|
|
192
|
+
"eslint": "^10.8.0",
|
|
178
193
|
"fake-indexeddb": "^6.2.5",
|
|
179
|
-
"mocha": "^11.
|
|
194
|
+
"mocha": "^11.8.0",
|
|
180
195
|
"node-polyfill-webpack-plugin": "^4.1.0",
|
|
181
196
|
"nodemon": "^3.1.14",
|
|
182
197
|
"nyc": "^18.0.0",
|
|
183
198
|
"sinon": "^21.1.2",
|
|
184
|
-
"wait-on": "^9.0
|
|
185
|
-
"webpack": "^5.
|
|
199
|
+
"wait-on": "^9.1.0",
|
|
200
|
+
"webpack": "^5.109.2"
|
|
186
201
|
},
|
|
187
202
|
"packageManager": "npm@11.3.0+sha512.96eb611483f49c55f7fa74df61b588de9e213f80a256728e6798ddc67176c7b07e4a1cfc7de8922422cbce02543714367037536955221fa451b0c4fefaf20c66"
|
|
188
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) {
|
|
@@ -134,6 +134,23 @@ module.exports = class EcRepository {
|
|
|
134
134
|
return Reflect.deleteProperty(...arguments);
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
+
/**
|
|
138
|
+
* Empties the in-memory cache and, if L2 caching is enabled, the
|
|
139
|
+
* IndexedDB backing store. Do not reassign cacheBacking to clear the
|
|
140
|
+
* cache -- the cache proxy permanently wraps the original object.
|
|
141
|
+
*
|
|
142
|
+
* @memberOf EcRepository
|
|
143
|
+
* @method clearCache
|
|
144
|
+
* @static
|
|
145
|
+
*/
|
|
146
|
+
static clearCache = () => {
|
|
147
|
+
for (let prop of Object.keys(EcRepository.cacheBacking))
|
|
148
|
+
delete EcRepository.cacheBacking[prop];
|
|
149
|
+
if (EcRepository.cachingL2 == true && EcRepository.cacheDB != null) {
|
|
150
|
+
const transaction = EcRepository.cacheDB.transaction(EcRepository.LONGIDS, "readwrite");
|
|
151
|
+
transaction.objectStore(EcRepository.LONGIDS).clear();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
137
154
|
static fetching = {};
|
|
138
155
|
static repos = [];
|
|
139
156
|
static defaultPlugins = [];
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require('fake-indexeddb/auto');
|
|
2
|
+
|
|
3
|
+
const schema = {
|
|
4
|
+
"Thing": require('../org/schema/Thing.js')
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
let chai = require("chai");
|
|
8
|
+
const EcRepository = require('../org/cassproject/ebac/repository/EcRepository.js');
|
|
9
|
+
const EcCrypto = require('../com/eduworks/ec/crypto/EcCrypto.js');
|
|
10
|
+
const EcRemoteLinkedData = require('../org/cassproject/schema/general/EcRemoteLinkedData.js');
|
|
11
|
+
|
|
12
|
+
let assert = chai.assert;
|
|
13
|
+
|
|
14
|
+
const TEST_DB = "EcRepositoryCacheTest";
|
|
15
|
+
|
|
16
|
+
// Ids that contain /api/data/ and end in /<digits> are "versioned" and eligible
|
|
17
|
+
// for L2 caching; anything else is L1-only.
|
|
18
|
+
let versionedId = () => "http://localhost/api/data/schema.org.Thing/" + EcCrypto.generateUUID() + "/1690000000000";
|
|
19
|
+
let unversionedId = () => "http://localhost/api/data/schema.org.Thing/" + EcCrypto.generateUUID();
|
|
20
|
+
|
|
21
|
+
let makeThing = (id) => {
|
|
22
|
+
let thing = new schema.Thing();
|
|
23
|
+
thing.id = id;
|
|
24
|
+
thing.setName("Some Thing " + EcCrypto.generateUUID());
|
|
25
|
+
return thing;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let openTestDb = () => new Promise((resolve, reject) => {
|
|
29
|
+
const request = indexedDB.open(TEST_DB, 3);
|
|
30
|
+
request.onerror = reject;
|
|
31
|
+
request.onupgradeneeded = (event) => {
|
|
32
|
+
event.target.result.createObjectStore(EcRepository.LONGIDS, { keyPath: "id" });
|
|
33
|
+
};
|
|
34
|
+
request.onsuccess = (event) => resolve(event.target.result);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Reads straight from IndexedDB, bypassing L1, to observe what the proxy wrote.
|
|
38
|
+
let l2Get = (id) => new Promise((resolve, reject) => {
|
|
39
|
+
const transaction = EcRepository.cacheDB.transaction(EcRepository.LONGIDS, "readonly");
|
|
40
|
+
const request = transaction.objectStore(EcRepository.LONGIDS).get(id);
|
|
41
|
+
request.onerror = () => reject(request.error);
|
|
42
|
+
request.onsuccess = () => resolve(request.result == null ? null : request.result);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let l2Count = () => new Promise((resolve, reject) => {
|
|
46
|
+
const transaction = EcRepository.cacheDB.transaction(EcRepository.LONGIDS, "readonly");
|
|
47
|
+
const request = transaction.objectStore(EcRepository.LONGIDS).count();
|
|
48
|
+
request.onerror = () => reject(request.error);
|
|
49
|
+
request.onsuccess = () => resolve(request.result);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("EcRepository (Cache Management)", () => {
|
|
53
|
+
before(async () => {
|
|
54
|
+
EcRepository.caching = true;
|
|
55
|
+
EcRepository.cachingL2 = false;
|
|
56
|
+
EcRepository.clearCache();
|
|
57
|
+
EcRepository.cacheDB = await openTestDb();
|
|
58
|
+
});
|
|
59
|
+
after(() => {
|
|
60
|
+
EcRepository.cachingL2 = true;
|
|
61
|
+
EcRepository.clearCache();
|
|
62
|
+
EcRepository.caching = false;
|
|
63
|
+
EcRepository.cachingL2 = false;
|
|
64
|
+
EcRepository.cacheDB.close();
|
|
65
|
+
EcRepository.cacheDB = null;
|
|
66
|
+
indexedDB.deleteDatabase(TEST_DB);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("L1", () => {
|
|
70
|
+
before(() => {
|
|
71
|
+
EcRepository.cachingL2 = false;
|
|
72
|
+
});
|
|
73
|
+
it('cache proxy reads and writes the cacheBacking object', async () => {
|
|
74
|
+
let id = unversionedId();
|
|
75
|
+
let thing = makeThing(id);
|
|
76
|
+
EcRepository.cache[id] = thing;
|
|
77
|
+
assert.strictEqual(EcRepository.cacheBacking[id], thing, "Write through proxy did not land in cacheBacking.");
|
|
78
|
+
EcRepository.cacheBacking[id + "/direct"] = thing;
|
|
79
|
+
assert.strictEqual(EcRepository.cache[id + "/direct"], thing, "Write to cacheBacking not visible through proxy.");
|
|
80
|
+
assert.strictEqual(await EcRepository.cacheGet(id), thing);
|
|
81
|
+
delete EcRepository.cache[id + "/direct"];
|
|
82
|
+
});
|
|
83
|
+
it('delete through the proxy removes the entry', async () => {
|
|
84
|
+
let id = unversionedId();
|
|
85
|
+
EcRepository.cache[id] = makeThing(id);
|
|
86
|
+
delete EcRepository.cache[id];
|
|
87
|
+
assert.isUndefined(EcRepository.cacheBacking[id]);
|
|
88
|
+
assert.isUndefined(await EcRepository.cacheGet(id));
|
|
89
|
+
});
|
|
90
|
+
it('clearCache empties L1', async () => {
|
|
91
|
+
let ids = [unversionedId(), unversionedId(), versionedId()];
|
|
92
|
+
for (let id of ids)
|
|
93
|
+
EcRepository.cache[id] = makeThing(id);
|
|
94
|
+
EcRepository.clearCache();
|
|
95
|
+
assert.equal(Object.keys(EcRepository.cacheBacking).length, 0);
|
|
96
|
+
for (let id of ids)
|
|
97
|
+
assert.isUndefined(await EcRepository.cacheGet(id));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("L2", () => {
|
|
102
|
+
before(() => {
|
|
103
|
+
EcRepository.cachingL2 = true;
|
|
104
|
+
});
|
|
105
|
+
it('versioned writes go to both L1 and IndexedDB', async () => {
|
|
106
|
+
let id = versionedId();
|
|
107
|
+
EcRepository.cache[id] = makeThing(id);
|
|
108
|
+
assert.isDefined(EcRepository.cacheBacking[id], "Entry missing from L1.");
|
|
109
|
+
let stored = await l2Get(id);
|
|
110
|
+
assert.isNotNull(stored, "Entry missing from IndexedDB.");
|
|
111
|
+
assert.equal(stored.id, id);
|
|
112
|
+
});
|
|
113
|
+
it('unversioned writes stay in L1 only', async () => {
|
|
114
|
+
let id = unversionedId();
|
|
115
|
+
EcRepository.cache[id] = makeThing(id);
|
|
116
|
+
assert.isDefined(EcRepository.cacheBacking[id], "Entry missing from L1.");
|
|
117
|
+
assert.isNull(await l2Get(id), "Unversioned entry should not be in IndexedDB.");
|
|
118
|
+
});
|
|
119
|
+
it('cacheGet falls back to IndexedDB on L1 miss', async () => {
|
|
120
|
+
let id = versionedId();
|
|
121
|
+
let thing = makeThing(id);
|
|
122
|
+
EcRepository.cache[id] = thing;
|
|
123
|
+
// Evict from L1 only; a proxy delete would also remove the L2 copy.
|
|
124
|
+
delete EcRepository.cacheBacking[id];
|
|
125
|
+
let got = await EcRepository.cacheGet(id);
|
|
126
|
+
assert.instanceOf(got, EcRemoteLinkedData);
|
|
127
|
+
assert.equal(got.id, id);
|
|
128
|
+
assert.equal(got.getName(), thing.getName());
|
|
129
|
+
});
|
|
130
|
+
it('delete through the proxy removes the entry from both layers', async () => {
|
|
131
|
+
let id = versionedId();
|
|
132
|
+
EcRepository.cache[id] = makeThing(id);
|
|
133
|
+
delete EcRepository.cache[id];
|
|
134
|
+
assert.isUndefined(EcRepository.cacheBacking[id], "Entry still in L1.");
|
|
135
|
+
assert.isNull(await l2Get(id), "Entry still in IndexedDB.");
|
|
136
|
+
assert.isUndefined(await EcRepository.cacheGet(id));
|
|
137
|
+
});
|
|
138
|
+
it('setting an entry to null removes it from IndexedDB', async () => {
|
|
139
|
+
let id = versionedId();
|
|
140
|
+
EcRepository.cache[id] = makeThing(id);
|
|
141
|
+
assert.isNotNull(await l2Get(id));
|
|
142
|
+
EcRepository.cache[id] = null;
|
|
143
|
+
assert.isNull(await l2Get(id), "Entry still in IndexedDB.");
|
|
144
|
+
assert.isNull(EcRepository.cacheBacking[id], "L1 should hold the null marker.");
|
|
145
|
+
delete EcRepository.cache[id];
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("clearCache", () => {
|
|
150
|
+
it('clears both L1 and L2 when L2 caching is enabled', async () => {
|
|
151
|
+
EcRepository.cachingL2 = true;
|
|
152
|
+
let vId = versionedId();
|
|
153
|
+
let uId = unversionedId();
|
|
154
|
+
EcRepository.cache[vId] = makeThing(vId);
|
|
155
|
+
EcRepository.cache[uId] = makeThing(uId);
|
|
156
|
+
assert.isAbove(await l2Count(), 0);
|
|
157
|
+
EcRepository.clearCache();
|
|
158
|
+
assert.equal(Object.keys(EcRepository.cacheBacking).length, 0);
|
|
159
|
+
assert.equal(await l2Count(), 0, "IndexedDB store not emptied.");
|
|
160
|
+
assert.isUndefined(await EcRepository.cacheGet(vId));
|
|
161
|
+
assert.isUndefined(await EcRepository.cacheGet(uId));
|
|
162
|
+
});
|
|
163
|
+
it('clears only L1 when L2 caching is disabled', async () => {
|
|
164
|
+
EcRepository.cachingL2 = true;
|
|
165
|
+
let id = versionedId();
|
|
166
|
+
EcRepository.cache[id] = makeThing(id);
|
|
167
|
+
EcRepository.cachingL2 = false;
|
|
168
|
+
EcRepository.clearCache();
|
|
169
|
+
assert.equal(Object.keys(EcRepository.cacheBacking).length, 0);
|
|
170
|
+
assert.isNotNull(await l2Get(id), "IndexedDB copy should survive an L1-only clear.");
|
|
171
|
+
// The surviving L2 copy is served again once L2 caching is re-enabled.
|
|
172
|
+
EcRepository.cachingL2 = true;
|
|
173
|
+
let got = await EcRepository.cacheGet(id);
|
|
174
|
+
assert.instanceOf(got, EcRemoteLinkedData);
|
|
175
|
+
assert.equal(got.id, id);
|
|
176
|
+
EcRepository.clearCache();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|