├── .gitignore ├── src ├── schema │ ├── index.js │ ├── message │ │ ├── type.graphql │ │ ├── post │ │ │ ├── resolver.js │ │ │ └── type.graphql │ │ ├── about │ │ │ ├── resolver.js │ │ │ └── type.graphql │ │ ├── default │ │ │ ├── type.graphql │ │ │ └── resolver.js │ │ ├── channel │ │ │ ├── resolver.js │ │ │ └── type.graphql │ │ ├── contact │ │ │ ├── resolver.js │ │ │ └── type.graphql │ │ ├── resolver.js │ │ ├── default.js │ │ ├── helpers.js │ │ └── index.js │ ├── query.js │ ├── channel.js │ ├── resolvers.js │ ├── user.js │ └── typeDefs.js └── index.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /src/schema/index.js: -------------------------------------------------------------------------------- 1 | const typeDefs = require('./typeDefs'); 2 | const resolvers = require('./resolvers'); 3 | 4 | module.exports = { resolvers, typeDefs }; -------------------------------------------------------------------------------- /src/schema/message/type.graphql: -------------------------------------------------------------------------------- 1 | interface Message { 2 | content: String 3 | key: String! 4 | sequence: Int! 5 | timestamp: Float! 6 | type: String! 7 | } 8 | -------------------------------------------------------------------------------- /src/schema/message/post/resolver.js: -------------------------------------------------------------------------------- 1 | const DefaultMessage = require('../default/resolver'); 2 | 3 | module.exports = { 4 | ...DefaultMessage, 5 | text: (msg) => msg.value.content.text, 6 | } 7 | -------------------------------------------------------------------------------- /src/schema/message/about/resolver.js: -------------------------------------------------------------------------------- 1 | const DefaultMessage = require('../default/resolver'); 2 | 3 | module.exports = { 4 | ...DefaultMessage, 5 | name: (msg) => msg.value.content.name, 6 | } 7 | -------------------------------------------------------------------------------- /src/schema/message/default/type.graphql: -------------------------------------------------------------------------------- 1 | # import Message from '../type.graphql' 2 | 3 | type DefaultMessage implements Message { 4 | content: String 5 | key: String! 6 | sequence: Int! 7 | timestamp: Float! 8 | type: String! 9 | } 10 | -------------------------------------------------------------------------------- /src/schema/message/default/resolver.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: (msg) => JSON.stringify(msg.value.content), 3 | sequence: (msg) => msg.value.sequence, 4 | timestamp: (msg) => msg.value.timestamp, 5 | type: (msg) => msg.value.content.type, 6 | } 7 | -------------------------------------------------------------------------------- /src/schema/message/channel/resolver.js: -------------------------------------------------------------------------------- 1 | const DefaultMessage = require('../default/resolver'); 2 | 3 | module.exports = { 4 | ...DefaultMessage, 5 | channel: (msg) => msg.value.content.channel, 6 | subscribed: (msg) => msg.value.content.subscribed, 7 | } 8 | -------------------------------------------------------------------------------- /src/schema/message/about/type.graphql: -------------------------------------------------------------------------------- 1 | # import Message from '../type.graphql' 2 | 3 | type AboutMessage implements Message { 4 | content: String 5 | key: String! 6 | name: String 7 | sequence: Int! 8 | timestamp: Float! 9 | type: String! 10 | } 11 | -------------------------------------------------------------------------------- /src/schema/message/post/type.graphql: -------------------------------------------------------------------------------- 1 | # import Message from '../type.graphql' 2 | 3 | type PostMessage implements Message { 4 | content: String 5 | key: String! 6 | sequence: Int! 7 | text: String 8 | timestamp: Float! 9 | type: String! 10 | } 11 | -------------------------------------------------------------------------------- /src/schema/message/channel/type.graphql: -------------------------------------------------------------------------------- 1 | # import Message from '../type.graphql' 2 | 3 | type ChannelMessage implements Message { 4 | channel: String! 5 | content: String 6 | key: String! 7 | sequence: Int! 8 | subscribed: Boolean! 9 | timestamp: Float! 10 | type: String! 11 | } 12 | -------------------------------------------------------------------------------- /src/schema/message/contact/resolver.js: -------------------------------------------------------------------------------- 1 | const { getProfile } = require('../../user/helpers'); 2 | const DefaultMessage = require('../default/resolver'); 3 | 4 | module.exports = { 5 | ...DefaultMessage, 6 | contact: (msg, { sbot }) => getProfile({ id: msg.value.content.contact }, sbot), 7 | following: (msg) => msg.value.content.following, 8 | } 9 | -------------------------------------------------------------------------------- /src/schema/message/contact/type.graphql: -------------------------------------------------------------------------------- 1 | # import Message from '../type.graphql' 2 | # import User from '../../user/type.graphql' 3 | 4 | type ContactMessage implements Message { 5 | contact: User! 6 | content: String 7 | following: Boolean! 8 | key: String! 9 | sequence: Int! 10 | timestamp: Float! 11 | type: String! 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/message/resolver.js: -------------------------------------------------------------------------------- 1 | const DefaultMessage = require('./default/resolver'); 2 | 3 | const typeMap = { 4 | about: 'AboutMessage', 5 | channel: 'ChannelMessage', 6 | contact: 'ContactMessage', 7 | post: 'PostMessage', 8 | }; 9 | 10 | module.exports = { 11 | __resolveType: (obj) => typeMap[obj.value.content.type] || 'DefaultMessage', 12 | ...DefaultMessage, 13 | } 14 | -------------------------------------------------------------------------------- /src/schema/message/default.js: -------------------------------------------------------------------------------- 1 | const typeDef = ` 2 | type DefaultMessage implements Message { 3 | content: String 4 | key: String! 5 | sequence: Int! 6 | timestamp: Float! 7 | type: String! 8 | } 9 | `; 10 | 11 | const resolvers = { 12 | content: (msg) => JSON.stringify(msg.value.content), 13 | sequence: (msg) => msg.value.sequence, 14 | timestamp: (msg) => msg.value.timestamp, 15 | type: (msg) => msg.value.content.type, 16 | } 17 | 18 | module.exports = { resolvers, typeDef }; -------------------------------------------------------------------------------- /src/schema/query.js: -------------------------------------------------------------------------------- 1 | const { getHistory } = require('./message'); 2 | const { getId, getProfile } = require('./user'); 3 | 4 | const typeDef = ` 5 | type Query { 6 | history(id: String): [Message] 7 | profile(id: String): User 8 | whoami: String 9 | } 10 | `; 11 | 12 | const resolvers = { 13 | history: (_, { id, sequence }, { sbot }) => getHistory({ id, sequence }, sbot), 14 | profile: (_, { id }, { sbot }) => getProfile({ id }, sbot), 15 | whoami: (_, obj, { sbot }) => getId(sbot), 16 | } 17 | 18 | module.exports = { resolvers, typeDef }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const expressGraphql = require('express-graphql'); 3 | const ssbClient = require('ssb-party'); 4 | const { makeExecutableSchema } = require('graphql-tools'); 5 | 6 | const { resolvers, typeDefs } = require('./schema'); 7 | 8 | const schema = makeExecutableSchema({ resolvers, typeDefs }); 9 | const app = express(); 10 | 11 | ssbClient({ party: { out: false } }, (err, sbot) => { 12 | if (err) { throw new Error(err); } 13 | app.use('/graphql', expressGraphql({ schema, context: { sbot }, graphiql: true })); 14 | app.listen(3000); 15 | }); 16 | -------------------------------------------------------------------------------- /src/schema/channel.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream'); 2 | 3 | const { getHistory } = require('./message'); 4 | 5 | const typeDef = ` 6 | type Channel { 7 | name: String! 8 | subscribed: Boolean 9 | foo: String 10 | } 11 | `; 12 | 13 | const resolvers = {}; 14 | 15 | const getChannels = async ({ id }, sbot) => { 16 | const msgs = await getHistory({ id }, sbot); 17 | return Object.keys(msgs) 18 | .map((key) => msgs[key]) 19 | .filter((msg) => msg.value.content.type === 'channel') 20 | .reduce((channels, msg) => { 21 | const { channel, subscribed } = msg.value.content; 22 | return [...channels, { name: channel, subscribed }]; 23 | }, []); 24 | }; 25 | 26 | module.exports = { resolvers, typeDef, getChannels }; -------------------------------------------------------------------------------- /src/schema/message/helpers.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream'); 2 | const ref = require('ssb-ref'); 3 | 4 | const getHistory = ({ id, sequence = 0 }, sbot) => new Promise((resolve, reject) => { 5 | if (!ref.isFeedId(id)) { reject(new Error(`${id} is not a valid feed ID`)); } 6 | pull( 7 | sbot.createHistoryStream({ id, sequence }), 8 | pull.collect((err, msgs) => { if (err) { reject(err); } resolve(msgs); }), 9 | ); 10 | }); 11 | 12 | const getLinks = ({ source, dest, rel }, sbot) => new Promise((resolve, reject) => { 13 | pull( 14 | sbot.links({ source, dest, rel, values: true }), 15 | pull.collect((err, msgs) => { if (err) { reject(err); } resolve(msgs); }), 16 | ); 17 | }); 18 | 19 | module.exports = { getHistory, getLinks }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssb-graphql 2 | A GraphQL server for Secure Scuttlebutt (SSB) 3 | 4 | --- 5 | 6 | `ssb-graphql` is a collection of [GraphQL](http://graphql.org/) schema, queries, and mutations that make it easier for clients to work with [Scuttlebot](http://scuttlebot.io/). Ask only for what you need, when you need it. 7 | 8 | Example request: 9 | ``` 10 | query { 11 | profile (key: '@...') { 12 | name 13 | messages { key } 14 | channels { name } 15 | } 16 | } 17 | ``` 18 | 19 | Example response: 20 | ``` 21 | profile { 22 | name: 'Stanley' 23 | messages: [ 24 | { key: '@...' } 25 | { key: '@...' } 26 | { key: '@...' } 27 | ] 28 | channels: [ 29 | { name: 'scuttlebutt' } 30 | { name: 'solarpunk' } 31 | ] 32 | } 33 | ``` 34 | 35 | There is also a GraphiQL client that lets you explore the schema. 36 | -------------------------------------------------------------------------------- /src/schema/resolvers.js: -------------------------------------------------------------------------------- 1 | // const AboutMessage = require('./message/about/resolver'); 2 | // const Channel = require('./channel/resolver'); 3 | // const ChannelMessage = require('./message/channel/resolver'); 4 | // const ContactMessage = require('./message/contact/resolver'); 5 | const { resolvers: DefaultMessage } = require('./message/default'); 6 | // const Message = require('./message/resolver'); 7 | // const PostMessage = require('./message/post/resolver'); 8 | const { resolvers: Message } = require('./message'); 9 | const { resolvers: Query } = require('./query'); 10 | const { resolvers: User } = require('./user'); 11 | // const User = require('./user/resolver'); 12 | 13 | module.exports = { 14 | // AboutMessage, 15 | // Channel, 16 | // ChannelMessage, 17 | // ContactMessage, 18 | DefaultMessage, 19 | Message, 20 | // PostMessage, 21 | Query, 22 | User, 23 | }; 24 | -------------------------------------------------------------------------------- /src/schema/user.js: -------------------------------------------------------------------------------- 1 | const { getChannels } = require('./channel'); 2 | const { getLinks } = require('./message'); 3 | 4 | const typeDef = ` 5 | type User { 6 | channels: [Channel] 7 | id: String! 8 | name: String 9 | } 10 | `; 11 | 12 | const resolvers = { 13 | channels: (obj, args, { sbot }) => getChannels(obj, sbot), 14 | } 15 | 16 | const getId = (sbot) => new Promise((resolve, reject) => { 17 | sbot.whoami((err, info) => { if (err) { reject(err); } resolve(info.id); }); 18 | }); 19 | 20 | const getProfile = async ({ id }, sbot) => { 21 | try { 22 | const msgs = await getLinks({ source: id, dest: id, rel: 'about' }, sbot); 23 | const profile = Object.keys(msgs) 24 | .map((key) => msgs[key]) 25 | .reduce((profile, msg) => ({ ...profile, ...msg.value.content }), {}); 26 | return { id, ...profile }; 27 | } catch (err) { 28 | return { id, name: id }; 29 | } 30 | }; 31 | 32 | module.exports = { resolvers, typeDef, getId, getProfile }; 33 | 34 | -------------------------------------------------------------------------------- /src/schema/typeDefs.js: -------------------------------------------------------------------------------- 1 | // const { importSchema } = require('graphql-import'); 2 | 3 | // const Message = importSchema('src/schema/message/type.graphql'); 4 | // const AboutMessage = importSchema('src/schema/message/about/type.graphql'); 5 | const { typeDef: DefaultMessage } = require('./message/default'); 6 | // const Channel = importSchema('src/schema/channel/type.graphql'); 7 | // const ChannelMessage = importSchema('src/schema/message/channel/type.graphql'); 8 | // const ContactMessage = importSchema('src/schema/message/contact/type.graphql'); 9 | // const PostMessage = importSchema('src/schema/message/post/type.graphql'); 10 | // const User = importSchema('src/schema/user/type.graphql'); 11 | 12 | const { typeDef: Channel } = require('./channel'); 13 | const { typeDef: Query } = require('./query'); 14 | const { typeDef: User } = require('./user'); 15 | const { typeDef: Message } = require('./message'); 16 | 17 | const Schema = ` 18 | schema { 19 | query: Query 20 | } 21 | `; 22 | 23 | module.exports = [ 24 | // AboutMessage, 25 | Channel, 26 | // ChannelMessage, 27 | // ContactMessage, 28 | DefaultMessage, 29 | Message, 30 | // PostMessage, 31 | Query, 32 | Schema, 33 | User, 34 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-graphql", 3 | "version": "0.0.0", 4 | "description": "GraphQL server for Scuttlebot", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "preserve": "babel src -d dist --presets es2015,stage-2", 9 | "start": "nodemon ./src", 10 | "serve": "node dist", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "dependencies": { 14 | "body-parser": "1.18.2", 15 | "express": "4.16.2", 16 | "express-graphql": "0.6.11", 17 | "graphql": "0.12.3", 18 | "graphql-import": "0.3.1", 19 | "graphql-tools": "2.18.0", 20 | "pull-stream": "3.6.1", 21 | "ssb-party": "0.5.1", 22 | "ssb-ref": "2.9.1" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "6.26.0", 26 | "babel-plugin-inline-import": "2.0.6", 27 | "babel-plugin-transform-object-rest-spread": "6.26.0", 28 | "babel-polyfill": "6.26.0", 29 | "babel-preset-es2015": "6.24.1", 30 | "babel-preset-stage-2": "6.24.1", 31 | "nodemon": "1.14.10" 32 | }, 33 | "babel": { 34 | "presets": [ 35 | "es2015", 36 | "stage-2" 37 | ], 38 | "plugins": [ 39 | "babel-plugin-inline-import", 40 | "transform-object-rest-spread" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/schema/message/index.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream'); 2 | const ref = require('ssb-ref'); 3 | 4 | const { resolvers: DefaultMessage } = require('./default'); 5 | 6 | const typeDef = ` 7 | interface Message { 8 | content: String 9 | key: String! 10 | sequence: Int! 11 | timestamp: Float! 12 | type: String! 13 | } 14 | `; 15 | 16 | const typeMap = { 17 | about: 'AboutMessage', 18 | channel: 'ChannelMessage', 19 | contact: 'ContactMessage', 20 | post: 'PostMessage', 21 | }; 22 | 23 | const resolvers = { 24 | __resolveType: (obj) => typeMap[obj.value.content.type] || 'DefaultMessage', 25 | ...DefaultMessage, 26 | } 27 | 28 | const getHistory = ({ id, sequence = 0 }, sbot) => new Promise((resolve, reject) => { 29 | if (!ref.isFeedId(id)) { reject(new Error(`${id} is not a valid feed ID`)); } 30 | pull( 31 | sbot.createHistoryStream({ id, sequence }), 32 | pull.collect((err, msgs) => { if (err) { reject(err); } resolve(msgs); }), 33 | ); 34 | }); 35 | 36 | const getLinks = ({ source, dest, rel }, sbot) => new Promise((resolve, reject) => { 37 | pull( 38 | sbot.links({ source, dest, rel, values: true }), 39 | pull.collect((err, msgs) => { if (err) { reject(err); } resolve(msgs); }), 40 | ); 41 | }); 42 | 43 | module.exports = { resolvers, typeDef, getHistory, getLinks }; --------------------------------------------------------------------------------