Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions contacts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this module will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- [createNewGroup](https://solid-contrib.github.io/data-modules/contacts-rdflib-api/interfaces/ContactsModule.html#createNewGroup)

## 0.2.1

### Fixed
Expand Down
22 changes: 22 additions & 0 deletions contacts/examples/create-new-group.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {ContactsModuleRdfLib as ContactsModule} from '../dist/index.js';
import { Fetcher, graph, UpdateManager } from "rdflib";
import { faker } from "@faker-js/faker";

const store = graph();
const fetcher = new Fetcher(store);
const updater = new UpdateManager(store);
const contacts = new ContactsModule({ store, fetcher, updater });

let addressBookUri =
"http://localhost:3000/alice/public-write/ab9694d6-120e-415d-a315-90cd84c2e062/index.ttl#this";

const groupName = faker.company.name();

const uri = await contacts.createNewGroup({
addressBookUri,
groupName,
});
console.log("created group:", groupName, "at", uri);

const result = await contacts.readAddressBook(addressBookUri);
console.log("the updated address book:", result);
2 changes: 2 additions & 0 deletions contacts/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ module.exports = {
testEnvironment: "node",
rootDir: "src",
testPathIgnorePatterns: [".*\\.e2e\\.spec\\.ts"],
detectOpenHandles: true,
forceExit: true,
};
2 changes: 2 additions & 0 deletions contacts/jest.e2e.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ module.exports = {
testEnvironment: "node",
rootDir: "src/e2e-tests",
testTimeout: 60000,
detectOpenHandles: true,
forceExit: true,
};
4 changes: 2 additions & 2 deletions contacts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"scripts": {
"build": "tsc",
"build:doc": "typedoc src/index.ts --out ../gh-pages/contacts-rdflib-api",
"test": "jest --detectOpenHandles --forceExit",
"test": "jest",
"lint": "eslint",
"test:e2e": "jest --config jest.e2e.config.js --detectOpenHandles --forceExit",
"test:e2e": "jest --config jest.e2e.config.js",
"pod": "community-solid-server --config ./dev-server/config/config-mashlib.json --seedConfig ./dev-server/seed.json --rootFilePath ./dev-server/data",
"pod:init": "cp -r ./dev-server/initial-data/* ./dev-server/data/",
"pod:clean": "rm -rf ./dev-server/data"
Expand Down
20 changes: 20 additions & 0 deletions contacts/src/e2e-tests/contacts.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ describe("contacts module", () => {
],
});
});

it("can create a new group within an existing address book", async () => {
const contacts = setupModule();

const addressBookUri =
"http://localhost:3456/4243dbb6-3126-4bf9-9ea7-45e35c3c8d9d/index.ttl#this";

const groupName = faker.company.name();
const uri = await contacts.createNewGroup({
addressBookUri,
groupName,
});

const result = await contacts.readAddressBook(addressBookUri);

expect(result.groups).toContainEqual({
uri,
name: groupName,
});
});
});

function setupModule() {
Expand Down
24 changes: 24 additions & 0 deletions contacts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ export interface ContactsModule {
* @return Contact name, email addresses and phone numbers
*/
readContact(uri: string): Promise<FullContact>;

/**
* Creates a new group within a given address book
* @param command
* @return The URI of the newly created group
*/
createNewGroup({
addressBookUri,
groupName,
}: CreateNewGroupCommand): Promise<string>;
}

/**
Expand Down Expand Up @@ -121,3 +131,17 @@ export interface PhoneNumber {
* Partial group data listed when reading an address book
*/
export interface Group {}

/**
* Data needed to create a new group within an address book
*/
export interface CreateNewGroupCommand {
/**
* The URI of an existing address book the new group should be added to
*/
addressBookUri: string;
/**
* The name of the group to create
*/
groupName: string;
}
7 changes: 3 additions & 4 deletions contacts/src/rdflib/ContactsModuleRdfLib.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { when } from "jest-when";
import { createAddressBook } from "./createAddressBook";
import { createAddressBook, createNewContact } from "./update-operations";
import { ContactsModuleRdfLib } from "./ContactsModuleRdfLib";
import { executeUpdate } from "./web-operations/executeUpdate";
import { Fetcher, graph, UpdateManager } from "rdflib";
import { createNewContact } from "./createNewContact";

jest.mock("./createAddressBook");
jest.mock("./createNewContact");
jest.mock("./update-operations/createAddressBook");
jest.mock("./update-operations/createNewContact");
jest.mock("./web-operations/executeUpdate");
jest.mock("./web-operations/fetchNode");

Expand Down
18 changes: 14 additions & 4 deletions contacts/src/rdflib/ContactsModuleRdfLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
ContactsModule,
CreateAddressBookCommand,
CreateNewContactCommand,
CreateNewGroupCommand,
FullContact,
} from "..";
import { AddressBookQuery } from "./AddressBookQuery";
import { createAddressBook } from "./createAddressBook";
import { AddressBookQuery, ContactQuery } from "./queries";
import { createAddressBook, createNewContact } from "./update-operations";
import { executeUpdate } from "./web-operations/executeUpdate";
import { createNewContact } from "./createNewContact";
import { fetchNode } from "./web-operations/fetchNode";
import { ContactQuery } from "./ContactQuery";
import { createNewGroup } from "./update-operations/createNewGroup";

interface ModuleConfig {
store: IndexedFormula;
Expand Down Expand Up @@ -89,4 +89,14 @@ export class ContactsModuleRdfLib implements ContactsModule {
phoneNumbers,
};
}

async createNewGroup({ addressBookUri, groupName }: CreateNewGroupCommand) {
const addressBookNode = sym(addressBookUri);
await this.fetchNode(addressBookNode);

const query = new AddressBookQuery(this.store, addressBookNode);
const operation = createNewGroup(query, groupName);
await executeUpdate(this.fetcher, this.updater, operation);
return operation.uri;
}
}
74 changes: 74 additions & 0 deletions contacts/src/rdflib/create-new-group.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { v4 as uuid } from "uuid";
import { Fetcher, graph, UpdateManager } from "rdflib";
import { ContactsModuleRdfLib } from "./ContactsModuleRdfLib";
import {
mockNotFound,
mockTurtleResponse,
} from "../test-support/mockResponses";
import { expectPatchRequest } from "../test-support/expectRequests";

jest.mock("uuid");

describe("create new group", () => {
it("creates group resource", async () => {
const authenticatedFetch = jest.fn();

(uuid as jest.Mock).mockReturnValueOnce(
"b4e9fd85-3b38-4db7-8599-d0eda0b2ac74",
);

const store = graph();
const fetcher = new Fetcher(store, {
fetch: authenticatedFetch,
});
const updater = new UpdateManager(store);
const contacts = new ContactsModuleRdfLib({
store,
fetcher,
updater,
});

mockTurtleResponse(
authenticatedFetch,
"https://pod.test/alice/contacts/index.ttl",
`
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix ab: <http://www.w3.org/ns/pim/ab#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

<#this> a vcard:AddressBook;
dc:title "Alice's contacts";
vcard:nameEmailIndex <people.ttl>;
vcard:groupIndex <groups.ttl>.
`,
);

mockTurtleResponse(
authenticatedFetch,
"https://pod.test/alice/contacts/groups.ttl",
"",
);

mockNotFound(
authenticatedFetch,
"https://pod.test/alice/contacts/Group/b4e9fd85-3b38-4db7-8599-d0eda0b2ac74/index.ttl",
);

const createdUri = await contacts.createNewGroup({
addressBookUri: "https://pod.test/alice/contacts/index.ttl#this",
groupName: "best friends",
});

expect(createdUri).toEqual(
"https://pod.test/alice/contacts/Group/b4e9fd85-3b38-4db7-8599-d0eda0b2ac74/index.ttl#this",
);
expectPatchRequest(
authenticatedFetch,
"https://pod.test/alice/contacts/Group/b4e9fd85-3b38-4db7-8599-d0eda0b2ac74/index.ttl",
`INSERT DATA { <https://pod.test/alice/contacts/Group/b4e9fd85-3b38-4db7-8599-d0eda0b2ac74/index.ttl#this> <http://www.w3.org/2006/vcard/ns#fn> "best friends" .
<https://pod.test/alice/contacts/Group/b4e9fd85-3b38-4db7-8599-d0eda0b2ac74/index.ttl#this> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2006/vcard/ns#Group> .
}`,
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AddressBookQuery } from "./AddressBookQuery";
import { graph, lit, sym } from "rdflib";
import { dc, vcard } from "./namespaces";
import { dc, vcard } from "../namespaces";

import { v4 as uuid } from "uuid";

Expand Down Expand Up @@ -366,6 +366,25 @@ describe("AddressBookQuery", () => {
expect(result).toBe(null);
});

it("returns null if index is not a named node", () => {
const store = graph();
const addressBookNode = sym(
"http://pod.test/alice/contacts/index.ttl#this",
);
store.add(
addressBookNode,
vcard("groupIndex"),
lit("invalid index"),
addressBookNode.doc(),
);
const query = new AddressBookQuery(
store,
sym("http://pod.test/alice/contacts/index.ttl#this"),
);
const result = query.queryGroupIndex();
expect(result).toBe(null);
});

it("returns the node found in store", () => {
const store = graph();
const addressBookNode = sym(
Expand Down Expand Up @@ -648,4 +667,23 @@ describe("AddressBookQuery", () => {
);
});
});

describe("propose new group node", () => {
it("mints a new URI based on the address book container", () => {
(uuid as jest.Mock).mockReturnValueOnce(
"367da26e-460c-4ab8-b6ca-a32edc88df51",
);
const store = graph();
const addressBookNode = sym(
"http://pod.test/alice/contacts/index.ttl#this",
);
const query = new AddressBookQuery(store, addressBookNode);
const contactNode = query.proposeNewGroupNode();
expect(contactNode).toEqual(
sym(
"http://pod.test/alice/contacts/Group/367da26e-460c-4ab8-b6ca-a32edc88df51/index.ttl#this",
),
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IndexedFormula, isNamedNode, NamedNode, sym } from "rdflib";
import { dc, vcard } from "./namespaces";
import { Contact, Group } from "../index";
import { dc, vcard } from "../namespaces";
import { Contact, Group } from "../../index";
import { v4 as uuid } from "uuid";

export class AddressBookQuery {
Expand All @@ -14,10 +14,17 @@ export class AddressBookQuery {
}

proposeNewContactNode(): NamedNode {
return this.proposeNewNode("Person");
}

proposeNewGroupNode(): NamedNode {
return this.proposeNewNode("Group");
}

private proposeNewNode(containerPath: string) {
const id = uuid();
const baseUri = this.addressBookNode.dir()?.uri;
const personDir = "Person";
return sym(`${baseUri}${personDir}/${id}/index.ttl#this`);
return sym(`${baseUri}${containerPath}/${id}/index.ttl#this`);
}

queryTitle() {
Expand Down Expand Up @@ -65,13 +72,18 @@ export class AddressBookQuery {
: [];
}

queryGroupIndex() {
return this.store.any(
queryGroupIndex(): NamedNode | null {
const index = this.store.any(
this.addressBookNode,
vcard("groupIndex"),
undefined,
this.addressBookDoc,
);
if (isNamedNode(index)) {
return index as NamedNode;
} else {
return null;
}
}

queryGroups(): Group[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ContactQuery } from "./ContactQuery";
import { graph, lit, sym } from "rdflib";
import { rdf, vcard } from "./namespaces";
import { rdf, vcard } from "../namespaces";

describe("ContactQuery", () => {
describe("query name", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IndexedFormula, isNamedNode, NamedNode, Node, sym } from "rdflib";
import { vcard } from "./namespaces";
import { Email, PhoneNumber } from "../index";
import { vcard } from "../namespaces";
import { Email, PhoneNumber } from "../../index";
import { Namespace } from "rdflib/lib/factories/factory-types";

const MAILTO_URI_SCHEME = "mailto:";
Expand Down
2 changes: 2 additions & 0 deletions contacts/src/rdflib/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AddressBookQuery } from "./AddressBookQuery";
export { ContactQuery } from "./ContactQuery";
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createAddressBook } from "./createAddressBook";
import { v4 as uuid } from "uuid";
import { lit, st, sym } from "rdflib";
import { dc, vcard } from "./namespaces";
import { UpdateOperation } from "./web-operations/executeUpdate";
import { dc, vcard } from "../namespaces";
import { UpdateOperation } from "./index";

jest.mock("uuid");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { lit, st, sym } from "rdflib";
import { v4 as uuid } from "uuid";
import { dc, vcard } from "./namespaces";
import { UpdateOperation } from "./web-operations/executeUpdate";
import { dc, vcard } from "../namespaces";

import { UpdateOperation } from "./index";

export function createAddressBook(
container: string,
Expand Down
Loading