Skip to content

Commit

Permalink
Readme updates. Config page tweaks.
Browse files Browse the repository at this point in the history
  • Loading branch information
recalcitrantsupplant committed Oct 4, 2024
1 parent b0a5010 commit c2848bf
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 124 deletions.
41 changes: 41 additions & 0 deletions README-Custom-Endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Custom Data Endpoint Configuration

Prez allows the configuration of custom endpoints.

That is, you can specify the route structure e.g.

`/catalogs/{catalogId}/products/{productId}`

And then specify which classes these endpoints deliver, and what the RDF relations between those classes are.

## Set up instructions
To set up custom endpoints:
1. Set the CONFIGURATION_MODE environment variable to "true"
2. Go to the `/configure-endpoints` page and complete the form; submit
3. Set the CUSTOM_ENDPOINTS environment variable to "true"
4. Restart Prez. You should see the dynamic endpoints being created in the logs.
5. Confirm your endpoints are working as expected.

Once working as expected:
1. Copy the "custom_endpoints.ttl" file from `prez/reference_data/data_endpoints_custom` to the remote backend (e.g. triplestore)
> Prez will preferentially use the custom endpoints specified in the triplestore over the ones specified in the local file.
2. Set the CONFIGURATION_MODE environment variable to "false"

## Limitations
The following limitations apply at present:
- The endpoint structure must be specified in the config to match what is input through the form.
related to this:
- Only one route can be specified (though multiple class hierarchies which use that one route can be specified)
i.e. you can specify
`/catalogs/{catalogId}/products/{productId}`
but not
`/catalogs/{catalogId}/products/{productId}`
and
`/datasets/{datasetId}/items/{itemsId}
on the one prez instance.
- This limitation is only due to link generation, which looks up the (currently) single endpoint structure variable in the config file.
- This should be resolvable with a small amount of work. At link generation time, an endpoint nodeshape is in context, and endpoint nodeshapes are mapped to a route structure.

- The number of hierarchy levels within a route must be two or three (i.e. 2 or 3 levels of classes = 4-6 listing/object endpoints)
- The lower limit of two is because prez uses the relationships between classes to identify which objects to list. A single level of hierarchy has no reference to another level. A small amount of dev work would resolve this. The endpoint nodeshapes can be specified in the case of N=1 to not look for relationships to other classes.
- The higher limit of three is because the SHACL parsing is not completely recursive. It could be manually extended to N levels, however it would be better to write a general SHACL parsing library.
1 change: 0 additions & 1 deletion README-OGC-Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ ex:BDRScientificNameQueryableShape
.
```
It is recommended that templated SPARQL queries are used to periodically update the `sh:in` values, which correspond to enumerations.
# TODO other SHACL predicates can be reused to specify min/max values, etc. where the range is numeric and enumerations are not appropriate.

When Prez starts, it will query the remote repository (typically a triplestore) for all Queryables.
It queries for them using a CONSTRUCT query, serializes this as JSON-LD, and does a minimal transformation to produce the OGC Features compliant response.
Expand Down
257 changes: 134 additions & 123 deletions prez/static/endpoint_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ <h2>Instructions:</h2>
</div>
</div>

<div class="field">
<label class="label">Routes:</label>
<div class="control">
<button class="button is-primary add-route" type="button">Add Route</button>
</div>
</div>

<div class="route-container" id="route-container">
</div>

Expand All @@ -54,11 +47,12 @@ <h2>Instructions:</h2>
const routeContainer = document.querySelector('div#route-container');
let routeCount = 0;

function addRemoveButton(element, container, removeFunction) {
function addRemoveButton(element, container, removeFunction, isDisabled = false) {
const removeBtn = document.createElement('button');
removeBtn.className = 'button is-danger is-small';
removeBtn.textContent = 'Remove';
removeBtn.style.marginLeft = '10px';
removeBtn.disabled = isDisabled;
removeBtn.addEventListener('click', (e) => {
e.preventDefault();
removeFunction(element, container);
Expand All @@ -70,150 +64,164 @@ <h2>Instructions:</h2>
container.removeChild(element);
}

document.querySelector('button.add-route').addEventListener('click', (e) => {
e.preventDefault();
const newRoute = document.createElement('div');
newRoute.className = 'box';
newRoute.innerHTML = `
<h3 class="title is-4">Route ${routeCount + 1}</h3>
function addRoute() {
if (routeCount === 0) {
const newRoute = document.createElement('div');
newRoute.className = 'box';
newRoute.innerHTML = `
<h3 class="title is-4">Route ${routeCount + 1}</h3>
<div class="field">
<label class="label">Name:</label>
<div class="control">
<input class="input" type="text" name="name">
</div>
</div>
<div class="field">
<label class="label">Full API Path:</label>
<div class="control">
<input class="input" type="text" name="fullApiPath">
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-info add-hierarchy-relation" type="button">Add Hierarchy Relation</button>
</div>
</div>
<div class="hierarchy-relations-container" id="hierarchy-relations-container-${routeCount}"></div>
`;
routeContainer.appendChild(newRoute);
addRemoveButton(newRoute, routeContainer, removeElement, true); // Disable the remove button
routeCount++;

const hierarchyRelationBtn = newRoute.querySelector('button.add-hierarchy-relation');
hierarchyRelationBtn.addEventListener('click', addHierarchyRelation);

// Automatically add the first hierarchy relation
addHierarchyRelation({ target: hierarchyRelationBtn });
}
}

function addHierarchyRelation(event) {
const newHierarchyRelation = document.createElement('div');
newHierarchyRelation.className = 'box';
newHierarchyRelation.innerHTML = `
<h4 class="title is-5">Class Hierarchy</h4>
<div class="field">
<label class="label">Name:</label>
<div class="control">
<input class="input" type="text" name="name">
</div>
</div>
<div class="field">
<label class="label">Full API Path:</label>
<div class="control">
<input class="input" type="text" name="fullApiPath">
<button class="button is-info add-class" type="button">Add Class</button>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-info add-hierarchy-relation" type="button">Add Hierarchy Relation</button>
</div>
<div class="columns hierarchy-relation-container">
<div class="column class-container" id="class-container-${routeCount}"></div>
<div class="column relation-container" id="relation-container-${routeCount}"></div>
</div>
<div class="hierarchy-relations-container" id="hierarchy-relations-container-${routeCount}"></div>
`;
routeContainer.appendChild(newRoute);
addRemoveButton(newRoute, routeContainer, removeElement);
routeCount++;
const hierarchyRelationsContainer = event.target.closest('.box').querySelector('.hierarchy-relations-container');
hierarchyRelationsContainer.appendChild(newHierarchyRelation);
addRemoveButton(newHierarchyRelation, hierarchyRelationsContainer, removeElement);

const hierarchyRelationBtn = newRoute.querySelector('button.add-hierarchy-relation');
hierarchyRelationBtn.addEventListener('click', (e) => {
e.preventDefault();
const newHierarchyRelation = document.createElement('div');
newHierarchyRelation.className = 'box';
newHierarchyRelation.innerHTML = `
<h4 class="title is-5">Add Class Hierarchy</h4>
const classContainer = newHierarchyRelation.querySelector('.class-container');
const relationContainer = newHierarchyRelation.querySelector('.relation-container');
let classCount = 0;

const classBtn = newHierarchyRelation.querySelector('button.add-class');
classBtn.addEventListener('click', () => addClass(classContainer, relationContainer, classBtn));

// Automatically add two classes and one relation
addClass(classContainer, relationContainer, classBtn);
addClass(classContainer, relationContainer, classBtn);
}

function addClass(classContainer, relationContainer, classBtn) {
const classCount = classContainer.children.length;
if (classCount < 3) {
const newClass = document.createElement('div');
newClass.className = 'box class-item';
newClass.innerHTML = `
<h5 class="title is-6">Class at Hierarchy Level <span class="tag is-primary">${classCount + 1}</span></h5>
<div class="field">
<label class="label">Name:</label>
<label class="label">RDF Class:</label>
<div class="control">
<input class="input" type="text" name="name">
<input class="input" type="text" name="rdfClass">
</div>
</div>
<div class="field">
<label class="label">Class Name:</label>
<div class="control">
<button class="button is-info add-class" type="button">Add Class</button>
<input class="input" type="text" name="className">
</div>
</div>
<div class="columns hierarchy-relation-container">
<div class="column class-container" id="class-container-${routeCount}"></div>
<div class="column relation-container" id="relation-container-${routeCount}"></div>
</div>
`;
const hierarchyRelationsContainer = newRoute.querySelector(`div#hierarchy-relations-container-${routeCount - 1}`);
hierarchyRelationsContainer.appendChild(newHierarchyRelation);
addRemoveButton(newHierarchyRelation, hierarchyRelationsContainer, removeElement);

const classContainer = newHierarchyRelation.querySelector(`div#class-container-${routeCount}`);
const relationContainer = newHierarchyRelation.querySelector(`div#relation-container-${routeCount}`);
let classCount = 0;

const classBtn = newHierarchyRelation.querySelector('button.add-class');
classBtn.addEventListener('click', (e) => {
e.preventDefault();
if (classCount < 3) {
classCount++;
const newClass = document.createElement('div');
newClass.className = 'box class-item';
newClass.innerHTML = `
<h5 class="title is-6">Class at Hierarchy Level <span class="tag is-primary">${classCount}</span></h5>
<div class="field">
<label class="label">RDF Class:</label>
<div class="control">
<input class="input" type="text" name="rdfClass">
</div>
</div>
<div class="field">
<label class="label">Class Name:</label>
<div class="control">
<input class="input" type="text" name="className">
</div>
</div>
`;
classContainer.appendChild(newClass);
addRemoveButton(newClass, classContainer, (element, container) => {
removeElement(element, container);
classCount--;
classBtn.disabled = false;
updateClassLevels(classContainer);
updateRelations(relationContainer, classCount);
});

if (classCount > 1) {
const newRelation = document.createElement('div');
newRelation.className = 'box relation-item';
newRelation.innerHTML = `
<h5 class="title is-6">Relation between Classes at Hierarchy Levels ${classCount - 1} and ${classCount}</h5>
<div class="field">
<label class="label">Direction:</label>
<div class="control">
<div class="select">
<select name="direction">
<option value="outbound">Outbound</option>
<option value="inbound">Inbound</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">Predicate:</label>
<div class="control">
<input class="input" type="text" name="rdfPredicate">
</div>
</div>
`;
relationContainer.appendChild(newRelation);
addRemoveButton(newRelation, relationContainer, removeElement);
}

if (classCount === 3) {
classBtn.disabled = true;
}
classContainer.appendChild(newClass);
addRemoveButton(newClass, classContainer, (element, container) => {
if (container.children.length > 2) {
removeElement(element, container);
updateClassLevels(classContainer);
updateRelations(relationContainer, classContainer.children.length);
classBtn.disabled = false;
}
});

function updateClassLevels(container) {
const classes = container.querySelectorAll('.class-item');
classes.forEach((classItem, index) => {
const levelTag = classItem.querySelector('.tag');
levelTag.textContent = index + 1;
});
if (classCount > 0) {
addRelation(relationContainer, classCount, classCount + 1);
}

function updateRelations(container, classCount) {
const relations = container.querySelectorAll('.relation-item');
relations.forEach((relationItem, index) => {
const title = relationItem.querySelector('h5');
title.textContent = `Relation between Classes at Hierarchy Levels ${index + 1} and ${index + 2}`;
});
while (relations.length > classCount - 1) {
container.removeChild(relations[relations.length - 1]);
}
if (classCount === 2) {
classBtn.disabled = true;
}
}
}

function addRelation(relationContainer, levelFrom, levelTo) {
const newRelation = document.createElement('div');
newRelation.className = 'box relation-item';
newRelation.innerHTML = `
<h5 class="title is-6">Relation between Classes at Hierarchy Levels ${levelFrom} and ${levelTo}</h5>
<div class="field">
<label class="label">Direction:</label>
<div class="control">
<div class="select">
<select name="direction">
<option value="outbound">Outbound</option>
<option value="inbound">Inbound</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">Predicate:</label>
<div class="control">
<input class="input" type="text" name="rdfPredicate">
</div>
</div>
`;
relationContainer.appendChild(newRelation);
}

function updateClassLevels(container) {
const classes = container.querySelectorAll('.class-item');
classes.forEach((classItem, index) => {
const levelTag = classItem.querySelector('.tag');
levelTag.textContent = index + 1;
});
});
}

function updateRelations(container, classCount) {
const relations = container.querySelectorAll('.relation-item');
relations.forEach((relationItem, index) => {
const title = relationItem.querySelector('h5');
title.textContent = `Relation between Classes at Hierarchy Levels ${index + 1} and ${index + 2}`;
});
while (relations.length > classCount - 1) {
container.removeChild(relations[relations.length - 1]);
}
}

document.getElementById('configForm').addEventListener('submit', function(e) {
e.preventDefault();
Expand Down Expand Up @@ -283,6 +291,9 @@ <h5 class="title is-6">Relation between Classes at Hierarchy Levels ${classCount
alert('An error occurred while submitting the configuration. Please try again.');
});
});

// Automatically add the first route when the page loads
window.addEventListener('load', addRoute);
</script>
</body>
</html>

0 comments on commit c2848bf

Please sign in to comment.