Skip to content

Commit 76f4dc9

Browse files
committed
Add CreateActivityHandler and activity paths.
1 parent 26afad3 commit 76f4dc9

11 files changed

+624
-414
lines changed

.babelrc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
22
presets: [
33
["@babel/env"],
4-
]
4+
],
5+
plugins: [
6+
["babel-plugin-inline-import", {
7+
extensions: [
8+
".ttl",
9+
],
10+
}],
11+
],
512
}

package-lock.json

Lines changed: 457 additions & 406 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@
1616
"!dist/demo"
1717
],
1818
"dependencies": {
19-
"ldflex": "^1.2.0",
20-
"ldflex-comunica": "^1.0.0",
21-
"solid-auth-client": "^2.2.9"
19+
"ldflex": "^1.3.0",
20+
"ldflex-comunica": "^1.0.1",
21+
"solid-auth-client": "^2.2.9",
22+
"uuid": "^3.3.2"
2223
},
2324
"devDependencies": {
2425
"@babel/cli": "^7.0.0",
2526
"@babel/core": "^7.0.1",
2627
"@babel/preset-env": "^7.0.0",
2728
"babel-core": "^7.0.0-bridge.0",
2829
"babel-loader": "^8.0.2",
30+
"babel-plugin-inline-import": "^3.0.0",
2931
"clean-webpack-plugin": "^0.1.19",
3032
"copy-webpack-plugin": "^4.5.2",
3133
"eslint": "^5.6.0",

src/CreateActivityHandler.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import auth from 'solid-auth-client';
2+
import uuid from 'uuid/v4';
3+
import context from './context.json';
4+
import { iterablePromise } from 'ldflex';
5+
import activityTemplate from './activity.ttl';
6+
7+
const { as, xsd } = context['@context'];
8+
9+
/**
10+
* Handler that creates an activity in the user's data pod
11+
* Requires:
12+
* - the `root.user` handler
13+
* - the `root[...]` resolver
14+
*/
15+
export default class CreateActivityHandler {
16+
constructor({ type = `${as}#Like`, path = '/public/activities' } = {}) {
17+
this._type = type;
18+
this._path = path;
19+
}
20+
21+
execute(path, proxy) {
22+
const self = this;
23+
const root = proxy.root;
24+
const user = root.user;
25+
26+
// Return an iterator over the new activity URLs
27+
return () => iterablePromise((async function* () {
28+
// Create an activity for each object on the path
29+
const activities = [];
30+
const inserts = [];
31+
const type = self._type;
32+
const actor = await user;
33+
const time = new Date().toISOString();
34+
for await (const object of proxy) {
35+
if (typeof object === 'string' || object.termType === 'NamedNode') {
36+
const id = `#${uuid()}`;
37+
const props = { id, type, actor, object, time };
38+
activities.push(id);
39+
inserts.push(self._createActivity(props));
40+
}
41+
}
42+
43+
// Send the activity as a patch
44+
const location = new URL(self._path, await user.pim_storage);
45+
await self._sendPatch(location, { insert: inserts.join('') });
46+
47+
// Return the URLs of the new activities
48+
for (const id of activities)
49+
yield root[new URL(id, location)];
50+
})());
51+
}
52+
53+
// Creates a Turtle snippet representing the activity
54+
_createActivity({ id, type, actor, object, time }) {
55+
return activityTemplate
56+
.replace(/_:activity/, `<${id}>`)
57+
.replace(/_:type/, `<${type}>`)
58+
.replace(/_:actor/g, `<${actor}>`)
59+
.replace(/_:object/g, `<${object}>`)
60+
.replace(/_:published/g, `"${time}"^^<${xsd}dateTime>`);
61+
}
62+
63+
// Sends a PATCH request to create the activity
64+
_sendPatch(resource, { insert }) {
65+
const patch = `INSERT {\n${insert}\n}`;
66+
return auth.fetch(resource, {
67+
method: 'PATCH',
68+
headers: {
69+
'Content-Type': 'application/sparql-update',
70+
},
71+
body: patch,
72+
});
73+
}
74+
}

src/activity.ttl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
_:activity a _:type;
2+
<https://www.w3.org/ns/activitystreams#actor> _:actor;
3+
<https://www.w3.org/ns/activitystreams#object> _:object;
4+
<https://www.w3.org/ns/activitystreams#published> _:published.

src/index.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@ import { PathFactory, StringToLDflexHandler } from 'ldflex';
22
import context from './context.json';
33
import UserPathHandler from './UserPathHandler';
44
import SubjectPathResolver from './SubjectPathResolver';
5+
import CreateActivityHandler from './CreateActivityHandler';
6+
7+
const { as } = context['@context'];
8+
9+
let rootPath;
510

611
// Creates data paths that start from a given subject
7-
const subjectPathFactory = new PathFactory({ context });
12+
const subjectPathFactory = new PathFactory({
13+
context,
14+
handlers: {
15+
...PathFactory.defaultHandlers,
16+
// Activities on paths
17+
like: new CreateActivityHandler({ type: `${as}Like` }),
18+
dislike: new CreateActivityHandler({ type: `${as}Dislike` }),
19+
follow: new CreateActivityHandler({ type: `${as}Follow` }),
20+
// The `root` property restarts the path from the root
21+
root: () => rootPath,
22+
},
23+
});
824

925
// Export the root path that resolves the first property access
10-
export default new PathFactory({
26+
export default rootPath = new PathFactory({
1127
// Handlers of specific named properties
1228
handlers: {
1329
// Don't get mistaken for an ES6 module by loaders

test/.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
extends: plugin:jest/recommended,
33
rules: {
4+
camelcase: off,
45
global-require: off,
56
no-use-before-define: off,
67
},

test/CreateActivityHandler-test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import CreateActivityHandler from '../src/CreateActivityHandler';
2+
import auth from 'solid-auth-client';
3+
4+
describe('a CreateActivityHandler instance', () => {
5+
let handler;
6+
beforeEach(() => (handler = new CreateActivityHandler()));
7+
8+
describe('creating a like', () => {
9+
beforeEach(async () => {
10+
const like = handler.execute(null, createProxy());
11+
expect(like).toBeInstanceOf(Function);
12+
await like();
13+
});
14+
15+
it('issues a PATCH request', () => {
16+
expect(auth.fetch).toHaveBeenCalledTimes(1);
17+
const args = auth.fetch.mock.calls[0];
18+
expect(args[1]).toHaveProperty('method', 'PATCH');
19+
});
20+
21+
it('sets the Content-Type to application/sparql-update', () => {
22+
expect(auth.fetch).toHaveBeenCalledTimes(1);
23+
const args = auth.fetch.mock.calls[0];
24+
expect(args[1]).toHaveProperty('headers');
25+
expect(args[1].headers).toHaveProperty('Content-Type', 'application/sparql-update');
26+
});
27+
28+
it('sends a patch document', () => {
29+
expect(auth.fetch).toHaveBeenCalledTimes(1);
30+
const args = auth.fetch.mock.calls[0];
31+
expect(args[1]).toHaveProperty('body');
32+
expect(args[1].body).toMatch(/INSERT/);
33+
expect(args[1].body).toMatch(/Like/);
34+
});
35+
});
36+
});
37+
38+
function createProxy() {
39+
const user = { pim_storage: 'http://user.example/' };
40+
const root = { user };
41+
const proxy = (async function* proxy() {
42+
yield 'http://example.org/#user1';
43+
yield { termType: 'NamedNode' };
44+
yield 0;
45+
}());
46+
proxy.root = root;
47+
return proxy;
48+
}

test/__mocks__/ldflex-comunica.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const ComunicaEngine = jest.genMockFromModule('ldflex-comunica').default;
22

3-
const noResults = { next: async () => ({ done: true }) };
4-
ComunicaEngine.prototype.execute.mockReturnValue(noResults);
3+
async function* noResults() { /* empty */ }
4+
ComunicaEngine.prototype.execute.mockReturnValue(noResults());
55

66
export default ComunicaEngine;

test/__mocks__/solid-auth-client.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export default {
22
currentSession: jest.fn(),
3+
fetch: jest.fn(),
34
};

test/index-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ describe('The @solid/ldflex module', () => {
5656
expect(data.resolve()).toBe(data);
5757
});
5858
});
59+
60+
describe('the root path', () => {
61+
it('resolves to the root', () => {
62+
expect(data.user.root.root.user.root).toBe(data);
63+
});
64+
});
5965
});
6066

6167
const urlQuery = `SELECT ?firstName WHERE {

0 commit comments

Comments
 (0)