diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd1a612..0732129d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the broken production version due to a missing package (#123). - Corrected the counting of the total number of items in the result list (#120). +- Avoided extending query objects in the configuration (#126). ## [1.2.0] - 2024-05-07 diff --git a/src/App.jsx b/src/App.jsx index 384d1f48..0f2540be 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -64,9 +64,7 @@ function App() { authProvider={authenticationProvider} loginPage={SolidLoginForm} requireAuth={false} - dashboard={() => { - return Dashboard({ title: config.title, text: config.introductionText }) - }} + dashboard={Dashboard} > {config.queries.map((query) => { return ( diff --git a/src/components/ActionBar/ActionBar.jsx b/src/components/ActionBar/ActionBar.jsx index a37d803f..b27613e8 100644 --- a/src/components/ActionBar/ActionBar.jsx +++ b/src/components/ActionBar/ActionBar.jsx @@ -20,7 +20,6 @@ import SourceFetchStatusIcon from "./SourceFetchStatusIcon/SourceFetchStatusIcon import SourceVerificationIcon from "./SourceVerificationIcon/SourceVerificationIcon.jsx"; import configManager from "../../configManager/configManager.js"; -const config = configManager.getConfig(); /** * @@ -31,10 +30,6 @@ function ActionBar() { const [time, setTime] = useState(0); const [sourceInfoOpen, setSourceInfoOpen] = useState(false); - const context = config.queries.filter((query) => query.id === resource)[0] - .comunicaContext; - - const sources = context.sources; useEffect(() => { if (isLoading) { setTime(0); @@ -49,6 +44,10 @@ function ActionBar() { return () => clearInterval(intervalId); }, [time, isLoading]); + const config = configManager.getConfig(); + const query = configManager.getQueryWorkingCopyById(resource); + const context = query.comunicaContext; + const sources = context.sources; const resultCount = total <= perPage ? total : perPage; return ( diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 1508a031..d470216a 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -1,31 +1,25 @@ import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import {Title} from 'react-admin'; -import PropTypes from 'prop-types'; import './Dashboard.css'; +import configManager from '../../configManager/configManager'; + /** * This function returns a function that creates a dashboard with an introduction for . - * @param {object} props - This has the properties `title` (text for the app bar) and `text` (the introduction text). * @returns {function(): *} - A function that creates a dashboard with an introduction for . */ -function Dashboard(props) { - let {title, text} = props; - - title = title || 'You change this title via the config file.'; - text = text || 'You change this text via the config file.'; +function Dashboard() { + const config = configManager.getConfig(); + const title = config.title || 'You change this title via the config file.'; + const introductionText = config.introductionText || 'You change this introduction text via the config file.'; return ( - <CardContent>{text}</CardContent> + <CardContent>{introductionText}</CardContent> </Card> ); } -Dashboard.propTypes = { - title: PropTypes.string, - text: PropTypes.string -}; - export default Dashboard; diff --git a/src/components/InteractionLayout/NavigationBar/NavigationBar.jsx b/src/components/InteractionLayout/NavigationBar/NavigationBar.jsx index 70e14b75..95423bed 100644 --- a/src/components/InteractionLayout/NavigationBar/NavigationBar.jsx +++ b/src/components/InteractionLayout/NavigationBar/NavigationBar.jsx @@ -8,7 +8,6 @@ import { IconButton } from '@mui/material'; import { Tooltip } from '@mui/material'; import configManager from "../../../configManager/configManager"; -const config = configManager.getConfig(); function InvalidateButton() { const refresh = useRefresh(); @@ -31,6 +30,7 @@ function InvalidateButton() { * @returns {Component} custom AppBar as defined by react-admin */ function NavigationBar(props) { + const config = configManager.getConfig(); return ( <AppBar {...props} userMenu={<AuthenticationMenu />}> <img diff --git a/src/components/InteractionLayout/SelectionMenu/SelectionMenu.jsx b/src/components/InteractionLayout/SelectionMenu/SelectionMenu.jsx index 6ff47e03..e75e31ae 100644 --- a/src/components/InteractionLayout/SelectionMenu/SelectionMenu.jsx +++ b/src/components/InteractionLayout/SelectionMenu/SelectionMenu.jsx @@ -14,16 +14,15 @@ import IconProvider from "../../../IconProvider/IconProvider"; import ListAltIcon from '@mui/icons-material/ListAlt'; import configManager from '../../../configManager/configManager'; -const config = configManager.getConfig(); /** * A custom menu as defined in React Admin for selecting the query the user whishes to execute. * @returns {Component} the selection menu component */ function SelectionMenu() { - - const resources = useResourceDefinitions(); + const config = configManager.getConfig(); const queryGroups = config.queryGroups || []; + const resources = useResourceDefinitions(); // adding a list to the group that will contain all the queries for said group queryGroups.forEach(group => group.queries = []) diff --git a/src/components/ListResultTable/ListResultTable.jsx b/src/components/ListResultTable/ListResultTable.jsx index 1fc5e0e1..8b20f056 100644 --- a/src/components/ListResultTable/ListResultTable.jsx +++ b/src/components/ListResultTable/ListResultTable.jsx @@ -3,6 +3,8 @@ import PropTypes from "prop-types"; import {Component} from "react"; import QueryResultList from "./QueryResultList/QueryResultList"; +import configManager from "../../configManager/configManager"; + /** * @param {object} props - the props passed down to the component * @returns {Component} custom List as defined by react-admin which either shows a loading indicator or the query results @@ -18,8 +20,6 @@ function ListResultTable(props) { resource, sort, variables, - changeVariables, - submitted, ...rest } = props; @@ -31,6 +31,8 @@ function ListResultTable(props) { } }); + const query = configManager.getQueryWorkingCopyById(resource); + return ( <ListBase debounce={debounce} @@ -45,7 +47,7 @@ function ListResultTable(props) { sort={sort} > {isLoading && <Loading loadingSecondary={"The page is loading. Just a moment please."} />} - {!isLoading && <QueryResultList {...rest} changeVariables={changeVariables} submitted={submitted} />} + {!isLoading && <QueryResultList resource={resource} { ...rest } />} </ListBase> ); } @@ -59,9 +61,9 @@ ListResultTable.propTypes = { filterDefaultValues: PropTypes.object, perPage: PropTypes.number, queryOptions: PropTypes.object, - resource: PropTypes.string, + resource: PropTypes.string.isRequired, sort: PropTypes.object, - variables: PropTypes.object, + variables: PropTypes.object.isRequired }; export default ListResultTable; diff --git a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index f1d60646..7f9c566f 100644 --- a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -7,18 +7,18 @@ import TableHeader from "./TableHeader/TableHeader"; import Button from '@mui/material/Button'; import SearchOffIcon from '@mui/icons-material/SearchOff'; import { SvgIcon, Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; import configManager from "../../../configManager/configManager"; -const config = configManager.getConfig(); /** * @param {object} props - the props passed down to the component * @returns {Component} custom ListViewer as defined by react-admin containing the results of the query with each variable its generic field. */ function QueryResultList(props) { - const QueryTitle = useResourceDefinition().options.label; + const queryTitle = useResourceDefinition().options.label; const { data } = useListContext(props); - const {changeVariables, submitted} = props; + const { resource, changeVariables, submitted} = props; const [values, setValues] = useState(undefined); useEffect(() => { if (data && data.length > 0) { @@ -28,15 +28,18 @@ function QueryResultList(props) { } }, [data]); + const config = configManager.getConfig(); + const query = configManager.getQueryWorkingCopyById(resource); + return ( <div style={{ paddingLeft: '20px' , paddingRight: '10px' }}> <Title title={config.title} /> {submitted && <Aside changeVariables={changeVariables}/> /* Adding button to make a new query - top left corner */ } - <Typography fontSize={"2rem"} mt={2} > {QueryTitle} </Typography> + <Typography fontSize={"2rem"} mt={2} > {queryTitle} </Typography> {values ?( - <ListView title=" " actions={<ActionBar />} {...props} > - <Datagrid header={<TableHeader config={config}/>} bulkActionButtons={false}> + <ListView title=" " actions={<ActionBar />} {...props} > + <Datagrid header={<TableHeader query={query}/>} bulkActionButtons={false}> {Object.keys(values).map((key) => { return ( <GenericField @@ -49,12 +52,18 @@ function QueryResultList(props) { </Datagrid> </ListView> ): - <NoValuesDiplay/> + <NoValuesDisplay/> } </div> ); } +QueryResultList.propTypes = { + resource: PropTypes.string.isRequired, + changeVariables: PropTypes.func.isRequired, + submitted: PropTypes.bool.isRequired +}; + /** * * @param {Array<Term>} data - a list of data objects @@ -81,7 +90,7 @@ const Aside = (props) => { </div> )} -const NoValuesDiplay = () => { +const NoValuesDisplay = () => { return( <div> <Box display="flex" alignItems="center" sx={{m:3}}> diff --git a/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx index 05e0a138..aff92d7b 100644 --- a/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx +++ b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx @@ -8,18 +8,16 @@ import LinkIcon from "@mui/icons-material/Link"; import PropTypes from "prop-types"; import { Component } from "react"; +import configManager from "../../../../configManager/configManager"; + /** * * @param {object} props - the props passed down to the component * @param {Array<Component>} props.children - the children of the component - * @param {object} props.config - the config object of the application * @returns {Component} the header of the table containing the column names, the sort icons and ontology links */ -function TableHeader({ children, config }) { +function TableHeader({ children }) { const { sort, setSort, resource } = useListContext(); - const { variableOntology } = config.queries.filter( - (query) => query.id === resource - )[0]; /** * Handles the click on a header and sets the sort state accordingly @@ -37,6 +35,9 @@ function TableHeader({ children, config }) { setSort(newSort); } + const query = configManager.getQueryWorkingCopyById(resource); + const variableOntology = query.variableOntology; + return ( <TableHead> <TableRow> @@ -90,8 +91,6 @@ function TableHeader({ children, config }) { } TableHeader.propTypes = { - children: PropTypes.node, - config: PropTypes.object.isRequired, - + children: PropTypes.node } export default TableHeader; diff --git a/src/components/ListResultTable/TemplatedListResultTable.jsx b/src/components/ListResultTable/TemplatedListResultTable.jsx index f621564d..171b8303 100644 --- a/src/components/ListResultTable/TemplatedListResultTable.jsx +++ b/src/components/ListResultTable/TemplatedListResultTable.jsx @@ -6,7 +6,6 @@ import TemplatedQueryForm from "./TemplatedQueryForm.jsx"; import ListResultTable from "./ListResultTable.jsx"; import configManager from '../../configManager/configManager.js'; -const config = configManager.getConfig(); /** * A wrapper component around ListResultTable, to support templated queries @@ -22,15 +21,10 @@ const TemplatedListResultTable = (props) => { const [submitted, setSubmitted] = useState(false); const [searchPar, setSearchPar] = useState({}); - const query = config.queries.filter( - (query) => query.id === resource - )[0]; - + const query = configManager.getQueryWorkingCopyById(resource); const isTemplatedQuery = query.variables !== undefined; let tableEnabled = !isTemplatedQuery; - - if (isTemplatedQuery) { // Update variables from query parameters const queryParams = new URLSearchParams(location.search); @@ -83,7 +77,7 @@ const TemplatedListResultTable = (props) => { searchPar={searchPar} /> } - {tableEnabled && <ListResultTable {...props} variables={variables} changeVariables={changeVariables} submitted={submitted}/>} + {tableEnabled && <ListResultTable {...props} resource={resource} variables={variables} changeVariables={changeVariables} submitted={submitted}/>} </> ) } diff --git a/src/components/LoginPage/LoginPage.jsx b/src/components/LoginPage/LoginPage.jsx index 58e3fbaf..66026f4f 100644 --- a/src/components/LoginPage/LoginPage.jsx +++ b/src/components/LoginPage/LoginPage.jsx @@ -1,13 +1,13 @@ import "./LoginPage.css"; import { useLogin, useNotify } from "react-admin"; import { Component, useState } from "react"; + import configManager from "../../configManager/configManager"; /** * @returns {Component} a login form for logging into your Identity Provider. */ function SolidLoginForm() { - const config = configManager.getConfig(); const login = useLogin(); const notify = useNotify(); const [isIdp, setIsIdp] = useState(true); @@ -25,6 +25,8 @@ function SolidLoginForm() { } } + const config = configManager.getConfig(); + return ( <form onSubmit={handleLogin} className="login-form"> <div className="login-form-type-selection-box"> diff --git a/src/configManager/configManager.js b/src/configManager/configManager.js index 98c622a3..ae3e29f0 100644 --- a/src/configManager/configManager.js +++ b/src/configManager/configManager.js @@ -2,7 +2,10 @@ import configFile from "../config.json"; import { EventEmitter } from 'events'; /** - * A class maintaining a configuration object, initially read from the configuration file + * A class maintaining a configuration object, initially read from the configuration file. + * In addition, working copies of query objects are maintained as well. + * These working copies are meant for usage by clients that may extend those query objects, + * but shouldn't change the configuration object. */ class ConfigManager extends EventEmitter { constructor() { @@ -16,6 +19,8 @@ class ConfigManager extends EventEmitter { if (this.config.queryFolder.substring(this.config.queryFolder.length - 1) !== "/") { this.config.queryFolder = `${this.config.queryFolder}/`; } + + this.queryWorkingCopies = {}; } /** @@ -34,6 +39,7 @@ class ConfigManager extends EventEmitter { */ setConfig(newConfig) { this.config = { ...newConfig }; + this.queryWorkingCopies = {}; this.emit('configChanged', this.config); } @@ -44,6 +50,7 @@ class ConfigManager extends EventEmitter { */ changeConfig(changes) { this.config = { ...this.config, ...changes }; + this.queryWorkingCopies = {}; this.emit('configChanged', this.config); } @@ -56,6 +63,43 @@ class ConfigManager extends EventEmitter { this.config.queries = [...this.config.queries, newQuery]; this.emit('configChanged', this.config); } + + /** + * Gets the query with the given id in the config.queries array in the configuration + * @param {string} id - id property a query + * @returns {object} the query + */ + getQueryById(id) { + return this.config.queries.find((query) => query.id === id); + } + + /** + * Gets a working copy of the query with the given id (make a working copy first if needed) + * @param {string} id - id property a query + * @returns {object} the query + */ + getQueryWorkingCopyById(id) { + let workingCopy = this.queryWorkingCopies[id]; + if (!workingCopy) { + workingCopy = JSON.parse(JSON.stringify(this.getQueryById(id))); + this.queryWorkingCopies[id] = workingCopy; + } + return workingCopy; + } + + /** + * Gets the query text from a query + * @param {object} query - the input query + * @returns {string} the query text + */ + async getQueryText(query) { + + if (query.queryLocation) { + const fetchResult = await fetch(`${this.config.queryFolder}${query.queryLocation}`); + return await fetchResult.text(); + } + return query.queryString; + } } const configManager = new ConfigManager(); diff --git a/src/dataProvider/SparqlDataProvider.js b/src/dataProvider/SparqlDataProvider.js index 2a59f951..3892f786 100644 --- a/src/dataProvider/SparqlDataProvider.js +++ b/src/dataProvider/SparqlDataProvider.js @@ -35,7 +35,10 @@ configManager.on('configChanged', onConfigChanged); export default { getList: async function getList(resource, { pagination, sort, filter, meta }) { - const query = findQueryWithId(resource); + // make a working copy of the query object found in the configuration, to prevent changing the configuration + // this copy is extended here + // rendering should occur based on this working copy + const query = configManager.getQueryWorkingCopyById(resource); const limit = pagination.perPage; const offset = (pagination.page - 1) * pagination.perPage; query.sort = sort; @@ -98,30 +101,20 @@ export default { }; /** - * Finds a query with the given id in config.queries - * @param {number} id - identifier of a query - * @returns {object} the query element from the configuration + * Fetches the query file and builds the final query text. + * @param {object} query - the query object working copy + * @returns {string} the built query text */ -function findQueryWithId(id) { - return config.queries.find((query) => query.id === id); -} - -/** - * Fetches the query file from the given query and returns its text. - * @param {object} query - the query element from the configuration - * @returns {string} the text from the file location provided by the query relative to query location defined in the config file, modified for our needs. - */ -async function fetchQuery(query) { +async function buildQueryText(query) { try { - const result = await fetch(`${config.queryFolder}${query.queryLocation}`); - const parser = new Parser(); - let rawText = await result.text(); + let rawText = await configManager.getQueryText(query); if (query.variableValues) { rawText = replaceVariables(rawText, query.variableValues); } query.rawText = rawText; + const parser = new Parser(); const parsedQuery = parser.parse(rawText); if (!query.variableOntology) { query.variableOntology = findPredicates(parsedQuery); @@ -182,13 +175,13 @@ function findPredicates(query) { } /** - * A function that executes a given query and processes every result. - * @param {object} query - the query element from the configuration + * Executes the query in scope and processes every result. + * @param {object} query - the query object working copy * @returns {Array<Term>} the results of the query */ async function executeQuery(query) { try { - query.queryText = await fetchQuery(query); + query.queryText = await buildQueryText(query); return handleQueryExecution( await myEngine.query(query.queryText, { ...generateContext(query.comunicaContext),