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
1 change: 1 addition & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./acp/mod.js"
export * from "./solid/mod.js"
export * from "./webid/mod.js"

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

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)
}
}
9 changes: 9 additions & 0 deletions src/solid/MeetingDataset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DatasetWrapper } from "rdfjs-wrapper"
import { ICAL } from "../vocabulary/mod.js"
import { Meeting } from "./Meeting.js"

export class MeetingDataset extends DatasetWrapper {
get meeting(): Iterable<Meeting> {
return this.instancesOf(ICAL.Vevent, Meeting)
}
}
2 changes: 2 additions & 0 deletions src/solid/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./Container.js"
export * from "./ContainerDataset.js"
export * from "./Resource.js"
export * from "./Meeting.js"
export * from "./MeetingDataset.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"
} 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"
229 changes: 229 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,229 @@
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 = `
@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 +11 to +17
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.

Copy link
Member Author

Choose a reason for hiding this comment

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

"get at least the properties corresponding to the main fields in all well known calendar systems" Just to confirm @matthieubosquet, you'd like to add new properties to Meeting.ts and then add corresponding test cases? The existing properties are based on the SolidOS shape for Meeting at https://github.com/solid/shape-generation-artefacts/tree/main/shapes/Meeting with ref to https://github.com/SolidOS/meeting-pane/blob/main/src/meetingDetailsForm.ttl

`;

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");
assert.equal(meeting.location, "Zoom Room 123");
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", () => {
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", () => {
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");
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 = `
@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));
});



// RFC 5545 requires DTSTART and UID - test the required date behaviour

it("should parse a minimal RFC5545-style VEVENT", () => {
const minimalRDF = `
@prefix cal: <http://www.w3.org/2002/12/cal/ical#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<https://example.org/meeting/minimal>
a cal:Vevent ;
cal:dtstart "2026-06-01T09:00:00Z"^^xsd:dateTime .
`;

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

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

assert.ok(meeting);
assert.ok(meeting.startDate instanceof Date);
assert.equal(meeting.startDate?.toISOString(), "2026-06-01T09:00:00.000Z");

// Optional fields should be undefined
assert.equal(meeting.summary, undefined);
assert.equal(meeting.location, undefined);
assert.equal(meeting.comment, undefined);
assert.equal(meeting.endDate, undefined);
});


// With ref to common VEVENT examples in RFC 5545 3.6.1.

it("should parse an RFC-style VEVENT with summary, description, and location", () => {
const rfcStyleRDF = `
@prefix cal: <http://www.w3.org/2002/12/cal/ical#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<https://example.org/meeting/rfc1>
a cal:Vevent ;
cal:summary "Meeting" ;
cal:comment "Discuss events" ;
cal:location "Conference Room 1" ;
cal:dtstart "2026-07-10T13:00:00Z"^^xsd:dateTime ;
cal:dtend "2026-07-10T15:30:00Z"^^xsd:dateTime .
`;

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

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

assert.ok(meeting);

assert.equal(meeting.summary, "Meeting");
assert.equal(meeting.comment, "Discuss events");
assert.equal(meeting.location, "Conference Room 1");

assert.equal(meeting.startDate?.toISOString(), "2026-07-10T13:00:00.000Z");
assert.equal(meeting.endDate?.toISOString(), "2026-07-10T15:30:00.000Z");
});


// RFC 5545 allows DATE values for all-day events. This tests literalToDate handling.

it("should parse an all-day RFC-style event (DATE value)", () => {
const allDayRDF = `
@prefix cal: <http://www.w3.org/2002/12/cal/ical#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<https://example.org/meeting/allday>
a cal:Vevent ;
cal:summary "Company Holiday" ;
cal:dtstart "2026-12-25"^^xsd:date ;
cal:dtend "2026-12-26"^^xsd:date .
`;

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

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

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

// Ensure correct calendar date
assert.equal(meeting.startDate?.getUTCFullYear(), 2026);
assert.equal(meeting.startDate?.getUTCMonth(), 11); // December (0-based)
assert.equal(meeting.startDate?.getUTCDate(), 25);
});





});