Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
],
"license": "MIT",
"dependencies": {
"@solid/object": "^0.4.0",
"rdfjs-wrapper": "^0.15.0"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./acp/mod.js"
export * from "./solid/mod.js"
export * from "./webid/mod.js"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change


52 changes: 52 additions & 0 deletions src/solid/Meeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper"
import { ICAL } from "../vocabulary/mod.js"


export class MeetingDataset extends DatasetWrapper {
Comment on lines +2 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { ICAL } from "../vocabulary/mod.js"
export class MeetingDataset extends DatasetWrapper {
import { ICAL } from "../vocabulary/mod.js"
export class MeetingDataset extends DatasetWrapper {

get meeting(): Iterable<Meeting> {
return this.instancesOf(ICAL.vevent, Meeting)
}
}
Comment on lines +5 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should live in its own file: MeetingDataset.ts.

Also, fix capitalisation (reflects further review comments):

Suggested change
export class MeetingDataset extends DatasetWrapper {
get meeting(): Iterable<Meeting> {
return this.instancesOf(ICAL.vevent, Meeting)
}
}
export class MeetingDataset extends DatasetWrapper {
get meeting(): Iterable<Meeting> {
return this.instancesOf(ICAL.Vevent, Meeting)
}
}

It would be interesting to add a link to ical to justify why we use classification based selection (no properties used in the context of this "shape" actually has only domain Vevent: https://www.w3.org/2002/12/cal/ical.n3).

And maybe consider actually using a union pattern with subjectof the various used properties (because in this context, we might only use the properties for Vevents).

Example:

const subjects = new Set([
            ...this.instancesOf(ACP.AccessControlResource, AccessControlResource),
            ...this.subjectsOf(ACP.resource, AccessControlResource),
            ...this.subjectsOf(ACP.accessControl, AccessControlResource),
            ...this.subjectsOf(ACP.memberAccessControl, AccessControlResource)
        ])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corollary: If tdf:type is the chosen modelling paradigm (and I don't agree that it should be, I would prefer usage based classification) then the mapping class should enable consumers to read/write its type.

See https://github.com/solid/object/blob/main/src/acp/Typed.ts for a pattern for working with type statements.


export class Meeting extends TermWrapper {
get summary(): string | undefined {
return this.singularNullable(ICAL.summary, ValueMappings.literalToString)
}

set summary(value: string | undefined) {
this.overwriteNullable(ICAL.summary, value, TermMappings.stringToLiteral)
}

get location(): string | undefined {
return this.singularNullable(ICAL.location, ValueMappings.literalToString)
}

set location(value: string | undefined) {
this.overwriteNullable(ICAL.location, value, TermMappings.stringToLiteral)
}

get comment(): string | undefined {
return this.singularNullable(ICAL.comment, ValueMappings.literalToString)
}

set comment(value: string | undefined) {
this.overwriteNullable(ICAL.comment, value, TermMappings.stringToLiteral)
}

get startDate(): Date | undefined {
return this.singularNullable(ICAL.dtstart, ValueMappings.literalToDate)
}

set startDate(value: Date | undefined) {
this.overwriteNullable(ICAL.dtstart, value, TermMappings.dateToLiteral)
}

get endDate(): Date | undefined {
return this.singularNullable(ICAL.dtend, ValueMappings.literalToDate)
}

set endDate(value: Date | undefined) {
this.overwriteNullable(ICAL.dtend, value, TermMappings.dateToLiteral)
}

}
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
}
}

1 change: 1 addition & 0 deletions src/solid/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Container.js"
export * from "./ContainerDataset.js"
export * from "./Resource.js"
export * from "./Meeting.js"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export * from "./Meeting.js"
export * from "./Meeting.js"

1 change: 1 addition & 0 deletions src/vocabulary/ical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const ICAL = {
dtstart: "http://www.w3.org/2002/12/cal/ical#dtstart",
location: "http://www.w3.org/2002/12/cal/ical#location",
summary: "http://www.w3.org/2002/12/cal/ical#summary",
vevent: "http://www.w3.org/2002/12/cal/ical#Vevent"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
vevent: "http://www.w3.org/2002/12/cal/ical#Vevent"
Vevent: "http://www.w3.org/2002/12/cal/ical#Vevent"

} as const;
1 change: 1 addition & 0 deletions src/vocabulary/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./rdf.js"
export * from "./rdfs.js"
export * from "./solid.js"
export * from "./vcard.js"
export * from "./ical.js"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export * from "./ical.js"
export * from "./ical.js"

145 changes: 145 additions & 0 deletions test/unit/meeting.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it would be worth considering a test pattern for mutations where

  1. Test has some RDF in Turtle
  2. RDF is is loaded into a dataset
  3. Dataset is wrapped by mapping classes
  4. Instances of wrappers are mutated
  5. Actual dataset is asserted to be isomorphic with an expected dataset

This is more robust in a way because it does not rely on ther wrappers for reading out values modified by the wrapper itself.

Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { DataFactory, Parser, Store } from "n3"
import assert from "node:assert"
import { describe, it } from "node:test"

import { MeetingDataset } from "@solid/object";


describe("MeetingDataset / Meeting tests", () => {

const sampleRDF = `
Comment on lines +3 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { describe, it } from "node:test"
import { MeetingDataset } from "@solid/object";
describe("MeetingDataset / Meeting tests", () => {
const sampleRDF = `
import { describe, it } from "node:test"
import { MeetingDataset } from "@solid/object";
describe("MeetingDataset / Meeting tests", () => {
const sampleRDF = `

@prefix cal: <http://www.w3.org/2002/12/cal/ical#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<https://example.org/meeting/1> a cal:Vevent ;
cal:summary "Team Sync" ;
cal:location "Zoom Room 123" ;
cal:comment "Discuss project updates" ;
cal:dtstart "2026-02-09T10:00:00Z"^^xsd:dateTime ;
cal:dtend "2026-02-09T11:00:00Z"^^xsd:dateTime .
Comment on lines +14 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that is what corresponds to the current shape in the pane.

However, could you find a few slightly more comprehensive ical events examples so that we create a class that might be a bit more usable?

I wonder if we can get at least the properties corresponding to the main fields in all well known calendar systems. See maybe RFC5545 section 4 for examples.

`;

it("should parse and retrieve meeting properties", () => {
const store = new Store();
store.addQuads(new Parser().parse(sampleRDF));

const dataset = new MeetingDataset(store, DataFactory);
const meetings = Array.from(dataset.meeting);

const meeting = meetings[0];
assert.ok(meeting, "No meeting found")

// Check property types and values

assert.equal(meeting.summary, "Team Sync");
Comment on lines +33 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check property types and values
assert.equal(meeting.summary, "Team Sync");
// Check property types and values
assert.equal(meeting.summary, "Team Sync");

assert.equal(meeting.location, "Zoom Room 123");
assert.equal(meeting.comment, "Discuss project updates");


assert.ok(meeting.startDate instanceof Date);
Comment on lines +37 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.equal(meeting.comment, "Discuss project updates");
assert.ok(meeting.startDate instanceof Date);
assert.equal(meeting.comment, "Discuss project updates");
assert.ok(meeting.startDate instanceof Date);

assert.ok(meeting.endDate instanceof Date);

assert.equal(meeting.startDate?.toISOString(), "2026-02-09T10:00:00.000Z");
assert.equal(meeting.endDate?.toISOString(), "2026-02-09T11:00:00.000Z");
});



it("should allow setting of meeting properties", () => {
Comment on lines +45 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
it("should allow setting of meeting properties", () => {
});
it("should allow setting of meeting properties", () => {

const store = new Store();
store.addQuads(new Parser().parse(sampleRDF));

const dataset = new MeetingDataset(store, DataFactory);
const meetings = Array.from(dataset.meeting);

assert.ok(meetings.length > 0, "No meetings found");

const meeting = Array.from(dataset.meeting)[0]!;

// Set new values
meeting.summary = "Updated Meeting";
meeting.location = "Conference Room A";
meeting.comment = "New agenda";
const newStart = new Date("2026-02-09T12:00:00Z");
const newEnd = new Date("2026-02-09T13:00:00Z");
meeting.startDate = newStart;
meeting.endDate = newEnd;

// Retrieve again
assert.equal(meeting.summary, "Updated Meeting");
assert.equal(meeting.location, "Conference Room A");
assert.equal(meeting.comment, "New agenda");
assert.equal(meeting.startDate.toISOString(), newStart.toISOString());
assert.equal(meeting.endDate.toISOString(), newEnd.toISOString());
});



it("should ensure all properties are correct type", () => {
Comment on lines +75 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
it("should ensure all properties are correct type", () => {
});
it("should ensure all properties are correct type", () => {

const store = new Store();
store.addQuads(new Parser().parse(sampleRDF));

const dataset = new MeetingDataset(store, DataFactory);
const meeting = Array.from(dataset.meeting)[0];

assert.ok(meeting, "No meeting found")

// Check property types

assert.equal(typeof meeting.summary, "string");
Comment on lines +88 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check property types
assert.equal(typeof meeting.summary, "string");
// Check property types
assert.equal(typeof meeting.summary, "string");

assert.equal(typeof meeting.location, "string");
assert.equal(typeof meeting.comment, "string");

assert.ok(meeting.startDate instanceof Date, "startDate should be a Date");
assert.ok(meeting.endDate instanceof Date, "endDate should be a Date");

});


it("should ensure all properties are unique text or date values", () => {

const duplicateRDF = `
Comment on lines +95 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.ok(meeting.endDate instanceof Date, "endDate should be a Date");
});
it("should ensure all properties are unique text or date values", () => {
const duplicateRDF = `
assert.ok(meeting.endDate instanceof Date, "endDate should be a Date");
});
it("should ensure all properties are unique text or date values", () => {
const duplicateRDF = `

@prefix cal: <http://www.w3.org/2002/12/cal/ical#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<https://example.org/meeting/1> a cal:Vevent ;
cal:summary "Team Sync" ;
cal:summary "Duplicate Summary" ;
cal:location "Zoom Room 123" ;
cal:location "Duplicate Location" ;
cal:comment "Discuss project updates" ;
cal:comment "Duplicate Comment" ;
cal:dtstart "2026-02-09T10:00:00Z"^^xsd:dateTime ;
cal:dtstart "2026-02-09T09:00:00Z"^^xsd:dateTime ;
cal:dtend "2026-02-09T11:00:00Z"^^xsd:dateTime ;
cal:dtend "2026-02-09T12:00:00Z"^^xsd:dateTime .
`;

const store = new Store();
store.addQuads(new Parser().parse(duplicateRDF));

const dataset = new MeetingDataset(store, DataFactory);
const meeting = Array.from(dataset.meeting)[0];

assert.ok(meeting, "No meeting found");

// Ensure exposed values are single (unique) and correct type
assert.equal(typeof meeting.summary, "string");
assert.equal(typeof meeting.location, "string");
assert.equal(typeof meeting.comment, "string");

assert.ok(meeting.startDate instanceof Date);
assert.ok(meeting.endDate instanceof Date);

// Ensure no arrays are returned
assert.ok(!Array.isArray(meeting.summary));
assert.ok(!Array.isArray(meeting.location));
assert.ok(!Array.isArray(meeting.comment));
assert.ok(!Array.isArray(meeting.startDate));
assert.ok(!Array.isArray(meeting.endDate));
});



});
Comment on lines +141 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
});
});
});