From 3a33b3ea1341ddf809395ab36c90145cdf335d05 Mon Sep 17 00:00:00 2001 From: Precious Oritsedere Date: Mon, 27 Jul 2026 11:19:30 +0100 Subject: [PATCH 1/5] Add LDP container type IRIs to vocabulary --- src/vocabulary/ldp.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vocabulary/ldp.ts b/src/vocabulary/ldp.ts index 7ce70b9..557cd4d 100644 --- a/src/vocabulary/ldp.ts +++ b/src/vocabulary/ldp.ts @@ -1,3 +1,5 @@ export const LDP = { contains: "http://www.w3.org/ns/ldp#contains", + Container: "http://www.w3.org/ns/ldp#Container", + BasicContainer: "http://www.w3.org/ns/ldp#BasicContainer", } as const; From 53c162a557d6e061bb792fd535295d1a55782c20 Mon Sep 17 00:00:00 2001 From: Precious Oritsedere Date: Mon, 27 Jul 2026 11:42:03 +0100 Subject: [PATCH 2/5] Find empty containers by type, not only by ldp:contains --- src/solid/ContainerDataset.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/solid/ContainerDataset.ts b/src/solid/ContainerDataset.ts index a505155..8ecc42d 100644 --- a/src/solid/ContainerDataset.ts +++ b/src/solid/ContainerDataset.ts @@ -3,13 +3,22 @@ import { Container } from "./Container.js" import { LDP } from "../vocabulary/mod.js" export class ContainerDataset extends DatasetWrapper { - // TODO: Consider that this might be undefined if there are no contained resources. We might need different matching. + get container(): Container | undefined { - // Return the first container in the dataset + // Non-empty containers advertise children via ldp:contains. for (const s of this.subjectsOf(LDP.contains, Container)) { return s } + // Empty containers have no ldp:contains; resolving via rdf:type instead. + for (const s of this.instancesOf(LDP.Container, Container)) { + return s + } + + for (const s of this.instancesOf(LDP.BasicContainer, Container)) { + return s + } + return } } From 7146a36affa70633ae24140111b953c1c15b56b1 Mon Sep 17 00:00:00 2001 From: Precious Oritsedere Date: Mon, 27 Jul 2026 12:13:38 +0100 Subject: [PATCH 3/5] Add tests for empty and non-empty containers --- test/unit/container.test.ts | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/unit/container.test.ts diff --git a/test/unit/container.test.ts b/test/unit/container.test.ts new file mode 100644 index 0000000..34453b2 --- /dev/null +++ b/test/unit/container.test.ts @@ -0,0 +1,50 @@ +import { DataFactory, Parser, Store } from "n3"; +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { ContainerDataset } from "@solid/object"; + +describe("ContainerDataset", () => { + const emptyContainerRDF = ` + @prefix dc: . + @prefix ldp: . + @prefix posix: . + @prefix xsd: . + <> a ldp:Container, ldp:BasicContainer, ldp:Resource; + dc:modified "2026-07-23T10:41:12.846Z"^^xsd:dateTime; + posix:mtime 1784803272. + `; + + const nonEmptyContainerRDF = ` + @prefix ldp: . + + a ldp:Container; + ldp:contains . + + a ldp:Resource . + `; + + it("resolves an empty container via rdf:type when ldp:contains is absent", () => { + const store = new Store(); + const parser = new Parser({ baseIRI: "https://pod.example/empty/" }); + store.addQuads(parser.parse(emptyContainerRDF)); + const dataset = new ContainerDataset(store, DataFactory); + const container = dataset.container; + assert.ok(container !== undefined); + assert.equal(container.id, "https://pod.example/empty/"); + assert.equal(container.contains.size, 0); + }); + + it("still resolves a non-empty container via ldp:contains", () => { + const store = new Store(); + store.addQuads(new Parser().parse(nonEmptyContainerRDF)); + const dataset = new ContainerDataset(store, DataFactory); + const container = dataset.container; + assert.ok(container !== undefined); + assert.equal(container.id, "https://pod.example/container/"); + assert.equal(container.contains.size, 1); + assert.equal( + container.contains.values().next().value?.id, + "https://pod.example/container/file.txt", + ); + }); +}); From 32bc19b960340abbb2b21a87eb0dcb7466699c8c Mon Sep 17 00:00:00 2001 From: Precious Oritsedere Date: Mon, 27 Jul 2026 13:41:52 +0100 Subject: [PATCH 4/5] chore: reverted to original comment --- src/solid/ContainerDataset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solid/ContainerDataset.ts b/src/solid/ContainerDataset.ts index 8ecc42d..5518463 100644 --- a/src/solid/ContainerDataset.ts +++ b/src/solid/ContainerDataset.ts @@ -5,7 +5,7 @@ import { LDP } from "../vocabulary/mod.js" export class ContainerDataset extends DatasetWrapper { get container(): Container | undefined { - // Non-empty containers advertise children via ldp:contains. + // Return the first container in the dataset for (const s of this.subjectsOf(LDP.contains, Container)) { return s } From 9fc72bed29702df7ae3798dfd3d78cf223bb94d3 Mon Sep 17 00:00:00 2001 From: Precious Oritsedere Date: Mon, 27 Jul 2026 13:44:04 +0100 Subject: [PATCH 5/5] chore: apply jesse's suggested comment --- src/solid/ContainerDataset.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solid/ContainerDataset.ts b/src/solid/ContainerDataset.ts index 5518463..bfa3dc7 100644 --- a/src/solid/ContainerDataset.ts +++ b/src/solid/ContainerDataset.ts @@ -10,7 +10,9 @@ export class ContainerDataset extends DatasetWrapper { return s } - // Empty containers have no ldp:contains; resolving via rdf:type instead. + // If the container is empty, then the only instance of `ldp:Container` + // and `ldp:BasicContainer` in this dataset should be the container + // itself. for (const s of this.instancesOf(LDP.Container, Container)) { return s }