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/foaf.ts b/src/vocabulary/foaf.ts index c375e39..d8b7bd2 100644 --- a/src/vocabulary/foaf.ts +++ b/src/vocabulary/foaf.ts @@ -1,8 +1,21 @@ export const FOAF = { - isPrimaryTopicOf: "http://xmlns.com/foaf/0.1/isPrimaryTopicOf", - primaryTopic: "http://xmlns.com/foaf/0.1/primaryTopic", - name: "http://xmlns.com/foaf/0.1/name", + account: "http://xmlns.com/foaf/0.1/account", + accountName: "http://xmlns.com/foaf/0.1/accountName", email: "http://xmlns.com/foaf/0.1/email", homepage: "http://xmlns.com/foaf/0.1/homepage", + icon: "http://xmlns.com/foaf/0.1/icon", + isPrimaryTopicOf: "http://xmlns.com/foaf/0.1/isPrimaryTopicOf", knows: "http://xmlns.com/foaf/0.1/knows", + maker: "http://xmlns.com/foaf/0.1/maker", + name: "http://xmlns.com/foaf/0.1/name", + nick: "http://xmlns.com/foaf/0.1/nick", + primaryTopic: "http://xmlns.com/foaf/0.1/primaryTopic", + + + Account: "http://xmlns.com/foaf/0.1/Account", + OnlineAccount: "http://xmlns.com/foaf/0.1/OnlineAccount", + Person: "http://xmlns.com/foaf/0.1/Person", + PersonalProfileDocument: "http://xmlns.com/foaf/0.1/PersonalProfileDocument", + + } as const; 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..482db9c 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -8,3 +8,6 @@ export * from "./rdf.js" export * from "./rdfs.js" export * from "./solid.js" export * from "./vcard.js" +export * from "./ical.js" +export * from "./schema.js" +export * from "./org.js" \ No newline at end of file diff --git a/src/vocabulary/org.ts b/src/vocabulary/org.ts new file mode 100644 index 0000000..b54d77e --- /dev/null +++ b/src/vocabulary/org.ts @@ -0,0 +1,7 @@ +export const ORG = { + + + member: "http://www.w3.org/ns/org#member", + organization: "http://www.w3.org/ns/org#organization", + role: "http://www.w3.org/ns/org#role" +} \ No newline at end of file diff --git a/src/vocabulary/schema.ts b/src/vocabulary/schema.ts new file mode 100644 index 0000000..32b2af0 --- /dev/null +++ b/src/vocabulary/schema.ts @@ -0,0 +1,12 @@ +export const SCHEMA = { + knowsLanguage: "https://schema.org/knowsLanguage", + Organization: "https://schema.org/Organization", + skills: "https://schema.org/skills", + startDate: "https://schema.org/startDate", + endDate: "https://schema.org/endDate", + description: "https://schema.org/description", + name: "https://schema.org/name", + uri: "https://schema.org/uri", +} as const; + + diff --git a/src/vocabulary/soc.ts b/src/vocabulary/soc.ts new file mode 100644 index 0000000..23258fa --- /dev/null +++ b/src/vocabulary/soc.ts @@ -0,0 +1,22 @@ +export const SOC = { + BlueSkyAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#BlueSkyAccount", + Digg: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#Digg", + FacebookAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#FacebookAccount", + GithubAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#GithubAccount", + InstagramAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#InstagramAccount", + LinkedInAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#LinkedInAccount", + MastodonAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#MastodonAccount", + MatrixAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#MatrixAccount", + MediumAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#MediumAccount", + NostrAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#NostrAccount", + OrcidAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#OrcidAccount", + PinterestAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#PinterestAccount", + RedditAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#RedditAccount", + SnapchatAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#SnapchatAccount", + StravaAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#StravaAccount", + TiktokAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#TiktokAccount", + TumblrAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#TumblrAccount", + TwitterAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#TwitterAccount", + OtherAccount: "https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#OtherAccount", + }; + \ No newline at end of file diff --git a/src/vocabulary/solid.ts b/src/vocabulary/solid.ts index ead7f18..d69cec7 100644 --- a/src/vocabulary/solid.ts +++ b/src/vocabulary/solid.ts @@ -1,4 +1,19 @@ export const SOLID = { oidcIssuer: "http://www.w3.org/ns/solid/terms#oidcIssuer", storage: "http://www.w3.org/ns/solid/terms#storage", -} as const; + preferredSubjectPronoun: "http://www.w3.org/ns/solid/terms#preferredSubjectPronoun", + preferredObjectPronoun: "http://www.w3.org/ns/solid/terms#preferredObjectPronoun", + preferredRelativePronoun: "http://www.w3.org/ns/solid/terms#preferredRelativePronoun", + publicId: "http://www.w3.org/ns/solid/terms#publicId", + + + // the following terms are not defined but are present in https://github.com/SolidOS/profile-pane/blob/main/src/ontology/profileForm.ttl + Role: "http://www.w3.org/ns/solid/terms#Role", + CurrentRole: "http://www.w3.org/ns/solid/terms#CurrentRole", + FormerRole: "http://www.w3.org/ns/solid/terms#FormerRole", + FutureRole: "http://www.w3.org/ns/solid/terms#FutureRole", + + + +} + diff --git a/src/webid/Profile.ts b/src/webid/Profile.ts new file mode 100644 index 0000000..a906a6f --- /dev/null +++ b/src/webid/Profile.ts @@ -0,0 +1,224 @@ +import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" +import { FOAF, SOLID, SCHEMA, ORG, VCARD } from "../vocabulary/mod.js" + +import { Agent } from "@solid/object" + +export class ProfileDataset extends DatasetWrapper { + + get profile(): Iterable { + return this.instancesOf(FOAF.PersonalProfileDocument, Profile) + } +} + +export class Profile extends TermWrapper { + + get primaryTopic(): string | undefined { + return this.singularNullable(FOAF.primaryTopic, ValueMappings.iriToString ) + } + set primaryTopic(value: string | undefined) { + this.overwriteNullable(FOAF.primaryTopic, value, TermMappings.stringToIri) + } + + get maker(): string | undefined { + return this.singularNullable(FOAF.maker, ValueMappings.iriToString) + } + set maker(value: string | undefined) { + this.overwriteNullable(FOAF.maker, value, TermMappings.stringToIri) + } + + /* Nickname */ + get nickname(): string | undefined { + return this.singularNullable(FOAF.nick, ValueMappings.literalToString) + } + set nickname(value: string | undefined) { + this.overwriteNullable(FOAF.nick, value, TermMappings.stringToLiteral) + } + + /* Pronouns */ + get preferredSubjectPronoun(): string | undefined { + return this.singularNullable(SOLID.preferredSubjectPronoun, ValueMappings.literalToString) + } + set preferredSubjectPronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredSubjectPronoun, value, TermMappings.stringToLiteral) + } + get preferredObjectPronoun(): string | undefined { + return this.singularNullable(SOLID.preferredObjectPronoun, ValueMappings.literalToString) + } + set preferredObjectPronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredObjectPronoun, value, TermMappings.stringToLiteral) + } + get preferredRelativePronoun(): string | undefined { + return this.singularNullable(SOLID.preferredRelativePronoun, ValueMappings.literalToString) + } + set preferredRelativePronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredRelativePronoun, value, TermMappings.stringToLiteral) + } + + + /* Roles (inverse org:member) */ + + get roles(): Iterable { + return this.objects(ORG.member, Role) + } + + + /* Skills */ + get skills(): Iterable { + + return this.objects(SCHEMA.skills, Skill) + } + + /* Languages */ + get languages(): Iterable { + return this.objects(SCHEMA.knowsLanguage, Language) + } + + /* Online Accounts */ + get accounts(): Iterable { + return this.objects(FOAF.account, OnlineAccount) + } + } + + + export class Organization extends TermWrapper { + + get name(): string | undefined { + return this.singularNullable(SCHEMA.name, ValueMappings.literalToString) + } + + set name(value: string | undefined) { + this.overwriteNullable(SCHEMA.name, value, TermMappings.stringToLiteral) + } + + get uri(): string | undefined { + return this.singularNullable(SCHEMA.uri, ValueMappings.iriToString) + } + + set uri(value: string | undefined) { + this.overwriteNullable(SCHEMA.uri, value, TermMappings.stringToIri) + } + + get publicId(): string | undefined { + return this.singularNullable(SOLID.publicId, ValueMappings.iriToString) + } + + set publicId(value: string | undefined) { + this.overwriteNullable(SOLID.publicId, value, TermMappings.stringToIri) + } + + } + + export class Role extends TermWrapper { + + get organization(): Organization | undefined { + return this.singularNullable(ORG.organization, Organization) + } + + set organization(value: Organization | undefined) { + this.overwriteNullable(ORG.organization, value) + } + + + + /* Role Name */ + get roleName(): string | undefined { + return this.singularNullable(VCARD.role, ValueMappings.literalToString) + } + + set roleName(value: string | undefined) { + this.overwriteNullable(VCARD.role, value, TermMappings.stringToLiteral) + } + + /* Occupation */ + get occupation(): Role | undefined { + return this.singularNullable(ORG.role, Role) + } + + set occupation(value: Role | undefined) { + this.overwriteNullable(ORG.role, value) + } + + + /* Start Date */ + get startDate(): Date | undefined { + return this.singularNullable(SCHEMA.startDate, ValueMappings.literalToDate) + } + + set startDate(value: Date | undefined) { + this.overwriteNullable(SCHEMA.startDate, value, TermMappings.dateToLiteral) + } + + /* End Date */ + get endDate(): Date | undefined { + return this.singularNullable(SCHEMA.endDate, ValueMappings.literalToDate) + } + + set endDate(value: Date | undefined) { + this.overwriteNullable(SCHEMA.endDate, value, TermMappings.dateToLiteral) + } + + /* Description */ + get description(): string | undefined { + return this.singularNullable(SCHEMA.description, ValueMappings.literalToString) + } + + set description(value: string | undefined) { + this.overwriteNullable(SCHEMA.description, value, TermMappings.stringToLiteral) + } + + } + + + + export class OnlineAccount extends TermWrapper { + + get accountName(): string | undefined { + return this.singularNullable(FOAF.accountName, ValueMappings.literalToString) + } + + set accountName(value: string | undefined) { + this.overwriteNullable(FOAF.accountName, value, TermMappings.stringToLiteral) + } + + get homepage(): string | undefined { + return this.singularNullable(FOAF.homepage, ValueMappings.iriToString) + } + + set homepage(value: string | undefined) { + this.overwriteNullable(FOAF.homepage, value, TermMappings.stringToIri) + } + + get icon(): string | undefined { + return this.singularNullable(FOAF.icon, ValueMappings.literalToString) + } + + set icon(value: string | undefined) { + this.overwriteNullable(FOAF.icon, value, TermMappings.stringToLiteral) + } + + } + + +export class Skill extends TermWrapper { + + get publicId(): string | undefined { + return this.singularNullable(SOLID.publicId, ValueMappings.iriToString) + } + + set publicId(value: string | undefined) { + this.overwriteNullable(SOLID.publicId, value, TermMappings.stringToIri) + } + +} + +export class Language extends TermWrapper { + + get publicId(): string | undefined { + return this.singularNullable(SOLID.publicId, ValueMappings.iriToString) + } + + set publicId(value: string | undefined) { + this.overwriteNullable(SOLID.publicId, value, TermMappings.stringToIri) + } + +} diff --git a/src/webid/mod.ts b/src/webid/mod.ts index 45f65a9..4eb2baa 100644 --- a/src/webid/mod.ts +++ b/src/webid/mod.ts @@ -1,2 +1,3 @@ export * from "./Agent.js" export * from "./WebIdDataset.js" +export * from "./Profile.js" 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