From f3d8886f8b5a1b52b7ec7c2f8b942bdf77573c8e Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 9 Feb 2026 13:44:17 +0000 Subject: [PATCH 01/32] add Meeting object --- src/solid/Meeting.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ src/vocabulary/ical.ts | 1 + src/vocabulary/mod.ts | 1 + 3 files changed, 55 insertions(+) create mode 100644 src/solid/Meeting.ts diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts new file mode 100644 index 0000000..d93f91c --- /dev/null +++ b/src/solid/Meeting.ts @@ -0,0 +1,53 @@ +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/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 From fc6b44a6366fd0063a85c7bb284d6e1f31da366a Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 9 Feb 2026 15:23:21 +0000 Subject: [PATCH 02/32] add meeting test --- package.json | 1 + src/mod.ts | 2 + src/solid/Meeting.ts | 3 +- src/solid/mod.ts | 1 + test/unit/meeting.test.ts | 102 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 test/unit/meeting.test.ts 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 index d93f91c..aebbf52 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -1,4 +1,4 @@ -import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper, } from "rdfjs-wrapper" +import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" import { ICAL } from "../vocabulary/mod.js" @@ -8,7 +8,6 @@ export class MeetingDataset extends DatasetWrapper { } } - export class Meeting extends TermWrapper { get summary(): string | undefined { return this.singularNullable(ICAL.summary, ValueMappings.literalToString) 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/test/unit/meeting.test.ts b/test/unit/meeting.test.ts new file mode 100644 index 0000000..3329c60 --- /dev/null +++ b/test/unit/meeting.test.ts @@ -0,0 +1,102 @@ +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"); + + + + + + }); + +}); \ No newline at end of file From 36dec9b9dffbb8aeafed30d24eff0f4728736816 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 9 Feb 2026 15:44:57 +0000 Subject: [PATCH 03/32] add test for unique non array values --- test/unit/meeting.test.ts | 47 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 3329c60..3f44af9 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -12,6 +12,7 @@ describe("MeetingDataset / Meeting tests", () => { @prefix xsd: . a cal:Vevent ; + cal:summary "Team Sync" ; cal:location "Zoom Room 123" ; cal:comment "Discuss project updates" ; @@ -92,11 +93,53 @@ describe("MeetingDataset / Meeting tests", () => { 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 From bbf6a2c5cc4cb9f890f67fb009cf9c736da9b0c9 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 9 Feb 2026 17:10:39 +0000 Subject: [PATCH 04/32] start on Profile doc --- src/solid/Profile.ts | 158 +++++++++++++++++++++++++++++++++++++++++ src/vocabulary/foaf.ts | 4 ++ 2 files changed, 162 insertions(+) create mode 100644 src/solid/Profile.ts diff --git a/src/solid/Profile.ts b/src/solid/Profile.ts new file mode 100644 index 0000000..cb2d8f4 --- /dev/null +++ b/src/solid/Profile.ts @@ -0,0 +1,158 @@ +import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" +import { FOAF } from "../vocabulary/mod.js" +import { URL } from "url" +import { WebIdDataset } from "../mod.js" + + +export class PersonalProfileDataset extends DatasetWrapper { + get profile(): Iterable { + return this.instancesOf(FOAF.PersonalProfileDocument, PersonalProfileDocument) + } +} + +export class PersonalProfileDocument extends TermWrapper { + get primaryTopic(): WebIdDataset | undefined { + return this.singularNullable(FOAF.primaryTopic, ValueMappings.iriToString) + } + set primaryTopic(value: WebIdDataset | undefined) { + this.overwriteNullable(FOAF.primaryTopic, value, TermMappings.stringToIri) + } + + + get maker(): WebIdDataset | undefined { + + return this.singularNullable(FOAF.maker, ValueMappings.iriToString) + } + set maker(value: WebIdDataset | undefined) { + this.overwriteNullable(FOAF.maker, value, TermMappings.stringToIri) + } + +} + + + + + + + + + +/* + + + +fields defined in + +https://pdsinterop.org/conventions/profile/ + +via + +/Users/tanyagray/Documents/code/ODI/task-solid-extract-classes/solid-panes/docs/conventions.md + + + + +```turtle +@prefix ab: . +@prefix acl: . +@prefix dc: . +@prefix dct: . +@prefix flow: . +@prefix foaf: . +@prefix ical: . +@prefix ldp: . +@prefix mee: . +@prefix pim: . +@prefix rdf: . +@prefix schema: . +@prefix sioc: . +@prefix solid: . +@prefix stat: . +@prefix ui: . +@prefix vcard: . +@prefix XML: . +``` + +One of the most important RDF documents on your pod is your profile, which is the document that people get when they dereference your webid. We'll look at that first. After that, we'll look at each of the tools that can be created with the databrowser's + button: Addressbook, Notepad, Chat, LongChat, Meeting, Event, Link, Document, Folder, and Source. + +## Profile +(see also [pdsinterop.org's description](https://pdsinterop.org/conventions/profile/)) + +### Profile document + +To add information to your webid profile, you can use the following triples. Suppose your webid is `/profile/card#me`, then your profile document is `/profile/card` (without the `#me`). Add the following triples to it: + +```turtle + a foaf:PersonalProfileDocument . + foaf:maker . + foaf:primaryTopic . +``` + +### You as a person + +Now say your name is "John Doe", then add these triples to your profile document to publish your identity as a person: + +```turtle + a foaf:Person . + a schema:Person . + foaf:name "John Doe" . +``` + +### Linking to your pod + +Say your pod is at `/pod`, with the LDN inbox at `/pod/inbox/`, to link from your identity to your pod: + +```turtle + solid:account . + pim:storage . + ldp:inbox . +``` + +### Preferences + +To publish some of your generic preferences to apps, use: + +```turtle + pim:preferencesFile . + solid:publicTypeIndex . + solid:privateTypeIndex . +``` + + + + +https://github.com/SolidOS/profile-pane/blob/main/src/ontology/profileForm.ttl + + + 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) + } +*/ \ No newline at end of file diff --git a/src/vocabulary/foaf.ts b/src/vocabulary/foaf.ts index c375e39..a027d32 100644 --- a/src/vocabulary/foaf.ts +++ b/src/vocabulary/foaf.ts @@ -5,4 +5,8 @@ export const FOAF = { email: "http://xmlns.com/foaf/0.1/email", homepage: "http://xmlns.com/foaf/0.1/homepage", knows: "http://xmlns.com/foaf/0.1/knows", + Person: "http://xmlns.com/foaf/0.1/Person", + PersonalProfileDocument: "http://xmlns.com/foaf/0.1/PersonalProfileDocument", + maker: "http://xmlns.com/foaf/0.1/maker" + } as const; From 7910e57f709b9d28a4687bb88e0483f1da089a50 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 9 Feb 2026 22:38:18 +0000 Subject: [PATCH 05/32] early commit of Profile.ts --- src/solid/Profile.ts | 158 --------------------------- src/vocabulary/foaf.ts | 21 ++-- src/vocabulary/mod.ts | 4 +- src/vocabulary/org.ts | 7 ++ src/vocabulary/schema.ts | 12 +++ src/vocabulary/soc.ts | 22 ++++ src/vocabulary/solid.ts | 17 ++- src/webid/Profile.ts | 224 +++++++++++++++++++++++++++++++++++++++ src/webid/mod.ts | 1 + 9 files changed, 300 insertions(+), 166 deletions(-) delete mode 100644 src/solid/Profile.ts create mode 100644 src/vocabulary/org.ts create mode 100644 src/vocabulary/schema.ts create mode 100644 src/vocabulary/soc.ts create mode 100644 src/webid/Profile.ts diff --git a/src/solid/Profile.ts b/src/solid/Profile.ts deleted file mode 100644 index cb2d8f4..0000000 --- a/src/solid/Profile.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" -import { FOAF } from "../vocabulary/mod.js" -import { URL } from "url" -import { WebIdDataset } from "../mod.js" - - -export class PersonalProfileDataset extends DatasetWrapper { - get profile(): Iterable { - return this.instancesOf(FOAF.PersonalProfileDocument, PersonalProfileDocument) - } -} - -export class PersonalProfileDocument extends TermWrapper { - get primaryTopic(): WebIdDataset | undefined { - return this.singularNullable(FOAF.primaryTopic, ValueMappings.iriToString) - } - set primaryTopic(value: WebIdDataset | undefined) { - this.overwriteNullable(FOAF.primaryTopic, value, TermMappings.stringToIri) - } - - - get maker(): WebIdDataset | undefined { - - return this.singularNullable(FOAF.maker, ValueMappings.iriToString) - } - set maker(value: WebIdDataset | undefined) { - this.overwriteNullable(FOAF.maker, value, TermMappings.stringToIri) - } - -} - - - - - - - - - -/* - - - -fields defined in - -https://pdsinterop.org/conventions/profile/ - -via - -/Users/tanyagray/Documents/code/ODI/task-solid-extract-classes/solid-panes/docs/conventions.md - - - - -```turtle -@prefix ab: . -@prefix acl: . -@prefix dc: . -@prefix dct: . -@prefix flow: . -@prefix foaf: . -@prefix ical: . -@prefix ldp: . -@prefix mee: . -@prefix pim: . -@prefix rdf: . -@prefix schema: . -@prefix sioc: . -@prefix solid: . -@prefix stat: . -@prefix ui: . -@prefix vcard: . -@prefix XML: . -``` - -One of the most important RDF documents on your pod is your profile, which is the document that people get when they dereference your webid. We'll look at that first. After that, we'll look at each of the tools that can be created with the databrowser's + button: Addressbook, Notepad, Chat, LongChat, Meeting, Event, Link, Document, Folder, and Source. - -## Profile -(see also [pdsinterop.org's description](https://pdsinterop.org/conventions/profile/)) - -### Profile document - -To add information to your webid profile, you can use the following triples. Suppose your webid is `/profile/card#me`, then your profile document is `/profile/card` (without the `#me`). Add the following triples to it: - -```turtle - a foaf:PersonalProfileDocument . - foaf:maker . - foaf:primaryTopic . -``` - -### You as a person - -Now say your name is "John Doe", then add these triples to your profile document to publish your identity as a person: - -```turtle - a foaf:Person . - a schema:Person . - foaf:name "John Doe" . -``` - -### Linking to your pod - -Say your pod is at `/pod`, with the LDN inbox at `/pod/inbox/`, to link from your identity to your pod: - -```turtle - solid:account . - pim:storage . - ldp:inbox . -``` - -### Preferences - -To publish some of your generic preferences to apps, use: - -```turtle - pim:preferencesFile . - solid:publicTypeIndex . - solid:privateTypeIndex . -``` - - - - -https://github.com/SolidOS/profile-pane/blob/main/src/ontology/profileForm.ttl - - - 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) - } -*/ \ No newline at end of file diff --git a/src/vocabulary/foaf.ts b/src/vocabulary/foaf.ts index a027d32..d8b7bd2 100644 --- a/src/vocabulary/foaf.ts +++ b/src/vocabulary/foaf.ts @@ -1,12 +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", - maker: "http://xmlns.com/foaf/0.1/maker" - + PersonalProfileDocument: "http://xmlns.com/foaf/0.1/PersonalProfileDocument", + + } as const; diff --git a/src/vocabulary/mod.ts b/src/vocabulary/mod.ts index ed63146..482db9c 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -8,4 +8,6 @@ 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 +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" From 1014dd759a426646094449d1812ff01437019564 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:43:37 +0000 Subject: [PATCH 06/32] Update src/mod.ts Co-authored-by: Matthieu Bosquet --- src/mod.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mod.ts b/src/mod.ts index 1d8f1af..6230957 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -2,4 +2,3 @@ export * from "./acp/mod.js" export * from "./solid/mod.js" export * from "./webid/mod.js" - From d4cf43b71e125fcd6c025b60f44a49598726d6ac Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:44:00 +0000 Subject: [PATCH 07/32] Update src/vocabulary/mod.ts Co-authored-by: Matthieu Bosquet --- src/vocabulary/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vocabulary/mod.ts b/src/vocabulary/mod.ts index ed63146..4afb431 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -8,4 +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 +export * from "./ical.js" From 7bfd1c6b0a1536882aa051c9740de722e4ef7ae4 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:44:16 +0000 Subject: [PATCH 08/32] Update src/vocabulary/ical.ts Co-authored-by: Matthieu Bosquet --- src/vocabulary/ical.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vocabulary/ical.ts b/src/vocabulary/ical.ts index 9d53fbe..75f5cfa 100644 --- a/src/vocabulary/ical.ts +++ b/src/vocabulary/ical.ts @@ -4,5 +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" + Vevent: "http://www.w3.org/2002/12/cal/ical#Vevent" } as const; From 569b956eb37083d314fd4ac90fedefaddb642cf9 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:44:35 +0000 Subject: [PATCH 09/32] Update src/solid/mod.ts Co-authored-by: Matthieu Bosquet --- src/solid/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solid/mod.ts b/src/solid/mod.ts index 36d1929..c7516c4 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -1,4 +1,4 @@ export * from "./Container.js" export * from "./ContainerDataset.js" export * from "./Resource.js" -export * from "./Meeting.js" \ No newline at end of file +export * from "./Meeting.js" From a8f21f3e8a48dd0bf315365192f2eb1a05d4e4c2 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:46:34 +0000 Subject: [PATCH 10/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 3f44af9..9c98a2b 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -139,7 +139,4 @@ describe("MeetingDataset / Meeting tests", () => { assert.ok(!Array.isArray(meeting.startDate)); assert.ok(!Array.isArray(meeting.endDate)); }); - - - -}); \ No newline at end of file +}); From afa7837d5213416d1f57211ac8db36d5b09e9cde Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:47:17 +0000 Subject: [PATCH 11/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 9c98a2b..a6c1ec3 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -1,12 +1,9 @@ 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: . From ffdc50806343099fe033b1a598e3b6c64e63919f Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:47:34 +0000 Subject: [PATCH 12/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index a6c1ec3..40995ff 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -28,7 +28,6 @@ describe("MeetingDataset / Meeting tests", () => { 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"); From 338e9c22925a0d3f49c90d9c30f0f35b7c1b079c Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:47:51 +0000 Subject: [PATCH 13/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 40995ff..0d14e47 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -32,7 +32,6 @@ describe("MeetingDataset / Meeting tests", () => { 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); From 8370142689be5666eaf7cc2c1599ea576b75c5c1 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:48:10 +0000 Subject: [PATCH 14/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 0d14e47..1f8f13b 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -39,8 +39,6 @@ describe("MeetingDataset / Meeting tests", () => { 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)); From 06b70a16e8d89f253ae6705a37df803dd1c95844 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:48:32 +0000 Subject: [PATCH 15/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 1f8f13b..6b57c50 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -86,12 +86,9 @@ describe("MeetingDataset / Meeting tests", () => { 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: . From e8f205d08d9242e29d9e2a3d8490e7fbbb87ec2f Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:48:49 +0000 Subject: [PATCH 16/32] Update src/solid/Meeting.ts Co-authored-by: Samu Lang --- src/solid/Meeting.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts index aebbf52..b003bc4 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -48,5 +48,4 @@ export class Meeting extends TermWrapper { set endDate(value: Date | undefined) { this.overwriteNullable(ICAL.dtend, value, TermMappings.dateToLiteral) } - } From 998a568c5e3b5dfe47233d372e67ef8dc7f78f86 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:51:02 +0000 Subject: [PATCH 17/32] Update src/solid/Meeting.ts Co-authored-by: Samu Lang --- src/solid/Meeting.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts index b003bc4..dc09411 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -1,7 +1,6 @@ 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) From 273f23c5d24b4fd9b01d7a2c639a878d324b66db Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:51:36 +0000 Subject: [PATCH 18/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 6b57c50..02dc76e 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -67,8 +67,6 @@ describe("MeetingDataset / Meeting tests", () => { 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)); From aa5a61b7ea59191966f0cea2ba7b99ce8f83f9e6 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:52:41 +0000 Subject: [PATCH 19/32] Update src/solid/Meeting.ts Co-authored-by: Matthieu Bosquet --- src/solid/Meeting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts index dc09411..8732aef 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -3,7 +3,7 @@ import { ICAL } from "../vocabulary/mod.js" export class MeetingDataset extends DatasetWrapper { get meeting(): Iterable { - return this.instancesOf(ICAL.vevent, Meeting) + return this.instancesOf(ICAL.Vevent, Meeting) } } From fd67108833e0d3e149413ab6426ddedbc3059572 Mon Sep 17 00:00:00 2001 From: _tanya_gray Date: Mon, 16 Feb 2026 09:53:39 +0000 Subject: [PATCH 20/32] Update test/unit/meeting.test.ts Co-authored-by: Samu Lang --- test/unit/meeting.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 02dc76e..c25340c 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -77,7 +77,6 @@ describe("MeetingDataset / Meeting tests", () => { 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"); From fcd9ed4a52a7184db05c7049caa31a32e58a94df Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 10:07:05 +0000 Subject: [PATCH 21/32] move MeetingDataset to own file --- src/solid/Meeting.ts | 6 ------ src/solid/MeetingDataset.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 src/solid/MeetingDataset.ts diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts index 8732aef..4a56be1 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -1,12 +1,6 @@ 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) diff --git a/src/solid/MeetingDataset.ts b/src/solid/MeetingDataset.ts new file mode 100644 index 0000000..dedccdb --- /dev/null +++ b/src/solid/MeetingDataset.ts @@ -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 { + return this.instancesOf(ICAL.Vevent, Meeting) + } +} From 9916e3d1221853fda8ec4d0e15d110702220b4f2 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 10:54:29 +0000 Subject: [PATCH 22/32] add meeting test cases --- src/solid/mod.ts | 1 + test/unit/meeting.test.ts | 100 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/solid/mod.ts b/src/solid/mod.ts index c7516c4..26f4dad 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -2,3 +2,4 @@ export * from "./Container.js" export * from "./ContainerDataset.js" export * from "./Resource.js" export * from "./Meeting.js" +export * from "./MeetingDataset.js" diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index c25340c..6706afc 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -126,4 +126,104 @@ describe("MeetingDataset / Meeting tests", () => { 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: . + @prefix xsd: . + + + 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: . + @prefix xsd: . + + + a cal:Vevent ; + cal:summary "Board Meeting" ; + cal:comment "Discuss quarterly earnings and projections." ; + 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: . + @prefix xsd: . + + + 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); + }); + + + + + }); From 5eb48d43bc3732f60388708cb4811c3fc952a63f Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 14:36:34 +0000 Subject: [PATCH 23/32] add Group and Person --- package.json | 3 +++ src/solid/Group.ts | 32 ++++++++++++++++++++++++++++++++ src/solid/GroupDataset.ts | 9 +++++++++ src/solid/Meeting.ts | 2 +- src/solid/Person.ts | 32 ++++++++++++++++++++++++++++++++ src/solid/mod.ts | 3 +++ src/vocabulary/mod.ts | 3 ++- src/vocabulary/owl.ts | 3 +++ src/vocabulary/vcard.ts | 7 ++++++- src/webid/Agent.ts | 2 +- 10 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/solid/Group.ts create mode 100644 src/solid/GroupDataset.ts create mode 100644 src/solid/Person.ts create mode 100644 src/vocabulary/owl.ts diff --git a/package.json b/package.json index d7bb8de..acf0f7f 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,16 @@ ], "license": "MIT", "dependencies": { + "@rdfjs/data-model": "^2.1.1", "@solid/object": "^0.4.0", + "rdf-namespaces": "^1.16.0", "rdfjs-wrapper": "^0.15.0" }, "devDependencies": { "@rdfjs/types": "^2", "@types/n3": "^1", "@types/node": "^24", + "@types/rdf-js": "^4.0.1", "n3": "^1", "typescript": "^5" }, diff --git a/src/solid/Group.ts b/src/solid/Group.ts new file mode 100644 index 0000000..831216e --- /dev/null +++ b/src/solid/Group.ts @@ -0,0 +1,32 @@ +import { TermMappings, ValueMappings, TermWrapper } from "rdfjs-wrapper" +import { VCARD } from "../vocabulary/mod.js" +import { Person } from "./Person.js" + +export class Group extends TermWrapper { + get name(): string | undefined { + const value = this.singularNullable(VCARD.fn, ValueMappings.literalToString) + + if (!value) { + throw new Error("Group must have a vcard:fn (name)") + } + return value + } + + set name(value: string | undefined) { + if (!value) { + throw new Error("Group name cannot be empty") + } + this.overwriteNullable(VCARD.fn, value, TermMappings.stringToLiteral) + } + + get members(): Set { + const individuals = new Set() + + for (const iri of this.objects(VCARD.member, ValueMappings.iriToString, TermMappings.stringToIri)) { + const individual = new Person(iri, this.dataset, this.factory) + individuals.add(individual) + } + return individuals + } + +} diff --git a/src/solid/GroupDataset.ts b/src/solid/GroupDataset.ts new file mode 100644 index 0000000..4bd8fbd --- /dev/null +++ b/src/solid/GroupDataset.ts @@ -0,0 +1,9 @@ +import { DatasetWrapper } from "rdfjs-wrapper" +import { VCARD } from "../vocabulary/mod.js" +import { Group } from "./Group.js" + +export class GroupDataset extends DatasetWrapper { + get group(): Iterable { + return this.instancesOf(VCARD.Group, Group) + } +} diff --git a/src/solid/Meeting.ts b/src/solid/Meeting.ts index 4a56be1..15087d4 100644 --- a/src/solid/Meeting.ts +++ b/src/solid/Meeting.ts @@ -1,4 +1,4 @@ -import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" +import { TermMappings, ValueMappings, TermWrapper } from "rdfjs-wrapper" import { ICAL } from "../vocabulary/mod.js" export class Meeting extends TermWrapper { diff --git a/src/solid/Person.ts b/src/solid/Person.ts new file mode 100644 index 0000000..cda084f --- /dev/null +++ b/src/solid/Person.ts @@ -0,0 +1,32 @@ +import { TermWrapper, ValueMappings, TermMappings } from "rdfjs-wrapper" +import { VCARD } from "../vocabulary/mod.js" +import { rdf } from "rdf-namespaces" // or define RDF.type yourself + +export class Person extends TermWrapper { + constructor(term: any, dataset: any, factory?: any) { + super(term, dataset, factory) + if (!dataset.has(term, rdf.type, VCARD.Individual)) { + throw new Error(`Node ${term.value} is not a vcard:Individual`) + } + } + + get name(): string | undefined { + return this.singularNullable(VCARD.fn, ValueMappings.literalToString) + } + + set name(value: string | undefined) { + if (!value) throw new Error("Individual name cannot be empty") + this.overwriteNullable(VCARD.fn, value, TermMappings.stringToLiteral) + } + + + + get webId(): string | undefined { + return this.singularNullable(VCARD.hasURL, ValueMappings.iriToString) + } + + set webId(value: string | undefined) { + if (!value) return + this.overwriteNullable(VCARD.hasURL, value, TermMappings.stringToIri) + } +} diff --git a/src/solid/mod.ts b/src/solid/mod.ts index 26f4dad..25f6d6c 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -3,3 +3,6 @@ export * from "./ContainerDataset.js" export * from "./Resource.js" export * from "./Meeting.js" export * from "./MeetingDataset.js" +export * from "./Group.js" +export * from "./GroupDataset.js" +export * from "./Person.js" \ No newline at end of file diff --git a/src/vocabulary/mod.ts b/src/vocabulary/mod.ts index 4afb431..fd5b62c 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -1,11 +1,12 @@ export * from "./acp.js" export * from "./dc.js" export * from "./foaf.js" +export * from "./ical.js" export * from "./ldp.js" +export * from "./owl.js" export * from "./pim.js" export * from "./posix.js" export * from "./rdf.js" export * from "./rdfs.js" export * from "./solid.js" export * from "./vcard.js" -export * from "./ical.js" diff --git a/src/vocabulary/owl.ts b/src/vocabulary/owl.ts new file mode 100644 index 0000000..ab28ec0 --- /dev/null +++ b/src/vocabulary/owl.ts @@ -0,0 +1,3 @@ +export const OWL = { + sameAs: "http://www.w3.org/2002/07/owl#", +} as const; diff --git a/src/vocabulary/vcard.ts b/src/vocabulary/vcard.ts index 99ae2d3..916b0c8 100644 --- a/src/vocabulary/vcard.ts +++ b/src/vocabulary/vcard.ts @@ -6,7 +6,12 @@ export const VCARD = { hasPhoto: "http://www.w3.org/2006/vcard/ns#hasPhoto", hasTelephone: "http://www.w3.org/2006/vcard/ns#hasTelephone", title: "http://www.w3.org/2006/vcard/ns#title", - hasUrl: "http://www.w3.org/2006/vcard/ns#hasUrl", + hasURL: "http://www.w3.org/2006/vcard/ns#hasURL", organizationName: "http://www.w3.org/2006/vcard/ns#organization-name", role: "http://www.w3.org/2006/vcard/ns#organization-name", + Group: "http://www.w3.org/2006/vcard/ns#Group", + Individual: "http://www.w3.org/2006/vcard/ns#Individual", + member: "http://www.w3.org/2006/vcard/ns#member", + phone: "http://www.w3.org/2006/vcard/ns#phone", + email: "http://www.w3.org/2006/vcard/ns#email", } as const; diff --git a/src/webid/Agent.ts b/src/webid/Agent.ts index d0d6d82..e60b5df 100644 --- a/src/webid/Agent.ts +++ b/src/webid/Agent.ts @@ -7,7 +7,7 @@ export class Agent extends TermWrapper { } get vcardHasUrl(): string | undefined { - return this.singularNullable(VCARD.hasUrl, ValueMappings.iriToString) + return this.singularNullable(VCARD.hasURL, ValueMappings.iriToString) } get organization(): string | null { From 914aaae32e1e323c1bedf7474ad9dc2735315d05 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 14:38:13 +0000 Subject: [PATCH 24/32] update spacing --- src/solid/Person.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/solid/Person.ts b/src/solid/Person.ts index cda084f..ea14053 100644 --- a/src/solid/Person.ts +++ b/src/solid/Person.ts @@ -19,8 +19,6 @@ export class Person extends TermWrapper { this.overwriteNullable(VCARD.fn, value, TermMappings.stringToLiteral) } - - get webId(): string | undefined { return this.singularNullable(VCARD.hasURL, ValueMappings.iriToString) } From 5898e77bb862e6fb7788cd6c340c11a286f65e20 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 18:43:40 +0000 Subject: [PATCH 25/32] update Group and Person --- src/solid/Group.ts | 13 +++++-------- src/solid/Person.ts | 27 ++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/solid/Group.ts b/src/solid/Group.ts index 831216e..7b0db9b 100644 --- a/src/solid/Group.ts +++ b/src/solid/Group.ts @@ -3,12 +3,10 @@ import { VCARD } from "../vocabulary/mod.js" import { Person } from "./Person.js" export class Group extends TermWrapper { + get name(): string | undefined { const value = this.singularNullable(VCARD.fn, ValueMappings.literalToString) - if (!value) { - throw new Error("Group must have a vcard:fn (name)") - } return value } @@ -20,13 +18,12 @@ export class Group extends TermWrapper { } get members(): Set { - const individuals = new Set() + const persons = new Set() for (const iri of this.objects(VCARD.member, ValueMappings.iriToString, TermMappings.stringToIri)) { - const individual = new Person(iri, this.dataset, this.factory) - individuals.add(individual) + const person = new Person(iri, this.dataset, this.factory) + persons.add(person) } - return individuals + return persons } - } diff --git a/src/solid/Person.ts b/src/solid/Person.ts index ea14053..334285e 100644 --- a/src/solid/Person.ts +++ b/src/solid/Person.ts @@ -1,8 +1,9 @@ import { TermWrapper, ValueMappings, TermMappings } from "rdfjs-wrapper" -import { VCARD } from "../vocabulary/mod.js" -import { rdf } from "rdf-namespaces" // or define RDF.type yourself +import { VCARD, OWL } from "../vocabulary/mod.js" +import { rdf } from "rdf-namespaces" export class Person extends TermWrapper { + constructor(term: any, dataset: any, factory?: any) { super(term, dataset, factory) if (!dataset.has(term, rdf.type, VCARD.Individual)) { @@ -15,16 +16,32 @@ export class Person extends TermWrapper { } set name(value: string | undefined) { - if (!value) throw new Error("Individual name cannot be empty") this.overwriteNullable(VCARD.fn, value, TermMappings.stringToLiteral) } + get phone(): string | undefined { + return this.singularNullable(VCARD.phone, ValueMappings.literalToString) + } + + set phone(value: string | undefined) { + this.overwriteNullable(VCARD.phone, value, TermMappings.stringToLiteral) + } + + get email(): string | undefined { + return this.singularNullable(VCARD.email, ValueMappings.literalToString) + } + + set email(value: string | undefined) { + this.overwriteNullable(VCARD.email, value, TermMappings.stringToLiteral) + } + + get webId(): string | undefined { - return this.singularNullable(VCARD.hasURL, ValueMappings.iriToString) + return this.singularNullable(OWL.sameAs, ValueMappings.iriToString) } set webId(value: string | undefined) { if (!value) return - this.overwriteNullable(VCARD.hasURL, value, TermMappings.stringToIri) + this.overwriteNullable(OWL.sameAs, value, TermMappings.stringToIri) } } From dfb67d1dc70f1df1ef79793c20583ca89c8048f4 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 18:44:51 +0000 Subject: [PATCH 26/32] update meeting test --- test/unit/meeting.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 6706afc..3ce84f2 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -168,8 +168,8 @@ describe("MeetingDataset / Meeting tests", () => { a cal:Vevent ; - cal:summary "Board Meeting" ; - cal:comment "Discuss quarterly earnings and projections." ; + 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 . From 563b8ab14e312a7534dbfa5917ccb7242b43de53 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 18:49:27 +0000 Subject: [PATCH 27/32] add addMember and deleteMember to group --- src/solid/Group.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/solid/Group.ts b/src/solid/Group.ts index 7b0db9b..6bd042c 100644 --- a/src/solid/Group.ts +++ b/src/solid/Group.ts @@ -26,4 +26,30 @@ export class Group extends TermWrapper { } return persons } + + /** Add a new Person to this group */ + addMember(person: Person) { + // Convert Person term to string IRI + const iri = person.term.value + + // Use objects() to get the live set and add the new member + const membersSet = this.objects( + VCARD.member, + ValueMappings.iriToString, + TermMappings.stringToIri + ) + membersSet.add(iri) + } + + /** Remove a Person from this group */ + deleteMember(person: Person) { + const iri = person.term.value + + const membersSet = this.objects( + VCARD.member, + ValueMappings.iriToString, + TermMappings.stringToIri + ) + membersSet.delete(iri) + } } From 2df46f3aa67792c00f2493ee43c963e4f112a9ab Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Mon, 16 Feb 2026 19:12:19 +0000 Subject: [PATCH 28/32] modify group test --- src/solid/Person.ts | 12 +++-- test/unit/group.test.ts | 106 ++++++++++++++++++++++++++++++++++++++ test/unit/meeting.test.ts | 4 +- 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/unit/group.test.ts diff --git a/src/solid/Person.ts b/src/solid/Person.ts index 334285e..314635a 100644 --- a/src/solid/Person.ts +++ b/src/solid/Person.ts @@ -4,10 +4,14 @@ import { rdf } from "rdf-namespaces" export class Person extends TermWrapper { - constructor(term: any, dataset: any, factory?: any) { - super(term, dataset, factory) - if (!dataset.has(term, rdf.type, VCARD.Individual)) { - throw new Error(`Node ${term.value} is not a vcard:Individual`) + constructor(term: string | any, dataset: any, factory?: any) { + // Convert string to NamedNode if needed + const t = typeof term === "string" ? (factory || dataset.factory).namedNode(term) : term + super(t, dataset, factory) + + // ✅ Always declare as vcard:Individual + if (!dataset.has(this.term, rdf.type, VCARD.Individual)) { + dataset.add((factory || dataset.factory).quad(this.term, rdf.type, VCARD.Individual)) } } diff --git a/test/unit/group.test.ts b/test/unit/group.test.ts new file mode 100644 index 0000000..43c2e70 --- /dev/null +++ b/test/unit/group.test.ts @@ -0,0 +1,106 @@ +import { DataFactory, Parser, Store } from "n3" +import assert from "node:assert" +import { describe, it } from "node:test" + +import { GroupDataset } from "@solid/object" +import { Person } from "@solid/object" + + +// Sample RDF for an existing group with two members + + /* +const sampleRDF = ` +@prefix vcard: . + + a vcard:Group ; + vcard:fn "Engineering Team" ; + vcard:member ; + vcard:member . +` + +describe("GroupDataset / Group tests", () => { + + + it("should parse a group and retrieve its properties", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const dataset = new GroupDataset(store, DataFactory) + const groups = Array.from(dataset.group) + assert.ok(groups.length > 0, "No groups found") + + const group = groups[0]! + assert.equal(group.name, "Engineering Team") + + const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) + assert.ok(memberIRIs.includes("https://example.org/person/alice")) + assert.ok(memberIRIs.includes("https://example.org/person/bob")) + }) + + + it("should allow changing group name", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const dataset = new GroupDataset(store, DataFactory) + const group = Array.from(dataset.group)[0]! + + group.name = "Product Team" + assert.equal(group.name, "Product Team") + }) + + + it("should allow adding a new member", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const dataset = new GroupDataset(store, DataFactory) + const group = Array.from(dataset.group)[0]! + + // Create new person (automatically typed as vcard:Individual) + const charlie = new Person("https://example.org/person/charlie", store, DataFactory) + charlie.name = "Charlie" + + group.addMember(charlie) + + const memberNames = Array.from(group.members).map((m: Person) => m.name) + assert.ok(memberNames.includes("Charlie")) + assert.ok(memberNames.includes("Alice") || memberNames.includes("Bob")) + }) + it("should allow removing a member", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const dataset = new GroupDataset(store, DataFactory) + const group = Array.from(dataset.group)[0]! + + const alice = new Person("https://example.org/person/alice", store, DataFactory) + group.deleteMember(alice) + + const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) + assert.ok(!memberIRIs.includes("https://example.org/person/alice")) + assert.ok(memberIRIs.includes("https://example.org/person/bob")) + }) + + it("should reflect live changes in the dataset", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const dataset = new GroupDataset(store, DataFactory) + const group = Array.from(dataset.group)[0]! + + const dave = new Person("https://example.org/person/dave", store, DataFactory) + group.addMember(dave) + + const bob = new Person("https://example.org/person/bob", store, DataFactory) + group.deleteMember(bob) + + const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) + assert.deepEqual(memberIRIs.sort(), [ + "https://example.org/person/alice", + "https://example.org/person/dave" + ]) + }) + +}) + */ \ No newline at end of file diff --git a/test/unit/meeting.test.ts b/test/unit/meeting.test.ts index 6706afc..3ce84f2 100644 --- a/test/unit/meeting.test.ts +++ b/test/unit/meeting.test.ts @@ -168,8 +168,8 @@ describe("MeetingDataset / Meeting tests", () => { a cal:Vevent ; - cal:summary "Board Meeting" ; - cal:comment "Discuss quarterly earnings and projections." ; + 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 . From 7fd2a480506c32465c696120fe04ce50d519dc55 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Tue, 17 Feb 2026 10:55:49 +0000 Subject: [PATCH 29/32] add person and orgnization contacts --- src/solid/Organization.ts | 67 ++++++++++++++++++++++++++++++++ src/solid/OrganizationDataset.ts | 9 +++++ src/solid/Person.ts | 2 +- src/solid/PersonDataset.ts | 9 +++++ src/vocabulary/foaf.ts | 1 + src/vocabulary/mod.ts | 1 + src/vocabulary/schema.ts | 11 ++++++ src/vocabulary/vcard.ts | 16 ++++---- 8 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 src/solid/Organization.ts create mode 100644 src/solid/OrganizationDataset.ts create mode 100644 src/solid/PersonDataset.ts create mode 100644 src/vocabulary/schema.ts diff --git a/src/solid/Organization.ts b/src/solid/Organization.ts new file mode 100644 index 0000000..cd9c035 --- /dev/null +++ b/src/solid/Organization.ts @@ -0,0 +1,67 @@ +import { TermWrapper, ValueMappings, TermMappings } from "rdfjs-wrapper" +import { VCARD, SCHEMA, RDF } from "../vocabulary/mod.js" + +type SchemaOrganizationType = typeof SCHEMA[keyof typeof SCHEMA] + +const allowedOrgTypes = new Set([ + SCHEMA.Corporation, + SCHEMA.EducationalOrganization, + SCHEMA.GovernmentOrganization, + SCHEMA.NGO, + SCHEMA.PerformingGroup, + SCHEMA.Project, + SCHEMA.SportsOrganization, +]) + +export class Organization extends TermWrapper { + + constructor(term: string | any, dataset: any, factory?: any) { + // Convert string to NamedNode if needed + const t = typeof term === "string" ? (factory || dataset.factory).namedNode(term) : term + super(t, dataset, factory) + + // Always declare as vcard:Organization + if (!dataset.has(this.term, RDF.type, VCARD.Organization)) { + dataset.add((factory || dataset.factory).quad(this.term, RDF.type, VCARD.Organization)) + } + } + + get name(): string | undefined { + return this.singularNullable(SCHEMA.name, ValueMappings.literalToString) + } + + set name(value: string | undefined) { + this.overwriteNullable(SCHEMA.name, value, TermMappings.stringToLiteral) + } + + get url(): string | undefined { + return this.singularNullable(SCHEMA.url, ValueMappings.iriToString) + } + + set url(value: string | undefined) { + this.overwriteNullable(SCHEMA.url, value, TermMappings.stringToIri) + } + + get types(): Set { + const orgTypes = new Set() + for (const iri of this.objects(RDF.type, ValueMappings.iriToString, TermMappings.stringToIri)) { + orgTypes.add(iri) + } + return orgTypes + } + + /** Add a new type for this organization */ + addType(orgType: SchemaOrganizationType): void { + if (!allowedOrgTypes.has(orgType)) { + throw new Error(`Invalid organization type: ${orgType}`) + } + const types = this.objects( + RDF.type, + ValueMappings.iriToString, + TermMappings.stringToIri + ) + types.add(orgType) + } + + +} diff --git a/src/solid/OrganizationDataset.ts b/src/solid/OrganizationDataset.ts new file mode 100644 index 0000000..d8dfa3c --- /dev/null +++ b/src/solid/OrganizationDataset.ts @@ -0,0 +1,9 @@ +import { DatasetWrapper } from "rdfjs-wrapper" +import { VCARD } from "../vocabulary/mod.js" +import { Organization } from "./Organization.js" + +export class OrganizationDataset extends DatasetWrapper { + get person(): Iterable { + return this.instancesOf(VCARD.Individual, Organization) + } +} diff --git a/src/solid/Person.ts b/src/solid/Person.ts index 314635a..b377e77 100644 --- a/src/solid/Person.ts +++ b/src/solid/Person.ts @@ -9,7 +9,7 @@ export class Person extends TermWrapper { const t = typeof term === "string" ? (factory || dataset.factory).namedNode(term) : term super(t, dataset, factory) - // ✅ Always declare as vcard:Individual + // Always declare as vcard:Individual if (!dataset.has(this.term, rdf.type, VCARD.Individual)) { dataset.add((factory || dataset.factory).quad(this.term, rdf.type, VCARD.Individual)) } diff --git a/src/solid/PersonDataset.ts b/src/solid/PersonDataset.ts new file mode 100644 index 0000000..7ec3bb5 --- /dev/null +++ b/src/solid/PersonDataset.ts @@ -0,0 +1,9 @@ +import { DatasetWrapper } from "rdfjs-wrapper" +import { VCARD } from "../vocabulary/mod.js" +import { Person } from "./Person.js" + +export class PersonDataset extends DatasetWrapper { + get person(): Iterable { + return this.instancesOf(VCARD.Individual, Person) + } +} diff --git a/src/vocabulary/foaf.ts b/src/vocabulary/foaf.ts index c375e39..2eda74c 100644 --- a/src/vocabulary/foaf.ts +++ b/src/vocabulary/foaf.ts @@ -5,4 +5,5 @@ export const FOAF = { email: "http://xmlns.com/foaf/0.1/email", homepage: "http://xmlns.com/foaf/0.1/homepage", knows: "http://xmlns.com/foaf/0.1/knows", + Person: "http://xmlns.com/foaf/0.1/Person", } as const; diff --git a/src/vocabulary/mod.ts b/src/vocabulary/mod.ts index fd5b62c..c2921a3 100644 --- a/src/vocabulary/mod.ts +++ b/src/vocabulary/mod.ts @@ -10,3 +10,4 @@ export * from "./rdf.js" export * from "./rdfs.js" export * from "./solid.js" export * from "./vcard.js" +export * from "./schema.js" diff --git a/src/vocabulary/schema.ts b/src/vocabulary/schema.ts new file mode 100644 index 0000000..c703657 --- /dev/null +++ b/src/vocabulary/schema.ts @@ -0,0 +1,11 @@ +export const SCHEMA = { + Corporation: "https://schema.org/Corporation", + EducationalOrganization: "https://schema.org/EducationalOrganization", + GovernmentOrganization: "https://schema.org/GovernmentOrganization", + NGO: "https://schema.org/NGO", + PerformingGroup: "https://schema.org/PerformingGroup", + Project: "https://schema.org/Project", + SportsOrganization: "https://schema.org/SportsOrganization", + name: "https://schema.org/name", + url: "https://schema.org/url", +} as const; diff --git a/src/vocabulary/vcard.ts b/src/vocabulary/vcard.ts index 916b0c8..a6e58a2 100644 --- a/src/vocabulary/vcard.ts +++ b/src/vocabulary/vcard.ts @@ -1,17 +1,17 @@ - export const VCARD = { + Group: "http://www.w3.org/2006/vcard/ns#Group", + Individual: "http://www.w3.org/2006/vcard/ns#Individual", + Organization: "http://www.w3.org/2006/vcard/ns#Organization", + email: "http://www.w3.org/2006/vcard/ns#email", fn: "http://www.w3.org/2006/vcard/ns#fn", hasEmail: "http://www.w3.org/2006/vcard/ns#hasEmail", - hasValue: "http://www.w3.org/2006/vcard/ns#hasValue", hasPhoto: "http://www.w3.org/2006/vcard/ns#hasPhoto", hasTelephone: "http://www.w3.org/2006/vcard/ns#hasTelephone", - title: "http://www.w3.org/2006/vcard/ns#title", hasURL: "http://www.w3.org/2006/vcard/ns#hasURL", - organizationName: "http://www.w3.org/2006/vcard/ns#organization-name", - role: "http://www.w3.org/2006/vcard/ns#organization-name", - Group: "http://www.w3.org/2006/vcard/ns#Group", - Individual: "http://www.w3.org/2006/vcard/ns#Individual", + hasValue: "http://www.w3.org/2006/vcard/ns#hasValue", member: "http://www.w3.org/2006/vcard/ns#member", + organizationName: "http://www.w3.org/2006/vcard/ns#organization-name", phone: "http://www.w3.org/2006/vcard/ns#phone", - email: "http://www.w3.org/2006/vcard/ns#email", + role: "http://www.w3.org/2006/vcard/ns#organization-name", + title: "http://www.w3.org/2006/vcard/ns#title", } as const; From 1645284f7a91cea2bcfc9734ee1a583867ac639b Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Tue, 17 Feb 2026 11:13:17 +0000 Subject: [PATCH 30/32] add org and person tests --- src/solid/mod.ts | 3 +- test/tsconfig.json | 2 +- test/unit/organization.test.ts | 97 ++++++++++++++++++++++++++++++++++ test/unit/person.test.ts | 44 +++++++++++++++ 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 test/unit/organization.test.ts create mode 100644 test/unit/person.test.ts diff --git a/src/solid/mod.ts b/src/solid/mod.ts index 25f6d6c..1dae7d9 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -5,4 +5,5 @@ export * from "./Meeting.js" export * from "./MeetingDataset.js" export * from "./Group.js" export * from "./GroupDataset.js" -export * from "./Person.js" \ No newline at end of file +export * from "./Person.js" +export * from "./Organization.js" \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json index 79863ec..1536481 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./dist/", - "rootDir": ".", + "rootDir": "/", "paths": { "@solid/object": ["../dist/mod.d.ts"] } diff --git a/test/unit/organization.test.ts b/test/unit/organization.test.ts new file mode 100644 index 0000000..c01a232 --- /dev/null +++ b/test/unit/organization.test.ts @@ -0,0 +1,97 @@ +import { DataFactory, Parser, Store } from "n3"; +import assert from "node:assert"; +import { describe, it } from "node:test"; + +import { Organization } from "@solid/object"; +import {VCARD, SCHEMA, RDF} from "../../src/vocabulary/mod" + + +describe("Organization class tests", () => { + const sampleRDF = ` +@prefix vcard: . +@prefix schema: . +@prefix rdf: . + + a vcard:Organization ; + schema:name "Example Corp" ; + schema:url "https://example.org" ; + rdf:type schema:Corporation . +`; + + it("should parse Organization properties", () => { + const store = new Store(); + store.addQuads(new Parser().parse(sampleRDF)); + + const org = new Organization("https://example.org/org/1", store); + + assert.strictEqual(org.name, "Example Corp"); + assert.strictEqual(org.url, "https://example.org"); + assert.ok(org.types.has(VCARD.Organization), "Base type should be vcard:Organization"); + assert.ok(org.types.has(SCHEMA.Corporation), "Should include Corporation type"); + }); + + it("should allow setting Organization properties", () => { + const store = new Store(); + const org = new Organization("https://example.org/org/2", store); + + org.name = "New Org"; + org.url = "https://new.org"; + org.addType(SCHEMA.NGO); + + assert.strictEqual(org.name, "New Org"); + assert.strictEqual(org.url, "https://new.org"); + assert.ok(org.types.has(VCARD.Organization)); + assert.ok(org.types.has(SCHEMA.NGO)); + }); + + it("should enforce allowed organization types (runtime check)", () => { + const store = new Store(); + const org = new Organization("https://example.org/org/3", store); + + assert.throws(() => { + // bypass TS type safety for test + org.addType("https://schema.org/InvalidType" as any); + }, /Invalid organization type/); + }); + + it("should handle multiple types correctly", () => { + const store = new Store(); + const org = new Organization("https://example.org/org/4", store); + + org.addType(SCHEMA.Corporation); + org.addType(SCHEMA.NGO); + + const typesArray = Array.from(org.types); + assert.ok(typesArray.includes(VCARD.Organization)); + assert.ok(typesArray.includes(SCHEMA.Corporation)); + assert.ok(typesArray.includes(SCHEMA.NGO)); + + // Ensure no duplicates + const uniqueTypes = new Set(typesArray); + assert.strictEqual(uniqueTypes.size, typesArray.length); + }); + + it("should allow updating name and url multiple times", () => { + const store = new Store(); + const org = new Organization("https://example.org/org/5", store); + + org.name = "Org A"; + org.url = "https://orga.org"; + + org.name = "Org B"; + org.url = "https://orgb.org"; + + assert.strictEqual(org.name, "Org B"); + assert.strictEqual(org.url, "https://orgb.org"); + }); + + it("should handle minimal organization (only base type)", () => { + const store = new Store(); + const org = new Organization("https://example.org/org/minimal", store); + + assert.strictEqual(org.name, undefined); + assert.strictEqual(org.url, undefined); + assert.ok(org.types.has(VCARD.Organization)); + assert.strictEqual(org.types.size, 1); + }); +}); diff --git a/test/unit/person.test.ts b/test/unit/person.test.ts new file mode 100644 index 0000000..7bf0abf --- /dev/null +++ b/test/unit/person.test.ts @@ -0,0 +1,44 @@ +import { DataFactory, Parser, Store } from "n3"; +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { Person } from "@solid/object"; + +describe("Person class tests", () => { + const sampleRDF = ` +@prefix vcard: . +@prefix owl: . + + a vcard:Individual ; + vcard:fn "Alice Smith" ; + vcard:email "alice@example.org" ; + vcard:phone "+123456789" ; + owl:sameAs . +`; + + it("should parse Person properties", () => { + const store = new Store(); + store.addQuads(new Parser().parse(sampleRDF)); + + const person = new Person("https://example.org/person/1", store); + + assert.strictEqual(person.name, "Alice Smith"); + assert.strictEqual(person.email, "alice@example.org"); + assert.strictEqual(person.phone, "+123456789"); + assert.strictEqual(person.webId, "https://alice.example.org/#me"); + }); + + it("should allow setting Person properties", () => { + const store = new Store(); + const person = new Person("https://example.org/person/1", store); + + person.name = "Bob Jones"; + person.email = "bob@example.org"; + person.phone = "+987654321"; + person.webId = "https://bob.example.org/#me"; + + assert.strictEqual(person.name, "Bob Jones"); + assert.strictEqual(person.email, "bob@example.org"); + assert.strictEqual(person.phone, "+987654321"); + assert.strictEqual(person.webId, "https://bob.example.org/#me"); + }); +}); From cf6ba26057a86d45d34c93a0185927ede2b56f76 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Thu, 19 Feb 2026 00:32:03 +0000 Subject: [PATCH 31/32] update personal profile object --- src/solid/Profile.ts | 69 +++++++++++ src/solid/ProfileDataset.ts | 10 ++ src/solid/mod.ts | 4 +- src/webid/Profile.ts | 224 ------------------------------------ test/unit/profile.test.ts | 150 ++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 225 deletions(-) create mode 100644 src/solid/Profile.ts create mode 100644 src/solid/ProfileDataset.ts delete mode 100644 src/webid/Profile.ts create mode 100644 test/unit/profile.test.ts diff --git a/src/solid/Profile.ts b/src/solid/Profile.ts new file mode 100644 index 0000000..843eacb --- /dev/null +++ b/src/solid/Profile.ts @@ -0,0 +1,69 @@ +import { TermMappings, ValueMappings, TermWrapper, DatasetWrapper } from "rdfjs-wrapper" +import { FOAF, SOLID, SCHEMA, ORG, VCARD } from "../vocabulary/mod.js" +import { Person } from "@solid/object" + + +export class Profile extends TermWrapper { + + /* 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 */ + + // Preferred pronoun for subject role (he/she/they + get preferredSubjectPronoun(): string | undefined { + return this.singularNullable(SOLID.preferredSubjectPronoun, ValueMappings.literalToString) + } + set preferredSubjectPronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredSubjectPronoun, value, TermMappings.stringToLiteral) + } + + // Preferred pronoun for object role (him/her/them) + get preferredObjectPronoun(): string | undefined { + return this.singularNullable(SOLID.preferredObjectPronoun, ValueMappings.literalToString) + } + set preferredObjectPronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredObjectPronoun, value, TermMappings.stringToLiteral) + } + + // Preferred relative pronoun (his/hers/theirs) + get preferredRelativePronoun(): string | undefined { + return this.singularNullable(SOLID.preferredRelativePronoun, ValueMappings.literalToString) + } + set preferredRelativePronoun(value: string | undefined) { + this.overwriteNullable(SOLID.preferredRelativePronoun, value, TermMappings.stringToLiteral) + } + + + /* Roles / Organization involvement */ + + get roles(): Set { + const persons = new Set() + + for (const iri of this.objects(ORG.member, ValueMappings.iriToString, TermMappings.stringToIri)) { + const person = new Person(iri, this.dataset, this.factory) + persons.add(person) + } + return persons + } + + /* Skills */ + get skills(): Set { + return this.objects(SCHEMA.skills, ValueMappings.iriToString, TermMappings.stringToIri) + } + + /* Languages */ + get languages(): Set { + return this.objects(SCHEMA.knowsLanguage, ValueMappings.iriToString, TermMappings.stringToIri) + } + + /* Online/ Social Media Accounts */ + get accounts(): Set { + return this.objects(FOAF.account, ValueMappings.iriToString, TermMappings.stringToIri) + } +} diff --git a/src/solid/ProfileDataset.ts b/src/solid/ProfileDataset.ts new file mode 100644 index 0000000..fd4f8a8 --- /dev/null +++ b/src/solid/ProfileDataset.ts @@ -0,0 +1,10 @@ +import { DatasetWrapper } from "rdfjs-wrapper" +import { FOAF } from "../vocabulary/mod.js" +import { Profile } from "./Profile.js" + +export class ProfileDataset extends DatasetWrapper { + get profile(): Iterable { + return this.instancesOf(FOAF.PersonalProfileDocument, Profile) + } +} + diff --git a/src/solid/mod.ts b/src/solid/mod.ts index 36d1929..c70236c 100644 --- a/src/solid/mod.ts +++ b/src/solid/mod.ts @@ -1,4 +1,6 @@ export * from "./Container.js" export * from "./ContainerDataset.js" export * from "./Resource.js" -export * from "./Meeting.js" \ No newline at end of file +export * from "./Meeting.js" +export * from "./Person.js" +export * from "./Profile.js" \ No newline at end of file diff --git a/src/webid/Profile.ts b/src/webid/Profile.ts deleted file mode 100644 index a906a6f..0000000 --- a/src/webid/Profile.ts +++ /dev/null @@ -1,224 +0,0 @@ -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/test/unit/profile.test.ts b/test/unit/profile.test.ts new file mode 100644 index 0000000..300163e --- /dev/null +++ b/test/unit/profile.test.ts @@ -0,0 +1,150 @@ +import { DataFactory, Parser, Store } from "n3" +import assert from "node:assert" +import { describe, it } from "node:test" + +import { Profile } from "../profile.js" +import { Person } from "@solid/object" + +describe("Profile tests", () => { + + const sampleRDF = ` +@prefix foaf: . +@prefix solid: . +@prefix schema: . +@prefix org: . + + + foaf:nick "alex" ; + solid:preferredSubjectPronoun "they" ; + solid:preferredObjectPronoun "them" ; + solid:preferredRelativePronoun "theirs" ; + schema:skills ; + schema:skills ; + schema:knowsLanguage ; + schema:knowsLanguage ; + foaf:account ; + foaf:account ; + org:member . +`; + + it("should parse and retrieve profile properties", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const profile = new Profile( + DataFactory.namedNode("https://example.org/profile#me"), + store, + DataFactory + ) + + // Singular string properties + assert.equal(profile.nickname, "alex") + assert.equal(profile.preferredSubjectPronoun, "they") + assert.equal(profile.preferredObjectPronoun, "them") + assert.equal(profile.preferredRelativePronoun, "theirs") + + // Set-based properties + assert.ok(profile.skills instanceof Set) + assert.ok(profile.languages instanceof Set) + assert.ok(profile.accounts instanceof Set) + assert.ok(profile.roles instanceof Set) + + assert.ok(profile.skills.has("https://example.org/skills/TypeScript")) + assert.ok(profile.languages.has("https://example.org/lang/en")) + assert.ok(profile.accounts.has("https://github.com/example")) + + const role = Array.from(profile.roles)[0] + assert.ok(role instanceof Person) + }) + + + it("should allow setting of singular properties", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const profile = new Profile( + DataFactory.namedNode("https://example.org/profile#me"), + store, + DataFactory + ) + + profile.nickname = "updatedNick" + profile.preferredSubjectPronoun = "she" + profile.preferredObjectPronoun = "her" + profile.preferredRelativePronoun = "hers" + + assert.equal(profile.nickname, "updatedNick") + assert.equal(profile.preferredSubjectPronoun, "she") + assert.equal(profile.preferredObjectPronoun, "her") + assert.equal(profile.preferredRelativePronoun, "hers") + }) + + + it("should ensure singular properties are correct type", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const profile = new Profile( + DataFactory.namedNode("https://example.org/profile#me"), + store, + DataFactory + ) + + assert.equal(typeof profile.nickname, "string") + assert.equal(typeof profile.preferredSubjectPronoun, "string") + assert.equal(typeof profile.preferredObjectPronoun, "string") + assert.equal(typeof profile.preferredRelativePronoun, "string") + }) + + + it("should ensure set properties return Sets and not arrays", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) + + const profile = new Profile( + DataFactory.namedNode("https://example.org/profile#me"), + store, + DataFactory + ) + + assert.ok(profile.skills instanceof Set) + assert.ok(profile.languages instanceof Set) + assert.ok(profile.accounts instanceof Set) + assert.ok(profile.roles instanceof Set) + + assert.ok(!Array.isArray(profile.skills)) + assert.ok(!Array.isArray(profile.languages)) + assert.ok(!Array.isArray(profile.accounts)) + assert.ok(!Array.isArray(profile.roles)) + }) + + + it("should handle duplicate singular values by exposing only one", () => { + const duplicateRDF = ` +@prefix foaf: . +@prefix solid: . + + + foaf:nick "alex" ; + foaf:nick "duplicateNick" ; + solid:preferredSubjectPronoun "they" ; + solid:preferredSubjectPronoun "duplicatePronoun" . +` + + const store = new Store() + store.addQuads(new Parser().parse(duplicateRDF)) + + const profile = new Profile( + DataFactory.namedNode("https://example.org/profile#me"), + store, + DataFactory + ) + + assert.equal(typeof profile.nickname, "string") + assert.equal(typeof profile.preferredSubjectPronoun, "string") + + assert.ok(!Array.isArray(profile.nickname)) + assert.ok(!Array.isArray(profile.preferredSubjectPronoun)) + }) + +}) From f8abd16fcf012d0c9f68a2e94b6070f1b5ae28d5 Mon Sep 17 00:00:00 2001 From: Tanya Gray Date: Thu, 19 Feb 2026 09:02:48 +0000 Subject: [PATCH 32/32] update unit tests --- test/unit/group.test.ts | 193 +++++++++++++++++++++++---------- test/unit/organization.test.ts | 108 ++++++------------ test/unit/person.test.ts | 70 ++++++------ test/unit/profile.test.ts | 127 +++++----------------- 4 files changed, 230 insertions(+), 268 deletions(-) diff --git a/test/unit/group.test.ts b/test/unit/group.test.ts index 43c2e70..819c9dd 100644 --- a/test/unit/group.test.ts +++ b/test/unit/group.test.ts @@ -2,105 +2,178 @@ import { DataFactory, Parser, Store } from "n3" import assert from "node:assert" import { describe, it } from "node:test" -import { GroupDataset } from "@solid/object" -import { Person } from "@solid/object" +import { Group, Person } from "@solid/object"; -// Sample RDF for an existing group with two members - /* -const sampleRDF = ` + +describe("Group tests", () => { + + const sampleRDF = ` @prefix vcard: . - a vcard:Group ; + + a vcard:Group ; vcard:fn "Engineering Team" ; - vcard:member ; - vcard:member . -` + vcard:member ; + vcard:member . +`; -describe("GroupDataset / Group tests", () => { - - - it("should parse a group and retrieve its properties", () => { + it("should parse and retrieve group name", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) - const dataset = new GroupDataset(store, DataFactory) - const groups = Array.from(dataset.group) - assert.ok(groups.length > 0, "No groups found") + const group = new Group( + DataFactory.namedNode("https://example.org/group/1"), + store, + DataFactory + ) - const group = groups[0]! assert.equal(group.name, "Engineering Team") - - const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) - assert.ok(memberIRIs.includes("https://example.org/person/alice")) - assert.ok(memberIRIs.includes("https://example.org/person/bob")) + assert.equal(typeof group.name, "string") }) - - it("should allow changing group name", () => { + it("should allow setting group name", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) - const dataset = new GroupDataset(store, DataFactory) - const group = Array.from(dataset.group)[0]! + const group = new Group( + DataFactory.namedNode("https://example.org/group/1"), + store, + DataFactory + ) - group.name = "Product Team" - assert.equal(group.name, "Product Team") + group.name = "Updated Team" + + assert.equal(group.name, "Updated Team") }) - - it("should allow adding a new member", () => { + it("should throw when setting empty group name", () => { + const store = new Store() + + const group = new Group( + DataFactory.namedNode("https://example.org/group/empty"), + store, + DataFactory + ) + + assert.throws(() => { + group.name = undefined as any + }) + }) + + it("should parse members as Person instances", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) - const dataset = new GroupDataset(store, DataFactory) - const group = Array.from(dataset.group)[0]! + const group = new Group( + DataFactory.namedNode("https://example.org/group/1"), + store, + DataFactory + ) - // Create new person (automatically typed as vcard:Individual) - const charlie = new Person("https://example.org/person/charlie", store, DataFactory) - charlie.name = "Charlie" + const members = group.members - group.addMember(charlie) + assert.ok(members instanceof Set) + assert.equal(members.size, 2) - const memberNames = Array.from(group.members).map((m: Person) => m.name) - assert.ok(memberNames.includes("Charlie")) - assert.ok(memberNames.includes("Alice") || memberNames.includes("Bob")) + for (const member of members) { + assert.ok(member instanceof Person) + } }) - it("should allow removing a member", () => { + + it("should add a new member", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) - const dataset = new GroupDataset(store, DataFactory) - const group = Array.from(dataset.group)[0]! + const group = new Group( + DataFactory.namedNode("https://example.org/group/1"), + store, + DataFactory + ) + + const newPerson = new Person( + DataFactory.namedNode("https://example.org/person/3"), + store, + DataFactory + ) - const alice = new Person("https://example.org/person/alice", store, DataFactory) - group.deleteMember(alice) + group.addMember(newPerson) - const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) - assert.ok(!memberIRIs.includes("https://example.org/person/alice")) - assert.ok(memberIRIs.includes("https://example.org/person/bob")) + const members = group.members + assert.equal(members.size, 3) + + const iris = Array.from(members).map(p => p.term.value) + assert.ok(iris.includes("https://example.org/person/3")) }) - it("should reflect live changes in the dataset", () => { + it("should delete a member", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) - const dataset = new GroupDataset(store, DataFactory) - const group = Array.from(dataset.group)[0]! + const group = new Group( + DataFactory.namedNode("https://example.org/group/1"), + store, + DataFactory + ) + + const personToRemove = new Person( + DataFactory.namedNode("https://example.org/person/1"), + store, + DataFactory + ) + + group.deleteMember(personToRemove) + + const members = group.members + assert.equal(members.size, 1) - const dave = new Person("https://example.org/person/dave", store, DataFactory) - group.addMember(dave) + const iris = Array.from(members).map(p => p.term.value) + assert.ok(!iris.includes("https://example.org/person/1")) + }) + + it("should ensure members are unique", () => { + const duplicateRDF = ` +@prefix vcard: . - const bob = new Person("https://example.org/person/bob", store, DataFactory) - group.deleteMember(bob) + + a vcard:Group ; + vcard:member ; + vcard:member . +` + const store = new Store() + store.addQuads(new Parser().parse(duplicateRDF)) - const memberIRIs = Array.from(group.members).map((m: Person) => m.term.value) - assert.deepEqual(memberIRIs.sort(), [ - "https://example.org/person/alice", - "https://example.org/person/dave" - ]) + const group = new Group( + DataFactory.namedNode("https://example.org/group/dup"), + store, + DataFactory + ) + + const members = group.members + + assert.equal(members.size, 1) }) - + + it("should return empty set if no members exist", () => { + const emptyRDF = ` +@prefix vcard: . + + + a vcard:Group . +` + + const store = new Store() + store.addQuads(new Parser().parse(emptyRDF)) + + const group = new Group( + DataFactory.namedNode("https://example.org/group/empty"), + store, + DataFactory + ) + + assert.ok(group.members instanceof Set) + assert.equal(group.members.size, 0) + }) + }) - */ \ No newline at end of file diff --git a/test/unit/organization.test.ts b/test/unit/organization.test.ts index c01a232..379a62a 100644 --- a/test/unit/organization.test.ts +++ b/test/unit/organization.test.ts @@ -1,97 +1,53 @@ -import { DataFactory, Parser, Store } from "n3"; -import assert from "node:assert"; -import { describe, it } from "node:test"; - +import { DataFactory, Parser, Store } from "n3" +import assert from "node:assert" +import { describe, it } from "node:test" import { Organization } from "@solid/object"; -import {VCARD, SCHEMA, RDF} from "../../src/vocabulary/mod" +describe("Organization tests", () => { -describe("Organization class tests", () => { const sampleRDF = ` -@prefix vcard: . @prefix schema: . +@prefix vcard: . @prefix rdf: . - a vcard:Organization ; + + a schema:Corporation ; schema:name "Example Corp" ; - schema:url "https://example.org" ; - rdf:type schema:Corporation . + schema:url . `; + it("should parse name and url", () => { + const store = new Store() + store.addQuads(new Parser().parse(sampleRDF)) - it("should parse Organization properties", () => { - const store = new Store(); - store.addQuads(new Parser().parse(sampleRDF)); - - const org = new Organization("https://example.org/org/1", store); - - assert.strictEqual(org.name, "Example Corp"); - assert.strictEqual(org.url, "https://example.org"); - assert.ok(org.types.has(VCARD.Organization), "Base type should be vcard:Organization"); - assert.ok(org.types.has(SCHEMA.Corporation), "Should include Corporation type"); - }); - - it("should allow setting Organization properties", () => { - const store = new Store(); - const org = new Organization("https://example.org/org/2", store); - - org.name = "New Org"; - org.url = "https://new.org"; - org.addType(SCHEMA.NGO); - - assert.strictEqual(org.name, "New Org"); - assert.strictEqual(org.url, "https://new.org"); - assert.ok(org.types.has(VCARD.Organization)); - assert.ok(org.types.has(SCHEMA.NGO)); - }); - - it("should enforce allowed organization types (runtime check)", () => { - const store = new Store(); - const org = new Organization("https://example.org/org/3", store); + const org = new Organization( + "https://example.org/org/1", + store, + DataFactory + ) - assert.throws(() => { - // bypass TS type safety for test - org.addType("https://schema.org/InvalidType" as any); - }, /Invalid organization type/); - }); + assert.equal(org.name, "Example Corp") + assert.equal(org.url, "https://example.org") + }) - it("should handle multiple types correctly", () => { - const store = new Store(); - const org = new Organization("https://example.org/org/4", store); + it("should allow setting name and url", () => { + const store = new Store() - org.addType(SCHEMA.Corporation); - org.addType(SCHEMA.NGO); + const org = new Organization( + "https://example.org/org/2", + store, + DataFactory + ) - const typesArray = Array.from(org.types); - assert.ok(typesArray.includes(VCARD.Organization)); - assert.ok(typesArray.includes(SCHEMA.Corporation)); - assert.ok(typesArray.includes(SCHEMA.NGO)); + org.name = "New Org" + org.url = "https://new.org" - // Ensure no duplicates - const uniqueTypes = new Set(typesArray); - assert.strictEqual(uniqueTypes.size, typesArray.length); - }); + assert.equal(org.name, "New Org") + assert.equal(org.url, "https://new.org") + }) - it("should allow updating name and url multiple times", () => { - const store = new Store(); - const org = new Organization("https://example.org/org/5", store); - org.name = "Org A"; - org.url = "https://orga.org"; - org.name = "Org B"; - org.url = "https://orgb.org"; - assert.strictEqual(org.name, "Org B"); - assert.strictEqual(org.url, "https://orgb.org"); - }); - it("should handle minimal organization (only base type)", () => { - const store = new Store(); - const org = new Organization("https://example.org/org/minimal", store); - assert.strictEqual(org.name, undefined); - assert.strictEqual(org.url, undefined); - assert.ok(org.types.has(VCARD.Organization)); - assert.strictEqual(org.types.size, 1); - }); -}); +}) diff --git a/test/unit/person.test.ts b/test/unit/person.test.ts index 7bf0abf..38ca3eb 100644 --- a/test/unit/person.test.ts +++ b/test/unit/person.test.ts @@ -1,44 +1,44 @@ -import { DataFactory, Parser, Store } from "n3"; -import assert from "node:assert"; -import { describe, it } from "node:test"; +import { DataFactory, Parser, Store } from "n3" +import assert from "node:assert" +import { describe, it } from "node:test" import { Person } from "@solid/object"; -describe("Person class tests", () => { + + +describe("Person tests", () => { + const sampleRDF = ` @prefix vcard: . @prefix owl: . - a vcard:Individual ; - vcard:fn "Alice Smith" ; - vcard:email "alice@example.org" ; + + a vcard:Individual ; + vcard:fn "Alice" ; vcard:phone "+123456789" ; - owl:sameAs . + vcard:email "alice@example.org" ; + owl:sameAs . `; - it("should parse Person properties", () => { - const store = new Store(); - store.addQuads(new Parser().parse(sampleRDF)); - - const person = new Person("https://example.org/person/1", store); - - assert.strictEqual(person.name, "Alice Smith"); - assert.strictEqual(person.email, "alice@example.org"); - assert.strictEqual(person.phone, "+123456789"); - assert.strictEqual(person.webId, "https://alice.example.org/#me"); - }); - - it("should allow setting Person properties", () => { - const store = new Store(); - const person = new Person("https://example.org/person/1", store); - - person.name = "Bob Jones"; - person.email = "bob@example.org"; - person.phone = "+987654321"; - person.webId = "https://bob.example.org/#me"; - - assert.strictEqual(person.name, "Bob Jones"); - assert.strictEqual(person.email, "bob@example.org"); - assert.strictEqual(person.phone, "+987654321"); - assert.strictEqual(person.webId, "https://bob.example.org/#me"); - }); -}); + it("should allow setting properties", () => { + const store = new Store() + + const person = new Person( + "https://example.org/person/2", + store, + DataFactory + ) + + person.name = "Bob" + person.phone = "+987654321" + person.email = "bob@example.org" + person.webId = "https://bob.example.org/profile#me" + + assert.equal(person.name, "Bob") + assert.equal(person.phone, "+987654321") + assert.equal(person.email, "bob@example.org") + assert.equal(person.webId, "https://bob.example.org/profile#me") + }) + + + +}) diff --git a/test/unit/profile.test.ts b/test/unit/profile.test.ts index f37df9a..d49c209 100644 --- a/test/unit/profile.test.ts +++ b/test/unit/profile.test.ts @@ -2,149 +2,82 @@ import { DataFactory, Parser, Store } from "n3" import assert from "node:assert" import { describe, it } from "node:test" -import { Profile } from "@solid/object" -import { Person } from "@solid/object" +import { Profile, Person } from "@solid/object"; describe("Profile tests", () => { const sampleRDF = ` @prefix foaf: . @prefix solid: . -@prefix schema: . +@prefix schema: . @prefix org: . - - foaf:nick "alex" ; + + foaf:nick "Ali" ; solid:preferredSubjectPronoun "they" ; solid:preferredObjectPronoun "them" ; solid:preferredRelativePronoun "theirs" ; - schema:skills ; - schema:skills ; - schema:knowsLanguage ; - schema:knowsLanguage ; - foaf:account ; - foaf:account ; - org:member . + org:member ; + schema:skills ; + schema:knowsLanguage ; + foaf:account . `; - it("should parse and retrieve profile properties", () => { + it("should parse nickname and pronouns", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) const profile = new Profile( - DataFactory.namedNode("https://example.org/profile#me"), + DataFactory.namedNode("https://example.org/profile/1"), store, DataFactory ) - // Singular string properties - assert.equal(profile.nickname, "alex") + assert.equal(profile.nickname, "Ali") assert.equal(profile.preferredSubjectPronoun, "they") assert.equal(profile.preferredObjectPronoun, "them") assert.equal(profile.preferredRelativePronoun, "theirs") - - // Set-based properties - assert.ok(profile.skills instanceof Set) - assert.ok(profile.languages instanceof Set) - assert.ok(profile.accounts instanceof Set) - assert.ok(profile.roles instanceof Set) - - assert.ok(profile.skills.has("https://example.org/skills/TypeScript")) - assert.ok(profile.languages.has("https://example.org/lang/en")) - assert.ok(profile.accounts.has("https://github.com/example")) - - const role = Array.from(profile.roles)[0] - assert.ok(role instanceof Person) }) - - it("should allow setting of singular properties", () => { + it("should allow setting nickname and pronouns", () => { const store = new Store() - store.addQuads(new Parser().parse(sampleRDF)) const profile = new Profile( - DataFactory.namedNode("https://example.org/profile#me"), + DataFactory.namedNode("https://example.org/profile/2"), store, DataFactory ) - profile.nickname = "updatedNick" - profile.preferredSubjectPronoun = "she" - profile.preferredObjectPronoun = "her" - profile.preferredRelativePronoun = "hers" - - assert.equal(profile.nickname, "updatedNick") - assert.equal(profile.preferredSubjectPronoun, "she") - assert.equal(profile.preferredObjectPronoun, "her") - assert.equal(profile.preferredRelativePronoun, "hers") - }) - - - it("should ensure singular properties are correct type", () => { - const store = new Store() - store.addQuads(new Parser().parse(sampleRDF)) - - const profile = new Profile( - DataFactory.namedNode("https://example.org/profile#me"), - store, - DataFactory - ) + profile.nickname = "Sam" + profile.preferredSubjectPronoun = "he" + profile.preferredObjectPronoun = "him" + profile.preferredRelativePronoun = "his" - assert.equal(typeof profile.nickname, "string") - assert.equal(typeof profile.preferredSubjectPronoun, "string") - assert.equal(typeof profile.preferredObjectPronoun, "string") - assert.equal(typeof profile.preferredRelativePronoun, "string") + assert.equal(profile.nickname, "Sam") + assert.equal(profile.preferredSubjectPronoun, "he") + assert.equal(profile.preferredObjectPronoun, "him") + assert.equal(profile.preferredRelativePronoun, "his") }) - - it("should ensure set properties return Sets and not arrays", () => { + it("should parse roles as Person instances", () => { const store = new Store() store.addQuads(new Parser().parse(sampleRDF)) const profile = new Profile( - DataFactory.namedNode("https://example.org/profile#me"), + DataFactory.namedNode("https://example.org/profile/1"), store, DataFactory ) - assert.ok(profile.skills instanceof Set) - assert.ok(profile.languages instanceof Set) - assert.ok(profile.accounts instanceof Set) - assert.ok(profile.roles instanceof Set) - - assert.ok(!Array.isArray(profile.skills)) - assert.ok(!Array.isArray(profile.languages)) - assert.ok(!Array.isArray(profile.accounts)) - assert.ok(!Array.isArray(profile.roles)) - }) - - - it("should handle duplicate singular values by exposing only one", () => { - const duplicateRDF = ` -@prefix foaf: . -@prefix solid: . - - - foaf:nick "alex" ; - foaf:nick "duplicateNick" ; - solid:preferredSubjectPronoun "they" ; - solid:preferredSubjectPronoun "duplicatePronoun" . -` - - const store = new Store() - store.addQuads(new Parser().parse(duplicateRDF)) - - const profile = new Profile( - DataFactory.namedNode("https://example.org/profile#me"), - store, - DataFactory - ) + const roles = profile.roles - assert.equal(typeof profile.nickname, "string") - assert.equal(typeof profile.preferredSubjectPronoun, "string") + assert.ok(roles instanceof Set) + assert.equal(roles.size, 1) - assert.ok(!Array.isArray(profile.nickname)) - assert.ok(!Array.isArray(profile.preferredSubjectPronoun)) + for (const role of roles) { + assert.ok(role instanceof Person) + } }) + })