From 1c7c87e7fd5c31540b9d1e3cb7d7eb5f2477b13c Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Wed, 29 Oct 2025 17:27:41 +0100 Subject: [PATCH 1/4] Cleaned export; tested reserved variable name id --- CHANGELOG.md | 5 ++ README.md | 15 +++- main/configs/test/config.json | 12 +++ main/configs/test/public/queries/idols_id.rq | 9 +++ main/package-lock.json | 76 ++++++++----------- .../QueryResultList/QueryResultList.jsx | 32 +++++++- main/src/dataProvider/SparqlDataProvider.js | 8 ++ test/cypress.config.js | 44 +++++++++++ test/cypress/e2e/export.cy.js | 30 ++++++++ .../cypress/e2e/reserved-variable-names.cy.js | 9 +++ test/cypress/support/commands.js | 35 +++++++++ test/package-lock.json | 8 ++ test/package.json | 1 + 13 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 main/configs/test/public/queries/idols_id.rq create mode 100644 test/cypress/e2e/export.cy.js create mode 100644 test/cypress/e2e/reserved-variable-names.cy.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dec392..1df3acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Verifiable credentials use VC prototyping library now (#238). +- Export cleaned so that it doesn't contain irrelevant columns (#242). + +### Added + +- Documented and tested SPARQL query restrictions (#243). ## [2.1.0] - 2025-09-25 diff --git a/README.md b/README.md index acd04a8..78d9804 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Table of contents: * [Static, production build](#static-production-build) * [Logging in](#logging-in) * [Configuration file](#configuration-file) + * [Writing SPARQL queries](#writing-sparql-queries) * [Specifying sources](#specifying-sources) * [About httpProxies](#about-httpproxies) * [Adding variable type](#adding-variable-type) @@ -154,7 +155,7 @@ The configuration file must follow the structure shown below. "logoLocation": "Image location of the logo shown at the top of the app (relative to public folder.).", "logoRedirectURL": "The URL the Web application redirects to when a user clicks on the logo.", "defaultIDP": "The default value used for IDP when logging in, this IDP can be manually changed in the Web app as well. ", - "queryFolder": "The base location of the queries, all query locations will start from this folder (relative to public folder).", + "queryFolder": "The base location of the SPARQL queries, all query locations will start from this folder (relative to public folder).", "introductionText": "The text that the app shows on the dashboard, which the app also shows when you first open it.", "queryGroups" : [ { @@ -167,7 +168,7 @@ The configuration file must follow the structure shown below. { "id": "A unique ID for the query. This ID appears in the URL of the displayed result. Queries are ordered in the menu according to ascending ID.", "queryGroupId": "ID of the query group too which this query belongs. If not given, the query is displayed outside existing groups.", - "queryLocation": "Path to the query location, relative to 'queryFolder'", + "queryLocation": "Path to the SPARQL query location, relative to 'queryFolder'", "name": "A name for the query", "description": "Description of the query", "icon": "The key to the icon for the query. This is optional and a default menu icon will be used when left empty.", @@ -213,6 +214,16 @@ The configuration file must follow the structure shown below. } ``` +### Writing SPARQL queries + +Write each [SPARQL query](https://www.w3.org/TR/sparql11-query/) in a separate file (see `queryLocation` in the configuration file). + +The following restrictions apply to SPARQL queries in Miravi: + +* Mark your query variables with `?`, not with `$`. The `$` sign is reserved for use in [templated queries](#templated-queries). +* Do not use `_` in variable names, unless for [adding variable type](#adding-variable-type). +* Do not use `id` as a variable name. It is reserved for internal use. + ### Specifying sources The set of sources over which a query will be executed is derived from two *optional* inputs in a query entry: diff --git a/main/configs/test/config.json b/main/configs/test/config.json index 4fd5b2c..0181b08 100644 --- a/main/configs/test/config.json +++ b/main/configs/test/config.json @@ -607,6 +607,18 @@ ], "lenient": true } + }, + { + "id": "9300", + "queryGroupId": "gr-test", + "queryLocation": "idols_id.rq", + "name": "Reserved variable name test", + "description": "Tests a query with a reserved variable name.", + "comunicaContext": { + "sources": [ + "http://localhost:8000/example/idols" + ] + } } ] } \ No newline at end of file diff --git a/main/configs/test/public/queries/idols_id.rq b/main/configs/test/public/queries/idols_id.rq new file mode 100644 index 0000000..a1fd06a --- /dev/null +++ b/main/configs/test/public/queries/idols_id.rq @@ -0,0 +1,9 @@ +PREFIX schema: + +SELECT ?id ?birthDate_int WHERE { + ?list schema:name ?listTitle; + schema:itemListElement [ + schema:name ?id; + schema:birthDate ?birthDate_int; + ]. +} \ No newline at end of file diff --git a/main/package-lock.json b/main/package-lock.json index 994edd2..ee9e5e4 100644 --- a/main/package-lock.json +++ b/main/package-lock.json @@ -83,6 +83,7 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -10717,6 +10718,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -10760,6 +10762,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -11287,7 +11290,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==", "hasInstallScript": true, - "peer": true, "engines": { "node": ">=6" } @@ -11297,7 +11299,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==", "hasInstallScript": true, - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "^0.2.36" }, @@ -11426,7 +11427,6 @@ "resolved": "https://registry.npmjs.org/@inrupt/solid-client-authn-node/-/solid-client-authn-node-2.4.0.tgz", "integrity": "sha512-uJGh41Nb946gEvCXdR4SAq3LSlQWZiHmdfx4+biWinaLSD9bIorL8EGf+KYL4HZA0alFM5iy4LefPp+EqgcZHQ==", "license": "MIT", - "peer": true, "dependencies": { "@inrupt/solid-client-authn-core": "^2.4.0", "jose": "^5.1.3", @@ -11442,7 +11442,6 @@ "resolved": "https://registry.npmjs.org/@inrupt/solid-client-authn-core/-/solid-client-authn-core-2.4.0.tgz", "integrity": "sha512-jgIGsJLetycrNNlm3i4NNNANVLH2DMicKp0369TSabXobZN/QhIxdpTOpp8j1sYazH0g3/ROK9AG/oDqXL0o7Q==", "license": "MIT", - "peer": true, "dependencies": { "events": "^3.3.0", "jose": "^5.1.3", @@ -11457,7 +11456,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -11471,7 +11469,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -11588,6 +11585,7 @@ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -11614,6 +11612,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.17.1", @@ -11718,6 +11717,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.17.1", @@ -11772,6 +11772,7 @@ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "~7.2.15", @@ -12058,8 +12059,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@tarekraafat/autocomplete.js/-/autocomplete.js-7.2.0.tgz", "integrity": "sha512-p1aEcKC/WHpVBuFyRhXq/ie+mgO4QqCNEsdVIPUBgmNqmxV4dVfqYEpk///9vvKyranUUvrlVu4e2tdzAaXKIg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@triply/yasgui": { "version": "4.2.28", @@ -12145,7 +12145,6 @@ "resolved": "https://registry.npmjs.org/@triply/yasr/-/yasr-4.2.28.tgz", "integrity": "sha512-7EgOJX1/Xv1l9Sxu1S5Da/F+dG9hqANEfw/EJa6pbttkK+rQMN4Zv3paBD4dbHLwZGvybcupqCX7q7Ut+au8Bw==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/free-solid-svg-icons": "^5.14.0", "@triply/yasgui-utils": "^4.2.28", @@ -12174,8 +12173,7 @@ "version": "2.5.8", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/@ts-jison/common": { "version": "0.4.1-alpha.1", @@ -12364,6 +12362,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -12509,6 +12508,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12877,8 +12877,7 @@ "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "peer": true + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -12924,6 +12923,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -13094,7 +13094,6 @@ "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-9.1.0.tgz", "integrity": "sha512-6NnqiE/MNnNAiMzdW7phJ49nMQylkKMQ6La6PAS1+h1VhrGt38MOPnjzEJ3cRaECieqaGpl9eFGtI2icW27r8A==", "license": "MIT", - "peer": true, "dependencies": { "deepmerge": "^4.2.2", "fuse.js": "^3.4.6", @@ -13187,7 +13186,6 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.1.90" } @@ -13206,7 +13204,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/column-resizer/-/column-resizer-1.4.0.tgz", "integrity": "sha512-KM5Jh/UBKwVUr01oEGN/OvxF6gZIEn4c1Qde4iHSqNru9hxq93ao3u93qb9N1E1TZ2Sxjh4x7OHGe8v/P8FgkA==", - "peer": true, "dependencies": { "string-hash": "~1.1.3" }, @@ -13230,7 +13227,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "peer": true, "engines": { "node": ">= 6" } @@ -13537,7 +13533,6 @@ "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.11.tgz", "integrity": "sha512-AE6RkMXziRaqzPcu/pl3SJXeRa6fmXQG/fVjuRESujvkzqDCYEeKTTpPMuVJSGYJpPi32WGSphVNNY1G4nSN/g==", "license": "MIT", - "peer": true, "dependencies": { "jquery": "1.8 - 4" } @@ -13547,7 +13542,6 @@ "resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-1.13.11.tgz", "integrity": "sha512-4GpS4OFLwIMfhb5UdJh6bEnh0E52jIJOlx7KLKs1pSce/xpHjvcmucbUWNaEndQIpHXtIxmVPoqcDB0ZbiVB+A==", "license": "MIT", - "peer": true, "dependencies": { "datatables.net": "1.13.11", "jquery": "1.8 - 4" @@ -13601,7 +13595,6 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13985,8 +13978,7 @@ "node_modules/es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "peer": true + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" }, "node_modules/esbuild": { "version": "0.18.20", @@ -14054,6 +14046,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -14752,7 +14745,6 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", - "peer": true, "engines": { "node": ">=6" } @@ -15757,8 +15749,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", @@ -15834,7 +15825,6 @@ "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", - "peer": true, "dependencies": { "commander": "^6.1.0", "jsonparse": "^1.3.1", @@ -16053,14 +16043,12 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "engines": [ "node >= 0.2.0" - ], - "peer": true + ] }, "node_modules/jsuri": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsuri/-/jsuri-1.3.1.tgz", "integrity": "sha512-LLdAeqOf88/X0hylAI7oSir6QUsz/8kOW0FcJzzu/SJRfORA/oPHycAOthkNp7eLPlTAbqVDFbqNRHkRVzEA3g==", - "peer": true, "engines": { "node": "*" } @@ -16168,8 +16156,7 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -16263,6 +16250,7 @@ "integrity": "sha512-ndijEHIOikcs29W8068exHXlfkFviGFT/mPhREia7zSfQzHvTDkL2s+tWizvELjLHiKRO4KGTkkJyR3oeR8A5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "globby": "13.2.2", "markdownlint": "0.30.0", @@ -16708,7 +16696,6 @@ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", "license": "MIT", - "peer": true, "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", @@ -16724,7 +16711,6 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", - "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16736,8 +16722,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/optionator": { "version": "0.9.4", @@ -16810,8 +16795,7 @@ "node_modules/papaparse": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==", - "peer": true + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -17105,6 +17089,7 @@ "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.7.2.tgz", "integrity": "sha512-/X/1fmrnsFzC+JWipV8xKW9Zg5NOrem3c6NkD110ij8jTlmeuQFkIZnrjpvqBcBRvk6r/BRRBJQ+6Aoo3HGPiQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/react-query": "^5.21.7", "clsx": "^2.1.1", @@ -17821,6 +17806,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -17836,7 +17822,8 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -17853,6 +17840,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz", "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==", "license": "MIT", + "peer": true, "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", @@ -17877,6 +17865,7 @@ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz", "integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==", "license": "MIT", + "peer": true, "dependencies": { "react-router": "7.5.0" }, @@ -17951,7 +17940,6 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -18203,8 +18191,7 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -18441,8 +18428,7 @@ "node_modules/sortablejs": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.3.tgz", - "integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==", - "peer": true + "integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==" }, "node_modules/source-map": { "version": "0.5.7", @@ -18651,8 +18637,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/string-width": { "version": "4.2.3", @@ -19237,6 +19222,7 @@ "integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", diff --git a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index 5052d7b..662ab87 100644 --- a/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/main/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -1,5 +1,5 @@ import { Component } from "react"; -import { Datagrid, List, Title, Loading, useListContext, useResourceDefinition } from "react-admin"; +import { Datagrid, List, Title, Loading, useListContext, useResourceDefinition, downloadCSV } from "react-admin"; import ActionBar from "../../ActionBar/ActionBar"; import GenericField from "../../../representationProvider/GenericField"; import TableHeader from "./TableHeader/TableHeader"; @@ -11,7 +11,7 @@ import CustomQueryEditButton from "../../CustomQueryEditor/customQueryEditButton import IconProvider from "../../../IconProvider/IconProvider"; import configManager from "../../../configManager/configManager"; import CustomConversionButton from "../../CustomQueryEditor/customConversionButton"; - +import jsonExport from 'jsonexport/dist'; // LOG let queryResultListCounter = 0; // LOG let myDatagridCounter = 0; @@ -47,6 +47,7 @@ function QueryResultList(props) { } { ); } +// https://marmelab.com/react-admin/doc/5.7/List.html#exporter +const exporter = (rows, _, __, resource) => { + const entriesForExport = rows.map(row => strip(row)); + jsonExport(entriesForExport, (err, csv) => { + downloadCSV(csv, resource); // download as '.csv` file + }); +}; + +function strip(obj, level = 0) { + if (obj && typeof obj === 'object') { + const result = {}; + for (const [k, v] of Object.entries(obj)) { + if (level === 0 && k === 'id') { + // skip id at top level + } else if (level === 1 && k === 'datatype' && v && typeof v === 'object') { + // remove x.datatype.termType; promote x.datatype.value to x.datatype + const { termType, ...rest } = v; + result[k] = strip(rest, level + 1); + } else { + result[k] = strip(v, level + 1); + } + } + return result; + } + return obj; +} + export default QueryResultList; diff --git a/main/src/dataProvider/SparqlDataProvider.js b/main/src/dataProvider/SparqlDataProvider.js index 00e54ff..dedbed4 100644 --- a/main/src/dataProvider/SparqlDataProvider.js +++ b/main/src/dataProvider/SparqlDataProvider.js @@ -146,6 +146,11 @@ async function buildQueryText(query) { } query.rawText = rawText; + + if (rawText.includes("?id ") || rawText.includes("$id ")) { + throw new Error('Variable "id" is not allowed in Miravi queries. Please rename this variable.'); + } + const parser = new Parser(); const parsedQuery = parser.parse(rawText); if (!query.variableOntology) { @@ -235,6 +240,9 @@ async function executeQuery(query) { const callbackBindings = (bindings) => { const newResult = {}; for (const variable of variables) { + if (variable === "id") { + throw new Error('Variable name "id" is reserved - please rename this variable in the query'); + } const term = bindings.get(variable); newResult[variable] = term; } diff --git a/test/cypress.config.js b/test/cypress.config.js index 9b7d35a..ceb4b09 100644 --- a/test/cypress.config.js +++ b/test/cypress.config.js @@ -1,5 +1,7 @@ import { defineConfig } from "cypress"; import cypressLogToOutput from 'cypress-log-to-output'; +import fs from 'fs-extra'; +import path from 'path'; export default defineConfig({ e2e: { @@ -11,6 +13,48 @@ export default defineConfig({ setupNodeEvents(on, config) { // uncomment next line only temporarily to see app's console.log in cypress test output (https://www.npmjs.com/package/cypress-log-to-output) // cypressLogToOutput.install(on); + + on('task', { + clearFolder(folder) { + fs.emptyDirSync(folder); + return null; + } + }); + + on('task', { + findYoungestFileWithExtension({ folder, extension, timeout = 5000, interval = 200 }) { + const endTime = Date.now() + timeout; + + function checkForFile() { + const files = fs.readdirSync(folder) + .filter(f => f.toLowerCase().endsWith(extension.toLowerCase())); + + if (files.length) { + // sort newest first + const newest = files + .map(name => ({ + name, + time: fs.statSync(path.join(folder, name)).mtime.getTime() + })) + .sort((a, b) => b.time - a.time)[0]; + + return newest.name; + } + + // retry until timeout + if (Date.now() < endTime) { + return new Promise(resolve => + setTimeout(() => resolve(checkForFile()), interval) + ); + } + + return null; // nothing found even after retrying + } + + return checkForFile(); + } + }); + return config; }, }, diff --git a/test/cypress/e2e/export.cy.js b/test/cypress/e2e/export.cy.js new file mode 100644 index 0000000..7b0cc73 --- /dev/null +++ b/test/cypress/e2e/export.cy.js @@ -0,0 +1,30 @@ +describe("Export", () => { + it("Export of a list containing a Literal and a NamedNode should return the expected results", () => { + const downloadsFolder = 'cypress/downloads'; + cy.task('clearFolder', downloadsFolder); + cy.visit("/"); + cy.contains("Example queries").click(); + cy.contains("Some images").click(); + cy.contains("Finished in:"); + cy.get('.action-box').contains('Export').click(); + + cy.task('findYoungestFileWithExtension', { folder: downloadsFolder, extension: '.csv' }).then((filename) => { + cy.assertCsvRow(`${downloadsFolder}/${filename}`, { + 'name.termType': 'Literal', + 'name.value': 'Felix', + 'name.language': '', + 'name.datatype.value': 'http://www.w3.org/2001/XMLSchema#string', + 'image_img.termType': 'NamedNode', + 'image_img.value': 'http://localhost:8080/example/images/felix.jfif', + }); + cy.assertCsvRow(`${downloadsFolder}/${filename}`, { + 'name.termType': 'Literal', + 'name.value': 'Garfield', + 'name.language': '', + 'name.datatype.value': 'http://www.w3.org/2001/XMLSchema#string', + 'image_img.termType': 'NamedNode', + 'image_img.value': 'http://localhost:8080/example/images/cat.jfif', + }); + }); + }); +}); diff --git a/test/cypress/e2e/reserved-variable-names.cy.js b/test/cypress/e2e/reserved-variable-names.cy.js new file mode 100644 index 0000000..511b846 --- /dev/null +++ b/test/cypress/e2e/reserved-variable-names.cy.js @@ -0,0 +1,9 @@ +describe("Reserved variable names", () => { + it("Must report an error if a reserved variable name is used", () => { + cy.visit("/"); + cy.contains("For testing only").click(); + cy.contains("Reserved variable name test").click(); + cy.contains("Something went wrong..."); + cy.contains('Variable "id" is not allowed in Miravi queries. Please rename this variable.'); + }); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 7c14d35..a539309 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -1,3 +1,5 @@ +import Papa from 'papaparse'; + Cypress.Commands.add('checkCodeMirrorValue', (parentElementSelector, expected) => { cy.get(`${parentElementSelector} .CodeMirror`).then((editor) => { const cm = editor[0].CodeMirror; @@ -12,3 +14,36 @@ Cypress.Commands.add('setCodeMirrorValue', (parentElementSelector, value) => { cm.focus(); }); }); + +Cypress.Commands.add('assertCsvRow', (filePath, expected) => { + cy.readFile(filePath).then((content) => { + const { data: rows, meta } = Papa.parse(content, { + header: true, + skipEmptyLines: true, + }); + + const csvColumns = meta.fields || []; + const expectedColumns = Object.keys(expected); + + // required columns must exist + const missingColumns = expectedColumns.filter(col => !csvColumns.includes(col)); + + if (missingColumns.length > 0) { + throw new Error( + `CSV schema validation failed.\n` + + `Missing column(s): ${missingColumns.join(', ')}\n` + + `Available columns: ${csvColumns.join(', ')}` + ); + } + + // check that a matching row exists + const row = rows.find(r => + Object.entries(expected).every(([key, val]) => r[key] === val) + ); + + expect( + row, + `Expected CSV to contain a row matching ${JSON.stringify(expected)}` + ).to.exist; + }); +}); diff --git a/test/package-lock.json b/test/package-lock.json index 6279d72..a933471 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -17,6 +17,7 @@ "cypress-log-to-output": "^1.1.2", "express": "^4.18.2", "http-server": "^14.1.1", + "papaparse": "^5.5.3", "rimraf": "^5.0.1" } }, @@ -4476,6 +4477,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -6534,6 +6536,12 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/test/package.json b/test/package.json index f7566e1..d3e2844 100644 --- a/test/package.json +++ b/test/package.json @@ -26,6 +26,7 @@ "cypress-log-to-output": "^1.1.2", "express": "^4.18.2", "http-server": "^14.1.1", + "papaparse": "^5.5.3", "rimraf": "^5.0.1" } } From 9acdd9c4022d0014ceb4cb97f39268213918d6b8 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Thu, 30 Oct 2025 06:42:23 +0100 Subject: [PATCH 2/4] CHANGELOG --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df3acb..b539d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Export cleaned so that it doesn't contain irrelevant columns (#242). +- Documented and tested SPARQL query restrictions (#243). + ## [2.1.1] - 2025-10-14 ### Changed - Verifiable credentials use VC prototyping library now (#238). -- Export cleaned so that it doesn't contain irrelevant columns (#242). ### Added -- Documented and tested SPARQL query restrictions (#243). - ## [2.1.0] - 2025-09-25 ### Added From 16a18e68996b5949d311195cce264cadbe3ae6f6 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Thu, 30 Oct 2025 06:44:54 +0100 Subject: [PATCH 3/4] CHANGELOG again --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b539d33..84949cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Verifiable credentials use VC prototyping library now (#238). -### Added - ## [2.1.0] - 2025-09-25 ### Added From 56633edd84703f84fe2ec788f58df63d7629f7e5 Mon Sep 17 00:00:00 2001 From: Martin Vanbrabant Date: Thu, 30 Oct 2025 07:03:35 +0100 Subject: [PATCH 4/4] deleted useless test; improved test --- main/src/dataProvider/SparqlDataProvider.js | 3 --- test/cypress/e2e/export.cy.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/main/src/dataProvider/SparqlDataProvider.js b/main/src/dataProvider/SparqlDataProvider.js index dedbed4..03936f4 100644 --- a/main/src/dataProvider/SparqlDataProvider.js +++ b/main/src/dataProvider/SparqlDataProvider.js @@ -240,9 +240,6 @@ async function executeQuery(query) { const callbackBindings = (bindings) => { const newResult = {}; for (const variable of variables) { - if (variable === "id") { - throw new Error('Variable name "id" is reserved - please rename this variable in the query'); - } const term = bindings.get(variable); newResult[variable] = term; } diff --git a/test/cypress/e2e/export.cy.js b/test/cypress/e2e/export.cy.js index 7b0cc73..bd16bb8 100644 --- a/test/cypress/e2e/export.cy.js +++ b/test/cypress/e2e/export.cy.js @@ -1,6 +1,6 @@ describe("Export", () => { it("Export of a list containing a Literal and a NamedNode should return the expected results", () => { - const downloadsFolder = 'cypress/downloads'; + const downloadsFolder = Cypress.config('downloadsFolder'); cy.task('clearFolder', downloadsFolder); cy.visit("/"); cy.contains("Example queries").click();