@igea/oac_backend 1.0.43 → 1.0.45
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": "@igea/oac_backend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "Backend service for the OAC project",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"n3": "1.26.0",
|
|
37
37
|
"nodemailer": "7.0.6",
|
|
38
38
|
"pg": "8.16.3",
|
|
39
|
+
"rdf-validate-shacl": "0.6.5",
|
|
39
40
|
"strip-bom": "5.0.0",
|
|
40
41
|
"tmp": "0.2.5"
|
|
41
42
|
},
|
|
@@ -4,50 +4,61 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const ONTO_FOLDER = path.join(__dirname, '..', 'ontology');
|
|
6
6
|
const Converter = require('../models/converter');
|
|
7
|
+
const Validator = require('../models/validator');
|
|
7
8
|
const tmp = require('tmp');
|
|
9
|
+
const Investigations = require('../models/investigations');
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
let SCHEMAS = {}
|
|
12
|
+
|
|
13
|
+
const getSchema = function(format){
|
|
14
|
+
let fileContent = null;
|
|
12
15
|
let filePath = null;
|
|
16
|
+
let fileType = null;
|
|
13
17
|
switch(format){
|
|
14
18
|
case 'ttl':
|
|
15
19
|
filePath = 'config.shacl.ttl'; //'schema_v1.shacl.ttl';
|
|
16
|
-
|
|
20
|
+
fileType = 'text/turtle';
|
|
17
21
|
break;
|
|
18
22
|
case 'ttl2':
|
|
19
23
|
filePath = 'schema_v2.shacl.ttl';
|
|
20
|
-
|
|
24
|
+
fileType = 'text/turtle';
|
|
21
25
|
break;
|
|
22
26
|
case 'jsonld':
|
|
23
27
|
filePath = 'schema_v1.shacl.jsonld';
|
|
24
|
-
|
|
28
|
+
fileType = 'application/ld+json';
|
|
25
29
|
break;
|
|
26
30
|
case 'xml':
|
|
27
31
|
filePath = 'schema_v1.shacl.rdf';
|
|
28
|
-
|
|
32
|
+
fileType = 'application/rdf+xml';
|
|
29
33
|
break;
|
|
30
34
|
default:
|
|
31
|
-
|
|
32
|
-
success: false,
|
|
33
|
-
data: null,
|
|
34
|
-
message: `Unsupported format: ${format}`
|
|
35
|
-
});
|
|
36
|
-
return;
|
|
35
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
37
36
|
}
|
|
37
|
+
if(SCHEMAS.hasOwnProperty(format)){
|
|
38
|
+
fileContent = SCHEMAS[format];
|
|
39
|
+
}
|
|
40
|
+
if(!fileContent){
|
|
41
|
+
fileContent = fs.readFileSync(path.join(ONTO_FOLDER, filePath), 'utf8');
|
|
42
|
+
let protocol = process.env.OAC_EXPOSED_PROTOCOL || 'http';
|
|
43
|
+
let host = process.env.OAC_EXPOSED_HOST || '127.0.0.1';
|
|
44
|
+
if(host=="localhost") host="127.0.0.1";
|
|
45
|
+
let port = process.env.OAC_EXPOSED_PORT || '4000';
|
|
46
|
+
fileContent = fileContent.replace(/OAC_EXPOSED_PROTOCOL/g, protocol);
|
|
47
|
+
fileContent = fileContent.replace(/OAC_EXPOSED_HOST/g, host);
|
|
48
|
+
fileContent = fileContent.replace(/OAC_EXPOSED_PORT/g, port);
|
|
49
|
+
SCHEMAS[format] = fileContent;
|
|
50
|
+
}
|
|
51
|
+
return { content: fileContent , path: filePath, type: fileType };
|
|
52
|
+
}
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fileContent = fileContent.replace(/OAC_EXPOSED_PORT/g, port);
|
|
47
|
-
|
|
48
|
-
const tempFile = tmp.fileSync({ postfix: filePath });
|
|
54
|
+
router.get('/schema/:format', (req, res) => {
|
|
55
|
+
console.log(`Requesting SHACL schema in format: ${req.params.format}`);
|
|
56
|
+
let format = req.params.format || 'ttl';
|
|
57
|
+
let schema = getSchema(format);
|
|
58
|
+
let fileContent = schema.content;
|
|
59
|
+
res.setHeader('Content-Type', schema.type);
|
|
60
|
+
const tempFile = tmp.fileSync({ postfix: schema.path });
|
|
49
61
|
fs.writeFileSync(tempFile.name, fileContent);
|
|
50
|
-
|
|
51
62
|
res.sendFile(tempFile.name, (err) => {
|
|
52
63
|
if (err) {
|
|
53
64
|
res.status(500).json({
|
|
@@ -59,6 +70,77 @@ router.get('/schema/:format', (req, res) => {
|
|
|
59
70
|
});
|
|
60
71
|
});
|
|
61
72
|
|
|
73
|
+
router.get('/counter/:name', (req, res) => {
|
|
74
|
+
let name = req.params.name;
|
|
75
|
+
Investigations.getCounter(name).then( (count) => {
|
|
76
|
+
res.json({
|
|
77
|
+
success: true,
|
|
78
|
+
data: count,
|
|
79
|
+
message: `Counter ${name} value retrieved`
|
|
80
|
+
});
|
|
81
|
+
}).catch( (err) => {
|
|
82
|
+
console.log(err);
|
|
83
|
+
res.status(500).json({
|
|
84
|
+
success: false,
|
|
85
|
+
data: null,
|
|
86
|
+
message: `Error retrieving counter ${name}: ${err}`
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
router.post('/validate', (req, res) => {
|
|
92
|
+
let turtle = req.body.turtle;
|
|
93
|
+
let schema = getSchema('ttl2');
|
|
94
|
+
console.log("validate...")
|
|
95
|
+
let shacl = schema.content.replace(/owl:imports/g, '#owl:imports');
|
|
96
|
+
Validator.validateDataSyntax(turtle, shacl).then( (result) => {
|
|
97
|
+
res.json({
|
|
98
|
+
success: true,
|
|
99
|
+
data: result,
|
|
100
|
+
message: 'Validation completed'
|
|
101
|
+
});
|
|
102
|
+
}).catch( (err) => {
|
|
103
|
+
console.log(err);
|
|
104
|
+
res.status(500).json({
|
|
105
|
+
success: false,
|
|
106
|
+
data: null,
|
|
107
|
+
message: `Validation error: ${err}`
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
router.post('/form/save', (req, res) => {
|
|
113
|
+
let dataset = req.body.turtle;
|
|
114
|
+
let uuid = req.body.uuid;
|
|
115
|
+
Investigations.save({
|
|
116
|
+
uuid, dataset, format: 'turtle'
|
|
117
|
+
}).then( () => {
|
|
118
|
+
res.json({
|
|
119
|
+
success: true
|
|
120
|
+
});
|
|
121
|
+
}).catch( (err) => {
|
|
122
|
+
console.log("Error saving investigation: ", err);
|
|
123
|
+
res.json({
|
|
124
|
+
success: false,
|
|
125
|
+
message: `Error: ${err}`
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
router.get('/form/:uuid', (req, res) => {
|
|
131
|
+
let uuid = req.params.uuid;
|
|
132
|
+
res.setHeader('Content-Type', 'text/turtle');
|
|
133
|
+
Investigations.get(uuid).then( (response) => {
|
|
134
|
+
res.send(response ? response.dataset : null);
|
|
135
|
+
}).catch( (err) => {
|
|
136
|
+
console.log("Error getting investigation datset: ", err);
|
|
137
|
+
res.json({
|
|
138
|
+
success: false,
|
|
139
|
+
message: `Error: ${err}`
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
62
144
|
router.get('/schema/:type/:what', (req, res) => {
|
|
63
145
|
console.log(`Requesting SHACL schema in format: ${req.params.type}`);
|
|
64
146
|
let type = req.params.type || 'config';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const {db, schema } = require('./db')
|
|
2
|
+
const table = `${schema}.investigations`
|
|
3
|
+
|
|
4
|
+
class Investigations {
|
|
5
|
+
|
|
6
|
+
static get(uuid){
|
|
7
|
+
return new Promise(async (resolve, reject) => {
|
|
8
|
+
try{
|
|
9
|
+
const item = await db(table).select('*').where({ uuid }).first();
|
|
10
|
+
resolve(item)
|
|
11
|
+
}catch(e){
|
|
12
|
+
reject(e)
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getCounter(name){
|
|
18
|
+
return new Promise(async (resolve, reject) => {
|
|
19
|
+
try{
|
|
20
|
+
const sql = `SELECT nextval('${name}') as count`;
|
|
21
|
+
const result = await db.raw(sql);
|
|
22
|
+
const count = result.rows[0].count;
|
|
23
|
+
resolve(count)
|
|
24
|
+
}catch(e){
|
|
25
|
+
reject(e)
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static save(item){
|
|
31
|
+
item.format = item.format || 'turtle'
|
|
32
|
+
return new Promise(async (resolve, reject) => {
|
|
33
|
+
try {
|
|
34
|
+
const existing = await db(table)
|
|
35
|
+
.where({uuid: item.uuid})
|
|
36
|
+
.first();
|
|
37
|
+
let operation = existing ? 'UPDATE' : 'INSERT';
|
|
38
|
+
if(existing){
|
|
39
|
+
await db(table)
|
|
40
|
+
.where({uuid: item.uuid})
|
|
41
|
+
.update(item);
|
|
42
|
+
}else{
|
|
43
|
+
await db(table).insert(item);
|
|
44
|
+
}
|
|
45
|
+
resolve({
|
|
46
|
+
success: true, operation
|
|
47
|
+
})
|
|
48
|
+
}catch(e){
|
|
49
|
+
reject(e)
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = Investigations
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { Parser, Store } = require('n3');
|
|
2
|
+
const SHACLValidator = require('rdf-validate-shacl').default;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Validator {
|
|
6
|
+
|
|
7
|
+
static turtleToDataset(turtleStr) {
|
|
8
|
+
const parser = new Parser({ format: 'text/turtle' });
|
|
9
|
+
const quads = parser.parse(turtleStr);
|
|
10
|
+
const store = new Store();
|
|
11
|
+
quads.forEach(q => store.addQuad(q));
|
|
12
|
+
return store;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static async validateDataSyntax(dataContent, shapesContent) {
|
|
16
|
+
const dataDS = Validator.turtleToDataset(dataContent);
|
|
17
|
+
const shapesDS = Validator.turtleToDataset(shapesContent);
|
|
18
|
+
const validator = new SHACLValidator(shapesDS);
|
|
19
|
+
const report = await validator.validate(dataDS);
|
|
20
|
+
const details = (report.results || []).map(r => ({
|
|
21
|
+
focusNode: r.focusNode?.value,
|
|
22
|
+
message: (r.message || []).map(m => m.value || m),
|
|
23
|
+
path: r.path?.value,
|
|
24
|
+
severity: r.severity?.value,
|
|
25
|
+
sourceShape: r.sourceShape?.value,
|
|
26
|
+
sourceConstraintComponent: r.sourceConstraintComponent?.value,
|
|
27
|
+
value: r.value?.value
|
|
28
|
+
}));
|
|
29
|
+
return {
|
|
30
|
+
conforms: report.conforms,
|
|
31
|
+
details
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = Validator ;
|
|
@@ -117,14 +117,14 @@ ex:E29ActorShape
|
|
|
117
117
|
sh:targetClass crm:E29_Actor ;
|
|
118
118
|
sh:property [
|
|
119
119
|
sh:path ex:ente_richiedente ;
|
|
120
|
-
|
|
120
|
+
sh:datatype xsd:string ;
|
|
121
121
|
|
|
122
122
|
#sh:name "Ente richiedente" ;
|
|
123
123
|
#sh:maxCount 1 ;
|
|
124
124
|
|
|
125
125
|
sh:name "ID" ;
|
|
126
126
|
sh:nodeKind sh:IRI;
|
|
127
|
-
sh:description "http://diagnostica/actor/$
|
|
127
|
+
sh:description "http://diagnostica/actor/$SEQ3$" ;
|
|
128
128
|
|
|
129
129
|
] ;
|
|
130
130
|
sh:property [
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const chai = require('chai');
|
|
2
|
+
const expect = chai.expect;
|
|
3
|
+
const request = require('supertest');
|
|
4
|
+
const Validator = require('../../src/models/validator');
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
|
|
7
|
+
describe('Validator', () => {
|
|
8
|
+
|
|
9
|
+
const shaclTurtle = `
|
|
10
|
+
@prefix ex: <http://example.com/ns#> .
|
|
11
|
+
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
12
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
13
|
+
|
|
14
|
+
ex:PersonShape a sh:NodeShape ;
|
|
15
|
+
sh:targetClass ex:Person ;
|
|
16
|
+
sh:property [
|
|
17
|
+
sh:path ex:age ;
|
|
18
|
+
sh:datatype xsd:integer ; # Richiede un intero
|
|
19
|
+
] .
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should convert a turtle into a Dataset with 3 triples', () => {
|
|
27
|
+
const inputTurtle = `
|
|
28
|
+
@prefix ex: <http://example.org/> .
|
|
29
|
+
ex:subject1 ex:predicate1 ex:object1 .
|
|
30
|
+
ex:subject2 ex:predicate2 ex:object2 .
|
|
31
|
+
ex:subject3 ex:predicate3 ex:object3 .
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const ds = Validator.turtleToDataset(inputTurtle);
|
|
35
|
+
|
|
36
|
+
// Controlla che siano presenti 3 triple
|
|
37
|
+
expect(ds.size).to.equal(3);
|
|
38
|
+
|
|
39
|
+
// Controllo opzionale: verifica che i soggetti siano corretti
|
|
40
|
+
const subjects = Array.from(ds).map(q => q.subject.value);
|
|
41
|
+
expect(subjects).to.include.members([
|
|
42
|
+
'http://example.org/subject1',
|
|
43
|
+
'http://example.org/subject2',
|
|
44
|
+
'http://example.org/subject3'
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should fail for turtle with wrong age type', async () => {
|
|
49
|
+
const wrongTurtle = `
|
|
50
|
+
@prefix ex: <http://example.com/ns#> .
|
|
51
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
52
|
+
ex:Alice a ex:Person ;
|
|
53
|
+
ex:age "dodici" . # Valore letterale non valido per xsd:integer
|
|
54
|
+
`;
|
|
55
|
+
const result = await Validator.validateDataSyntax(wrongTurtle, shaclTurtle);
|
|
56
|
+
expect(result.conforms).to.equal(false);
|
|
57
|
+
expect(result.details.length).to.equal(1);
|
|
58
|
+
expect(result.details[0].sourceConstraintComponent).to.equal('http://www.w3.org/ns/shacl#DatatypeConstraintComponent');
|
|
59
|
+
expect(result.details[0].value).to.equal('dodici');
|
|
60
|
+
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should pass for turtle with valid age type', async () => {
|
|
64
|
+
const wrongTurtle = `
|
|
65
|
+
@prefix ex: <http://example.com/ns#> .
|
|
66
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
67
|
+
ex:Alice a ex:Person ;
|
|
68
|
+
ex:age 12 . # Valore valido per xsd:integer
|
|
69
|
+
`;
|
|
70
|
+
const result = await Validator.validateDataSyntax(wrongTurtle, shaclTurtle);
|
|
71
|
+
expect(result.conforms).to.equal(true);
|
|
72
|
+
expect(result.details.length).to.equal(0);
|
|
73
|
+
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
})
|