@igea/oac_backend 1.0.14 → 1.0.16

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
@@ -1,2 +1,8 @@
1
1
  # oac_backend
2
2
  Backend service for the OAC project
3
+
4
+ ## Required Ubuntu libraries
5
+ ### XSLT
6
+ ```console
7
+ sudo apt install xsltproc
8
+ ```
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@igea/oac_backend",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Backend service for the OAC project",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "start": "cross-env NODE_ENV=production node src/index.js",
8
- "dev": "cross-env NODE_ENV=development nodemon src/index.js"
8
+ "dev": "cross-env NODE_ENV=development nodemon src/index.js",
9
+ "test": "mocha \"test/**/*.js\""
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -24,10 +25,17 @@
24
25
  "express": "5.1.0",
25
26
  "get-port": "7.1.0",
26
27
  "knex": "3.1.0",
27
- "pg": "8.16.3"
28
+ "libxmljs2": "0.37.0",
29
+ "pg": "8.16.3",
30
+ "strip-bom": "5.0.0",
31
+ "xslt-processor": "3.3.1"
28
32
  },
29
33
  "devDependencies": {
34
+ "chai": "5.2.1",
30
35
  "cross-env": "7.0.3",
31
- "nodemon": "3.1.10"
36
+ "mocha": "11.7.1",
37
+ "nodemon": "3.1.10",
38
+ "sinon": "21.0.0",
39
+ "supertest": "7.1.3"
32
40
  }
33
41
  }
@@ -9,6 +9,7 @@ const configFuseki = config.fuseki || {
9
9
  }
10
10
  const fusekiUrl = `${configFuseki.protocol}://${configFuseki.host}:${configFuseki.port}/${configFuseki.dataset}/sparql`;
11
11
  const axios = require('axios');
12
+ const Fuseki = require('../models/fuseki');
12
13
 
13
14
  router.get('/count/entities', (req, res) => {
14
15
  const query = `
@@ -50,4 +51,49 @@ router.get('/count/entities', (req, res) => {
50
51
 
51
52
  });
52
53
 
54
+ router.get('/export/:format/:entity/:id', (req, res) => {
55
+ let accept = 'application/rdf+xml'
56
+ let filename = `${req.params.entity}_${req.params.id}`;
57
+ switch(req.params.format){
58
+ case 'turtle':
59
+ accept='text/turtle'
60
+ filename += ".ttl"
61
+ break;
62
+ case 'json':
63
+ accept='application/ld+json'
64
+ filename += ".jsonld"
65
+ break;
66
+ case 'n-triples':
67
+ accept='application/n-triples'
68
+ filename += ".nt"
69
+ break;
70
+ case 'trig':
71
+ accept='application/trig'
72
+ filename += ".trig"
73
+ break;
74
+ default:
75
+ filename += ".rdf"
76
+ }
77
+ const uri = 'http://diagnostica/campione/1'
78
+ const query = Fuseki.getQueryDownload2(uri, 4)
79
+ axios.post(fusekiUrl, `query=${encodeURIComponent(query)}`,{
80
+ headers: {
81
+ 'Accept': `${accept}`,
82
+ 'Content-Type': 'application/x-www-form-urlencoded'
83
+ },
84
+ responseType: 'stream'
85
+ }).then(response => {
86
+ res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
87
+ res.setHeader('Content-Type', response.headers['content-type'] || 'application/octet-stream');
88
+ response.data.pipe(res);
89
+ }).catch(err => {
90
+ res.status(500).json({
91
+ success: false,
92
+ data: null,
93
+ message: `Error: ${err}`
94
+ });
95
+ });
96
+
97
+ });
98
+
53
99
  module.exports = router
package/src/index.js CHANGED
@@ -11,7 +11,11 @@ const jwtLib = jwtLibFactory({
11
11
  `/${serviceName}/auth/authenticate`,
12
12
  `/${serviceName}/auth/echo`,
13
13
  `/${serviceName}/health`,
14
- `/${serviceName}/fuseki/count/entities`
14
+
15
+ `/${serviceName}/fuseki/count/entities`,
16
+ `/${serviceName}/fuseki/export/rdf/campione/1`,
17
+ `/${serviceName}/fuseki/export/turtle/campione/1`
18
+
15
19
  ],
16
20
  signOptions: { expiresIn: '15m' }
17
21
  });
@@ -0,0 +1,89 @@
1
+ class Fuseki {
2
+
3
+ /**
4
+ * Return the query to retrieve the definition of an entity
5
+ * @param {string} entityUrl
6
+ * @returns
7
+ */
8
+ static getQueryDownload(entityUrl){
9
+ //http://diagnostica/campione/1
10
+ return `
11
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
12
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
13
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
14
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
15
+ PREFIX dcterms: <http://purl.org/dc/terms/>
16
+ PREFIX foaf: <http://xmlns.com/foaf/0.1/>
17
+ PREFIX schema: <http://schema.org/>
18
+
19
+ CONSTRUCT {
20
+ <${entityUrl}> ?p1 ?o1 .
21
+ ?o1 ?p2 ?o2 .
22
+ ?o2 ?p3 ?o3 .
23
+ ?o3 ?p4 ?o4 .
24
+ }
25
+ WHERE {
26
+ <${entityUrl}> ?p1 ?o1 .
27
+ OPTIONAL {
28
+ ?o1 ?p2 ?o2 .
29
+ OPTIONAL {
30
+ ?o2 ?p3 ?o3 .
31
+ OPTIONAL {
32
+ ?o3 ?p4 ?o4 .
33
+ }
34
+ }
35
+ }
36
+ }
37
+ `;
38
+ }
39
+
40
+ static getQueryDownload2(entityUrl, depth = 3) {
41
+ const prefixes = `
42
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
43
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
44
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
45
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
46
+ PREFIX dcterms: <http://purl.org/dc/terms/>
47
+ PREFIX foaf: <http://xmlns.com/foaf/0.1/>
48
+ PREFIX schema: <http://schema.org/>
49
+ `;
50
+
51
+ // Generate CONSTRUCT triples
52
+ let constructTriples = [`<${entityUrl}> ?p1 ?o1 .`];
53
+ for (let i = 1; i <= depth; i++) {
54
+ const subject = i === 1 ? `?o1` : `?o${i}`;
55
+ const predicate = `?p${i + 1}`;
56
+ const object = `?o${i + 1}`;
57
+ constructTriples.push(`${subject} ${predicate} ${object} .`);
58
+ }
59
+
60
+ // Generate nested OPTIONAL blocks in WHERE
61
+ let where = `<${entityUrl}> ?p1 ?o1 .\n`;
62
+ let indent = '';
63
+ for (let i = 1; i <= depth; i++) {
64
+ const subject = i === 1 ? `?o1` : `?o${i}`;
65
+ const predicate = `?p${i + 1}`;
66
+ const object = `?o${i + 1}`;
67
+ where += `${indent}OPTIONAL {\n`;
68
+ indent += ' ';
69
+ where += `${indent}${subject} ${predicate} ${object} .\n`;
70
+ }
71
+ // Close all OPTIONALs
72
+ for (let i = 0; i < depth; i++) {
73
+ indent = indent.slice(0, -2);
74
+ where += `${indent}}\n`;
75
+ }
76
+
77
+ return `
78
+ ${prefixes}
79
+ CONSTRUCT {
80
+ ${constructTriples.join('\n ')}
81
+ }
82
+ WHERE {
83
+ ${where}
84
+ }`;
85
+ }
86
+
87
+ }
88
+
89
+ module.exports = Fuseki;
@@ -0,0 +1,82 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const XsltProcessor = require('xslt-processor');
4
+ const {XmlParser, Xslt} = XsltProcessor;
5
+ const libxmljs = require('libxmljs2');
6
+ const { exec } = require('child_process');
7
+ const stripBom = require('strip-bom').default;
8
+ const xslt = new Xslt({
9
+ escape: false,
10
+ outputMethod: 'text'
11
+ });
12
+ const xmlParser = new XmlParser();
13
+
14
+
15
+ class Parser{
16
+
17
+ static _INSTANCE = null;
18
+
19
+ constructor(){
20
+ let xsdPath = path.join(__dirname, 'vocabolaries.xsd');
21
+ this.xsltPath = path.join(__dirname, 'vocabolaries.xslt');
22
+ this.xsdData = fs.readFileSync(xsdPath, 'utf8');
23
+ this.xsdDoc = libxmljs.parseXml(this.xsdData);
24
+ this.xsltData = fs.readFileSync(this.xsltPath, 'utf8');
25
+ //this.xslt = xmlParser.xmlParse(this.xsltData);
26
+ this.xsltDoc = libxmljs.parseXml(this.xsdData);
27
+ }
28
+
29
+ _getXmlData(xmlPath){
30
+ let _xmlData = fs.readFileSync(xmlPath, 'utf8');
31
+ let xmlData = stripBom(_xmlData);
32
+ return xmlData;
33
+ }
34
+
35
+ parse(xmlPath){
36
+ let xmlData = this._getXmlData(xmlPath);
37
+ let xmlDoc = libxmljs.parseXml(xmlData);
38
+ let validation = xmlDoc.validate(this.xsdDoc);
39
+ if (!validation) {
40
+ throw new Error(`XML validation failed: ${xmlDoc.validationErrors}`);
41
+ }
42
+ return xmlDoc;
43
+ }
44
+
45
+ transform(xmlPath){
46
+ return new Promise((resolve, reject) => {
47
+ try{
48
+ exec('xsltproc ' + this.xsltPath + ' ' + xmlPath, (err, stdout, stderr) => {
49
+ if (err) {
50
+ reject(err)
51
+ }else{
52
+ var terms = stdout.split('\n')
53
+ resolve(terms.map(line => line.trim()).filter(line => line.length > 0));
54
+ }
55
+ });
56
+ }catch(e){
57
+ reject(e)
58
+ }
59
+
60
+ /*
61
+ let xmlData = this._getXmlData(xmlPath);
62
+ const xml = xmlParser.xmlParse(xmlData);
63
+ xslt.xsltProcess(xml, this.xslt)
64
+ .then(result => {
65
+ var terms = result.split('&#10;')
66
+ resolve(terms.map(line => line.trim()).filter(line => line.length > 0));
67
+ }).catch(err => {
68
+ reject(err)
69
+ });
70
+ */
71
+ });
72
+ }
73
+
74
+ static GET_INSTANCE(){
75
+ if (Parser._INSTANCE === null) {
76
+ Parser._INSTANCE = new Parser();
77
+ }
78
+ return Parser._INSTANCE;
79
+ }
80
+ }
81
+
82
+ module.exports = Parser
@@ -0,0 +1,54 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3
+ elementFormDefault="qualified">
4
+
5
+ <!-- Radice -->
6
+ <xs:element name="vocabularies">
7
+ <xs:complexType>
8
+ <xs:sequence>
9
+ <!-- Required prefix element with a mandatory "value" attribute -->
10
+ <xs:element name="prefix">
11
+ <xs:complexType>
12
+ <xs:attribute name="value" type="xs:string" use="required"/>
13
+ </xs:complexType>
14
+ </xs:element>
15
+
16
+ <!-- Multiple vocabulary entries -->
17
+ <xs:element name="vocabulary" maxOccurs="unbounded">
18
+ <xs:complexType>
19
+ <xs:sequence>
20
+ <xs:element name="term" type="termType"/>
21
+ </xs:sequence>
22
+ </xs:complexType>
23
+ </xs:element>
24
+ </xs:sequence>
25
+ </xs:complexType>
26
+ </xs:element>
27
+
28
+ <!-- Tipo ricorsivo per ogni term -->
29
+ <xs:complexType name="termType">
30
+ <xs:sequence>
31
+ <xs:element name="name">
32
+ <xs:complexType>
33
+ <xs:simpleContent>
34
+ <xs:extension base="xs:string">
35
+ <xs:attribute name="lang" type="xs:string" use="required"/>
36
+ </xs:extension>
37
+ </xs:simpleContent>
38
+ </xs:complexType>
39
+ </xs:element>
40
+
41
+ <!-- Sub-terms ricorsivo -->
42
+ <xs:element name="sub-terms" minOccurs="0">
43
+ <xs:complexType>
44
+ <xs:sequence>
45
+ <xs:element name="term" type="termType" maxOccurs="unbounded"/>
46
+ </xs:sequence>
47
+ </xs:complexType>
48
+ </xs:element>
49
+ </xs:sequence>
50
+
51
+ <xs:attribute name="id" type="xs:string" use="required"/>
52
+ </xs:complexType>
53
+
54
+ </xs:schema>
@@ -0,0 +1,50 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
3
+ version="1.0">
4
+
5
+ <xsl:output method="text" encoding="UTF-8"/>
6
+
7
+ <!-- Prendi il prefix e passa ai vocabolari -->
8
+ <xsl:template match="/vocabularies">
9
+ <xsl:variable name="prefix" select="prefix/@value"/>
10
+ <xsl:apply-templates select="vocabulary">
11
+ <xsl:with-param name="prefix" select="$prefix"/>
12
+ </xsl:apply-templates>
13
+ </xsl:template>
14
+
15
+ <!-- Vocabolario -->
16
+ <xsl:template match="vocabulary">
17
+ <xsl:param name="prefix"/>
18
+ <xsl:variable name="vocab-id" select="@id"/>
19
+ <xsl:apply-templates select="term">
20
+ <xsl:with-param name="path" select="concat('http://', $prefix, $vocab-id)"/>
21
+ </xsl:apply-templates>
22
+ </xsl:template>
23
+
24
+ <!-- Term ricorsivo -->
25
+ <xsl:template match="term">
26
+ <xsl:param name="path"/>
27
+ <xsl:variable name="current-id" select="@id"/>
28
+ <xsl:variable name="new-path" select="concat($path, '/', $current-id)"/>
29
+
30
+ <xsl:choose>
31
+ <!-- Se NON ha sub-terms/term -->
32
+ <xsl:when test="not(sub-terms/term)">
33
+ <xsl:for-each select="name">
34
+ <xsl:value-of select="$new-path"/>
35
+ <xsl:text> rdfs:label "</xsl:text>
36
+ <xsl:value-of select="."/>
37
+ <xsl:text>"@</xsl:text>
38
+ <xsl:value-of select="@lang"/>
39
+ <xsl:text>&#10;</xsl:text>
40
+ </xsl:for-each>
41
+ </xsl:when>
42
+ <xsl:otherwise>
43
+ <xsl:apply-templates select="sub-terms/term">
44
+ <xsl:with-param name="path" select="$new-path"/>
45
+ </xsl:apply-templates>
46
+ </xsl:otherwise>
47
+ </xsl:choose>
48
+ </xsl:template>
49
+
50
+ </xsl:stylesheet>
@@ -0,0 +1,34 @@
1
+ const chai = require('chai');
2
+ const expect = chai.expect;
3
+ const request = require('supertest');
4
+ const Parser = require('../../../src/models/vocabolaries/parser');
5
+
6
+
7
+ describe('Vocabolaries.Parsers', () => {
8
+
9
+ beforeEach(() => {
10
+
11
+ });
12
+
13
+ it('should create a parser instance', () => {
14
+ const parser = Parser.GET_INSTANCE();
15
+ expect(parser).to.be.an.instanceof(Parser);
16
+ });
17
+
18
+ it('should parse the vocabolaries.xml file', () => {
19
+ const parser = Parser.GET_INSTANCE();
20
+ var xmlDoc = parser.parse(__dirname + '/vocabolaries.xml');
21
+ expect(xmlDoc).to.not.be.null;
22
+ let nodes = xmlDoc.find('//vocabulary');
23
+ expect(nodes.length).to.be.equal(10);
24
+ });
25
+
26
+ it('should transform the vocabolaries.xml file', async () => {
27
+ const parser = Parser.GET_INSTANCE();
28
+ var terms = await parser.transform(__dirname + '/vocabolaries.xml');
29
+ expect(terms.length).to.be.equal(193);
30
+ expect(terms[0]).to.be.equal('http://diagnostica/condizioni-ambientali/ambiente/buio rdfs:label "Buio"@it');
31
+ expect(terms[1]).to.be.equal('http://diagnostica/condizioni-ambientali/ambiente/illuminato rdfs:label "Illuminato"@it');
32
+ });
33
+
34
+ });