Introduce data field as the main property on which to get/set other properties #4

Merged
jsheunis merged 2 commits from events into main 2026-02-04 08:50:05 +00:00
9 changed files with 115 additions and 99 deletions

View file

@ -28,17 +28,17 @@ const fileUrl = 'https://concepts.datalad.org/s/things/v1.shacl.ttl';
// Listen for and act on the 'graphLoaded' event
shapesDS.addEventListener('graphLoaded', (event) => {
console.log('Shapes graph fully loaded:', event.detail)
console.log(shapesDS.propertyGroups)
console.log(shapesDS.nodeShapes)
console.log(shapesDS.nodeShapeNames)
console.log(shapesDS.nodeShapeNamesArray)
console.log(shapesDS.nodeShapeIRIs)
console.log(shapesDS.prefixes)
console.log(shapesDS.serializedGraph)
console.log(shapesDS.graphLoaded)
console.log(shapesDS.prefixesLoaded)
console.log(shapesDS.graph)
console.log(shapesDS.graph.size)
console.log(shapesDS.data.propertyGroups)
console.log(shapesDS.data.nodeShapes)
console.log(shapesDS.data.nodeShapeNames)
console.log(shapesDS.data.nodeShapeNamesArray)
console.log(shapesDS.data.nodeShapeIRIs)
console.log(shapesDS.data.prefixes)
console.log(shapesDS.data.serializedGraph)
console.log(shapesDS.data.graphLoaded)
console.log(shapesDS.data.prefixesLoaded)
console.log(shapesDS.data.graph)
console.log(shapesDS.data.graph.size)
});
// Load the RDF
shapesDS.loadRDF(fileUrl);

View file

@ -7,8 +7,8 @@ import { RDFS } from '../modules/namespaces';
export class ClassDataset extends RdfDataset {
constructor() {
super()
constructor(data = {}) {
super(data)
}
onDataFn(quad) {

View file

@ -8,7 +8,7 @@ import { isEmptyObject, toIRI} from '../modules/utils';
export class FormBase {
constructor(id_iri = null) {
constructor(id_iri = null, content = {}) {
if (!id_iri) {
var msg = "id_iri is a required argument"
@ -16,7 +16,7 @@ export class FormBase {
throw new Error(msg)
}
this.ID_IRI = id_iri
this.content = {}
this.content = content
this.ignoredProperties = [
RDF.type.value,
]
@ -230,7 +230,7 @@ export class FormBase {
var quadArray = RdfDS.getSubjectTriples(subject_term)
var IdQuadExists = false
quadArray.forEach((quad) => {
var predicate_uri = toIRI(quad.predicate.value, RdfDS.prefixes)
var predicate_uri = toIRI(quad.predicate.value, RdfDS.data.prefixes)
if (predicate_uri === this.ID_IRI) {
IdQuadExists = true
}
@ -253,7 +253,7 @@ export class FormBase {
if (this.content[class_uri]) {
// If we are in edit mode, the first step is to delete existing quads from graphData
if (editMode) {
RdfDS.graph.deleteMatches(rdf.namedNode(node_uri), null, null, null)
RdfDS.data.graph.deleteMatches(rdf.namedNode(node_uri), null, null, null)
}
// Then we generate the quads
@ -261,7 +261,7 @@ export class FormBase {
var quads = this.formNodeToQuads(class_uri, node_uri, shapesDS)
// and add them to the dataset
quads.forEach(quad => {
RdfDS.graph.add(quad)
RdfDS.addQuad(quad)
});
// Some next steps depend on the type of the record's subject
@ -283,8 +283,8 @@ export class FormBase {
var objectQuads = RdfDS.getObjectTriples(rdf.namedNode(node_uri))
objectQuads.forEach((quad) => {
let new_quad = rdf.quad(quad.subject, quad.predicate, subject)
RdfDS.graph.delete(quad)
RdfDS.graph.add(new_quad)
RdfDS.data.graph.delete(quad)
RdfDS.data.graph.add(new_quad)
});
}
// Change formdata node_uri to the actual id, if this was present:

View file

@ -7,19 +7,35 @@ import formatsPretty from '@rdfjs/formats/pretty.js'
/**
* A class wrapping an RDF dataset (quad-store) from the `rdf-ext` library.
*/
export class RdfDataset extends EventTarget {
export class RdfDataset {
/**
* Create a wrapper object for an RDF dataset a.k.a. quad-store
*/
constructor() {
super()
constructor(data = {}) {
this.data = data;
this.rdfPretty = rdf.clone();
this.rdfPretty.formats.import(formatsPretty);
this.prefixes = {};
this.serializedGraph = '';
this.graphLoaded = false;
this.prefixesLoaded = false;
this.graph = this.createDataset();
this.data.prefixes = {};
this.data.serializedGraph = '';
this.data.graphLoaded = false;
this.data.prefixesLoaded = false;
this.data.graph = this.createDataset();
this._eventTarget = new EventTarget();
}
addEventListener(type, listener, options) {
this._eventTarget.addEventListener(type, listener, options);
}
removeEventListener(type, listener, options) {
this._eventTarget.removeEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._eventTarget.dispatchEvent(event);
}
/**
@ -61,7 +77,7 @@ export class RdfDataset extends EventTarget {
* Pre-load function to reset the graph loading state.
*/
beforeLoadFn() {
this.graphLoaded = false
this.data.graphLoaded = false
}
/**
@ -70,12 +86,12 @@ export class RdfDataset extends EventTarget {
* @param {import("rdf-ext").NamedNode} ns - The namespace associated with the prefix.
*/
onPrefixFn(prefix, ns) {
this.prefixes[prefix] = ns.value;
this.data.prefixes[prefix] = ns.value;
this.dispatchEvent(new CustomEvent('prefix', { detail: { prefix, ns } }));
}
onPrefixEndFn() {
this.prefixesLoaded = true
this.dispatchEvent(new CustomEvent('prefixesLoaded', { detail: this.prefixes }));
this.data.prefixesLoaded = true
this.dispatchEvent(new CustomEvent('prefixesLoaded', { detail: this.data.prefixes }));
}
/**
@ -87,9 +103,9 @@ export class RdfDataset extends EventTarget {
this.dispatchEvent(new CustomEvent('quad', { detail: quad }));
}
async onDataEndFn() {
this.serializedGraph = await this.serializeGraph()
this.graphLoaded = true
this.dispatchEvent(new CustomEvent('graphLoaded', { detail: this.graph }));
this.data.serializedGraph = await this.serializeGraph()
this.data.graphLoaded = true
this.dispatchEvent(new CustomEvent('graphLoaded', { detail: this.data.graph }));
}
/**
@ -97,7 +113,7 @@ export class RdfDataset extends EventTarget {
* @param {import("rdf-ext").Quad} quad - The RDF quad to add.
*/
addQuad(quad) {
this.graph.add(quad)
this.data.graph.add(quad)
}
/**
@ -105,7 +121,7 @@ export class RdfDataset extends EventTarget {
* @returns {Promise<string>} The serialized RDF graph in Turtle format.
*/
async serializeGraph() {
return (await this.rdfPretty.io.dataset.toText('text/turtle', this.graph)).trim()
return (await this.rdfPretty.io.dataset.toText('text/turtle', this.data.graph)).trim()
}
/**
@ -116,7 +132,7 @@ export class RdfDataset extends EventTarget {
isRdfList(node) {
let hasFirst = false;
let hasRest = false;
this.graph.forEach((quad) => {
this.data.graph.forEach((quad) => {
if (quad.subject.equals(node)) {
if (quad.predicate.value === RDF.first.value) hasFirst = true;
if (quad.predicate.value === RDF.rest.value) hasRest = true;
@ -136,11 +152,11 @@ export class RdfDataset extends EventTarget {
while (currentNode && currentNode.value !== RDF.nil.value) {
let listItem = null;
// Get the first element in the RDF list
this.graph.forEach((quad) => {
this.data.graph.forEach((quad) => {
if (quad.subject.equals(currentNode) && quad.predicate.value === RDF.first.value) {
// Resolve blank nodes recursively, but handle literals and IRIs separately
if (quad.object.termType === "BlankNode") {
listItem = this.resolveBlankNode(quad.object, this.graph);
listItem = this.resolveBlankNode(quad.object, this.data.graph);
} else if (quad.object.termType === "Literal") {
listItem = quad.object.value; // Store literal value
} else if (quad.object.termType === "NamedNode") {
@ -153,7 +169,7 @@ export class RdfDataset extends EventTarget {
}
// Move to the next item in the list (rdf:rest)
let nextNode = null;
this.graph.forEach((quad) => {
this.data.graph.forEach((quad) => {
if (quad.subject.equals(currentNode) && quad.predicate.value === RDF.rest.value) {
nextNode = quad.object;
}
@ -165,7 +181,7 @@ export class RdfDataset extends EventTarget {
resolveBlankNode(blankNode) {
let resolvedObject = {};
this.graph.forEach((quad) => {
this.data.graph.forEach((quad) => {
if (quad.subject.equals(blankNode)) {
const predicate = quad.predicate.value;
const object = quad.object;
@ -191,11 +207,11 @@ export class RdfDataset extends EventTarget {
getLiteralAndNamedNodes(predicate, propertyClass, prefixes) {
var propClassCurie = toCURIE(propertyClass, prefixes)
// a) use the literal node with xsd data type
const literalNodes = rdf.grapoi({ dataset: this.graph })
const literalNodes = rdf.grapoi({ dataset: this.data.graph })
.hasOut(predicate, rdf.literal(String(propClassCurie), XSD.anyURI))
.quads();
// b) and the named node
const uriNodes = rdf.grapoi({ dataset: this.graph })
const uriNodes = rdf.grapoi({ dataset: this.data.graph })
.hasOut(predicate, rdf.namedNode(propertyClass))
.quads();
// return as a concatenated array of quads
@ -203,12 +219,12 @@ export class RdfDataset extends EventTarget {
}
getSubjectTriples(someTerm) {
const quads = rdf.grapoi({ dataset: this.graph, term: someTerm }).out().quads();
const quads = rdf.grapoi({ dataset: this.data.graph, term: someTerm }).out().quads();
return Array.from(quads)
}
getObjectTriples(someTerm) {
const quads = rdf.grapoi({ dataset: this.graph, term: someTerm }).in().quads();
const quads = rdf.grapoi({ dataset: this.data.graph, term: someTerm }).in().quads();
return Array.from(quads)
}
}

View file

@ -9,13 +9,13 @@ import { toIRI} from '../modules/utils';
export class ShapesDataset extends RdfDataset {
constructor() {
super()
this.propertyGroups = {}
this.nodeShapes = {}
this.nodeShapeNames = {}
this.nodeShapeNamesArray = []
this.nodeShapeIRIs = null
constructor(data = {}) {
super(data)
this.data.propertyGroups = {}
this.data.nodeShapes = {}
this.data.nodeShapeNames = {}
this.data.nodeShapeNamesArray = []
this.data.nodeShapeIRIs = null
}
onDataFn(quad) {
@ -25,30 +25,30 @@ export class ShapesDataset extends RdfDataset {
const object = quad.object;
// Isolate sh:NodeShape instances
if (predicate === RDF.type.value && object.value === SHACL.NodeShape.value) {
this.nodeShapes[subject] = {properties: []};
this.data.nodeShapes[subject] = {properties: []};
}
// Get properties of node shapes
if (predicate === SHACL.property.value) {
this.nodeShapes[subject].properties.push(object);
this.data.nodeShapes[subject].properties.push(object);
}
// Get property groups, if any
if (predicate === RDF.type.value && object.value === SHACL.PropertyGroup.value) {
this.propertyGroups[subject] = {};
this.data.propertyGroups[subject] = {};
}
this.dispatchEvent(new CustomEvent('quad', { detail: quad }));
}
async onDataEndFn() {
// Loop through all nodeshapes to restructure them
for (const [key, val] of Object.entries(this.nodeShapes)) {
for (const [key, val] of Object.entries(this.data.nodeShapes)) {
// Get attributes (other than 'properties') of the nodeshape
this.graph.forEach(quad => {
this.data.graph.forEach(quad => {
if (quad.subject.value === key && quad.predicate.value != SHACL.property.value) {
// Check if the object is a blank node and resolve it
if (quad.object.termType === 'BlankNode') {
this.nodeShapes[key][quad.predicate.value] = this.resolveBlankNode(quad.object);
this.data.nodeShapes[key][quad.predicate.value] = this.resolveBlankNode(quad.object);
} else {
this.nodeShapes[key][quad.predicate.value] = quad.object.value;
this.data.nodeShapes[key][quad.predicate.value] = quad.object.value;
}
}
});
@ -62,7 +62,7 @@ export class ShapesDataset extends RdfDataset {
} else {
// Non-blank nodes are kept as they are, but eventually store only their `.value`
var new_node = {};
this.graph.forEach((quad) => {
this.data.graph.forEach((quad) => {
if (quad.subject.value === node.value) {
new_node[quad.predicate.value] = quad.object.value; // Store only .value
}
@ -71,27 +71,27 @@ export class ShapesDataset extends RdfDataset {
}
}
}
for (const iri of Object.keys(this.nodeShapes)) {
for (const iri of Object.keys(this.data.nodeShapes)) {
var parts = iri.split('/')
this.nodeShapeNames[parts[parts.length - 1]] = iri
this.data.nodeShapeNames[parts[parts.length - 1]] = iri
}
this.nodeShapeNamesArray = Object.keys(this.nodeShapeNames).sort()
this.nodeShapeIRIs = Object.keys(this.nodeShapes).sort()
this.data.nodeShapeNamesArray = Object.keys(this.data.nodeShapeNames).sort()
this.data.nodeShapeIRIs = Object.keys(this.data.nodeShapes).sort()
// Now handle the (possibility of) property groups
for (const [key, value] of Object.entries(this.propertyGroups)) {
this.graph.forEach(quad => {
for (const [key, value] of Object.entries(this.data.propertyGroups)) {
this.data.graph.forEach(quad => {
if (quad.subject.value === key && quad.predicate.value != RDF.type.value ) {
this.propertyGroups[key][quad.predicate.value] = quad.object.value
this.data.propertyGroups[key][quad.predicate.value] = quad.object.value
}
});
}
this.serializedGraph = await this.serializeGraph()
this.graphLoaded = true
this.dispatchEvent(new CustomEvent('graphLoaded', { detail: this.graph }));
this.data.serializedGraph = await this.serializeGraph()
this.data.graphLoaded = true
this.dispatchEvent(new CustomEvent('graphLoaded', { detail: this.data.graph }));
}
getPropertyNodeKind(class_uri, property_uri, id_uri) {
var nodeShape = this.nodeShapes[class_uri]
var nodeShape = this.data.nodeShapes[class_uri]
var propertyShapes = nodeShape.properties
// Find associated property shape, for information about nodekind
var propertyShape = propertyShapes.find((prop) => prop[SHACL.path.value] == property_uri)
@ -125,7 +125,7 @@ export class ShapesDataset extends RdfDataset {
if (propertyShape.hasOwnProperty(SHACL.class.value)) {
var shClass = propertyShape[SHACL.class.value];
// this now assumes that the class is part of the driving shacl shapes graph
var associatedNodeShape = this.nodeShapes[toIRI(shClass, this.prefixes)]
var associatedNodeShape = this.data.nodeShapes[toIRI(shClass, this.data.prefixes)]
var hasIdField = associatedNodeShape.properties.find((prop) => prop[SHACL.path.value] == id_uri)
if (hasIdField) {
nodeFunc = rdf.namedNode

View file

@ -18,8 +18,8 @@ describe('ClassDataset', () => {
console.log("Running ClassDataset Test 1...")
expect(dataset.graphLoaded).toBe(false);
expect(dataset.prefixesLoaded).toBe(false);
expect(dataset.data.graphLoaded).toBe(false);
expect(dataset.data.prefixesLoaded).toBe(false);
server = httpServer.createServer({ });
server.listen(PORT, HOST, (err) => {
@ -29,9 +29,9 @@ describe('ClassDataset', () => {
const fileUrl = `http://${HOST}:${PORT}/tests/mockData.ttl`
dataset.loadRDF(fileUrl);
await new Promise(resolve => dataset.addEventListener('graphLoaded', resolve));
expect(dataset.graph.size).toBe(1);
expect(dataset.graphLoaded).toBe(true);
expect(dataset.prefixesLoaded).toBe(true);
expect(dataset.data.graph.size).toBe(1);
expect(dataset.data.graphLoaded).toBe(true);
expect(dataset.data.prefixesLoaded).toBe(true);
const serializedGraph = await dataset.serializeGraph();
expect(serializedGraph).not.toContain('<http://example.com/subject>');
expect(serializedGraph).not.toContain('<http://example.com/predicate>');

View file

@ -93,7 +93,7 @@ describe('FormBase', () => {
form.saveNode(class_uri, subject_uri, shapesDS, rdfDSnew, false)
// Even though formdata has 3 predicates for the record,
// the id_iri field should not be saved as a separate quad
expect(rdfDSnew.graph.size).toBe(2)
expect(rdfDSnew.data.graph.size).toBe(2)
});

View file

@ -20,7 +20,7 @@ describe('RdfDataset', () => {
// test creation of class instance
it('should create an empty dataset', () => {
console.log(`Running RdfDataset Test ${i++}...`)
expect(dataset.graph.size).toBe(0);
expect(dataset.data.graph.size).toBe(0);
});
it('should add a quad to the dataset', () => {
@ -32,8 +32,8 @@ describe('RdfDataset', () => {
dataset.addQuad(quad);
expect(dataset.graph.size).toBe(1);
expect(dataset.graph.has(quad)).toBe(true);
expect(dataset.data.graph.size).toBe(1);
expect(dataset.data.graph.has(quad)).toBe(true);
});
it('should correctly detect an RDF list', () => {
@ -88,18 +88,18 @@ describe('RdfDataset', () => {
if (err && err.code !== 'EADDRINUSE') throw err;
console.log(`Test server started on http://${HOST}:${PORT}`);
});
expect(dataset.graphLoaded).toBe(false);
expect(dataset.prefixesLoaded).toBe(false);
expect(dataset.data.graphLoaded).toBe(false);
expect(dataset.data.prefixesLoaded).toBe(false);
const fileUrl = `http://${HOST}:${PORT}/tests/mockData.ttl`
const graphLoadedHandler = vi.fn();
dataset.addEventListener('graphLoaded', graphLoadedHandler);
dataset.loadRDF(fileUrl);
await new Promise(resolve => dataset.addEventListener('graphLoaded', resolve));
expect(graphLoadedHandler).toHaveBeenCalledTimes(1);
expect(dataset.graph.size).toBe(2);
expect(dataset.prefixes['ex']).toBe('http://example.com/');
expect(dataset.graphLoaded).toBe(true);
expect(dataset.prefixesLoaded).toBe(true);
expect(dataset.data.graph.size).toBe(2);
expect(dataset.data.prefixes['ex']).toBe('http://example.com/');
expect(dataset.data.graphLoaded).toBe(true);
expect(dataset.data.prefixesLoaded).toBe(true);
console.log(`Closing server on http://${HOST}:${PORT}`);
server.close();
@ -112,7 +112,7 @@ describe('RdfDataset', () => {
dataset.addEventListener('prefix', prefixHandler);
dataset.onPrefixFn('ex', rdf.namedNode('http://example.com/'));
expect(prefixHandler).toHaveBeenCalledTimes(1);
expect(dataset.prefixes['ex']).toBe('http://example.com/');
expect(dataset.data.prefixes['ex']).toBe('http://example.com/');
});
it('should resolve blank nodes correctly', () => {

View file

@ -15,8 +15,8 @@ describe('ShapesDataset', () => {
});
it('should load RDF data from Turtle file and populate all shapes-related variables', async () => {
expect(dataset.graphLoaded).toBe(false);
expect(dataset.prefixesLoaded).toBe(false);
expect(dataset.data.graphLoaded).toBe(false);
expect(dataset.data.prefixesLoaded).toBe(false);
server = httpServer.createServer({ });
server.listen(PORT, HOST, (err) => {
if (err && err.code !== 'EADDRINUSE') throw err;
@ -25,26 +25,26 @@ describe('ShapesDataset', () => {
const fileUrl = `http://${HOST}:${PORT}/tests/mockShapes.ttl`
dataset.loadRDF(fileUrl);
await new Promise(resolve => dataset.addEventListener('graphLoaded', resolve));
expect(dataset.graphLoaded).toBe(true);
expect(dataset.prefixesLoaded).toBe(true);
expect(dataset.graph.size).toBe(318); // number of quads in the mockShapes.ttl file
expect(dataset.data.graphLoaded).toBe(true);
expect(dataset.data.prefixesLoaded).toBe(true);
expect(dataset.data.graph.size).toBe(318); // number of quads in the mockShapes.ttl file
// Test content of all loaded variables
expect(dataset.nodeShapeNames).toEqual(
expect(dataset.data.nodeShapeNames).toEqual(
{
AttributeSpecification: 'https://concepts.datalad.org/s/things/v1/AttributeSpecification',
Person: 'https://concepts.datalad.org/s/social/unreleased/Person',
DOI: 'https://concepts.datalad.org/s/identifiers/unreleased/DOI'
}
)
expect(dataset.nodeShapeNamesArray).toEqual(['AttributeSpecification', 'DOI', 'Person'])
expect(dataset.nodeShapeIRIs).toEqual(
expect(dataset.data.nodeShapeNamesArray).toEqual(['AttributeSpecification', 'DOI', 'Person'])
expect(dataset.data.nodeShapeIRIs).toEqual(
[
'https://concepts.datalad.org/s/identifiers/unreleased/DOI',
'https://concepts.datalad.org/s/social/unreleased/Person',
'https://concepts.datalad.org/s/things/v1/AttributeSpecification'
]
)
expect(dataset.propertyGroups).toEqual(
expect(dataset.data.propertyGroups).toEqual(
{
'https://concepts.datalad.org/s/things/v1/BasicPropertyGroup': {
'http://www.w3.org/ns/shacl#order': '0',
@ -58,7 +58,7 @@ describe('ShapesDataset', () => {
}
}
)
expect(dataset.prefixes).toEqual(
expect(dataset.data.prefixes).toEqual(
{
'ex': 'http://example.com/',
'dlidentifiers': 'https://concepts.datalad.org/s/identifiers/unreleased/',