diff --git a/package.json b/package.json index d24e608..d7bb8de 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ ], "license": "MIT", "dependencies": { + "@solid/object": "^0.4.0", "rdfjs-wrapper": "^0.15.0" }, "devDependencies": { diff --git a/src/mod.ts b/src/mod.ts index a8678e4..1d8f1af 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -1,3 +1,5 @@ export * from "./acp/mod.js" export * from "./solid/mod.js" export * from "./webid/mod.js" + + diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts new file mode 100644 index 0000000..aebbf52 --- /dev/null +++ b/src/solid/Meeting.ts @@ -0,0 +1,52 @@ +import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" +import { ICAL } from "../vocabulary/mod.js" + + +export class MeetingDataset extends DatasetWrapper { + get meeting(): Iterable { + return this.instancesOf(ICAL.vevent, Meeting) + } +} + +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) + } + +} diff --git a/src/solid/mod.ts b/src/solid/mod.ts index fb84c5f..36d1929 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -1,3 +1,4 @@ export * from "./Container.js" export * from "./ContainerDataset.js" export * from "./Resource.js" +export * from "./Meeting.js" \ No newline at end of file diff --git a/src/vocabulary/ical.ts b/src/vocabulary/ical.ts index d17c669..9d53fbe 100644 --- a/src/vocabulary/ical.ts +++ b/src/vocabulary/ical.ts @@ -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; diff --git a/src/vocabulary/mod.ts b/src/vocabulary/mod.ts index 412818c..ed63146 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -8,3 +8,4 @@ export * from "./rdf.js" export * from "./rdfs.js" export * from "./solid.js" export * from "./vcard.js" +export * from "./ical.js" \ No newline at end of file diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts new file mode 100644 index 0000000..3f44af9 --- /dev/null +++ b/test/unit/meeting.test.ts @@ -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 = ` +@prefix cal: . +@prefix xsd: . + + 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 . +`; + + 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: . +@prefix xsd: . + + 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)); + }); + + + +}); \ No newline at end of file