diff --git a/.eslintignore b/.eslintignore
index 50d3f7109..43862e59b 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,10 +1,12 @@
**/*.html
dist
-docs
+
node_modules
plugins/*
public
+**/*.min.js
+
!plugins/talk-plugin-akismet
!plugins/talk-plugin-auth
!plugins/talk-plugin-author-menu
diff --git a/docs/_config.yml b/docs/_config.yml
index 96fcd07c3..f4c7639f4 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -120,6 +120,12 @@ sidebar:
url: /additional-plugins/
- title: Plugin Recipes
url: /plugin-recipes/
+ - title: Reference
+ children:
+ - title: Server Plugin API
+ url: /reference/server/
+ - title: GraphQL API
+ url: /reference/graphql/
- title: FAQ
children:
- title: FAQ
diff --git a/docs/package.json b/docs/package.json
index 2a5ff0dac..52f1abb57 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -11,6 +11,9 @@
"develop-theme": "nodemon -x 'rm db.json; hexo serve' -w assets/ -w code/ -w source/ -w themes/ -w scripts/"
},
"dependencies": {
+ "cheerio": "^1.0.0-rc.2",
+ "common-tags": "^1.7.2",
+ "graphql-docs": "^0.2.0",
"hexo": "^3.2.0",
"hexo-generator-index": "^0.2.0",
"hexo-renderer-marked": "^0.3.0",
diff --git a/docs/source/02-01-required-configuration.md b/docs/source/02-01-required-configuration.md
index 387799ff6..1bfaa361c 100644
--- a/docs/source/02-01-required-configuration.md
+++ b/docs/source/02-01-required-configuration.md
@@ -2,6 +2,7 @@
title: Required Configuration
permalink: /configuration/
class: configuration
+toc: true
---
Talk requires configuration in order to customize the installation. The default
@@ -10,8 +11,8 @@ behavior is to load it's configuration from the environment, following the
In development, you can specify configuration in a file named `.env` and it will
be loaded into the environment when you run `yarn watch:server`.
-The following variables do not have defaults, and are **required** to start your
-instance of Talk:
+The above variables do not have defaults, and are **required** to start your
+instance of Talk.
If you've already configured your application with the required configuration,
you can further customize it's behavior by applying
diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md
index 0105c894b..79c34f7df 100644
--- a/docs/source/02-02-advanced-configuration.md
+++ b/docs/source/02-02-advanced-configuration.md
@@ -2,6 +2,7 @@
title: Advanced Configuration
permalink: /advanced-configuration/
class: configuration
+toc: true
---
Talk requires configuration in order to customize the installation. The default
@@ -10,8 +11,8 @@ behavior is to load its configuration from the environment, following the
In development, you can specify configuration in a file named `.env` and it will
be loaded into the environment when you run `yarn watch:server`.
-The following variables have defaults, and are _optional_ to start your
-instance of Talk:
+The variables above have defaults, and are _optional_ to start your
+instance of Talk.
If this is your first time configuring Talk, ensure you've also added the
[Required Configuration](./configuration) as well,
diff --git a/docs/source/04-02-default-plugins.md b/docs/source/04-02-default-plugins.md
index 5bbc429d4..bd03ba34f 100644
--- a/docs/source/04-02-default-plugins.md
+++ b/docs/source/04-02-default-plugins.md
@@ -2,6 +2,7 @@
title: Default Plugins
permalink: /default-plugins/
class: configuration
+toc: true
---
The default Talk plugins can be found in the `plugins.default.json` file
diff --git a/docs/source/04-03-additional-plugins.md b/docs/source/04-03-additional-plugins.md
index 371d1b14a..65973b51f 100644
--- a/docs/source/04-03-additional-plugins.md
+++ b/docs/source/04-03-additional-plugins.md
@@ -2,6 +2,7 @@
title: Additional Plugins
permalink: /additional-plugins/
class: configuration
+toc: true
---
Talk ships with several plugins that aren't enabled by default. These plugins
diff --git a/docs/source/04-04-plugin-recipes.md b/docs/source/04-04-plugin-recipes.md
index f24d6664c..9dddf255d 100644
--- a/docs/source/04-04-plugin-recipes.md
+++ b/docs/source/04-04-plugin-recipes.md
@@ -2,6 +2,7 @@
title: Plugin Recipes
permalink: /plugin-recipes/
class: configuration
+toc: true
---
Plugin Recipes are plugin templates used to help bootstrap the development of a
diff --git a/docs/themes/coral/layout/404.swig b/docs/source/404.md
similarity index 98%
rename from docs/themes/coral/layout/404.swig
rename to docs/source/404.md
index 2307cd0d5..361a4912c 100644
--- a/docs/themes/coral/layout/404.swig
+++ b/docs/source/404.md
@@ -15,4 +15,4 @@
Page not found :(
The requested page could not be found. Try searching above for your page.
-
+
\ No newline at end of file
diff --git a/docs/source/reference/graphql.md b/docs/source/reference/graphql.md
new file mode 100644
index 000000000..100dca502
--- /dev/null
+++ b/docs/source/reference/graphql.md
@@ -0,0 +1,9 @@
+---
+title: GraphQL API
+permalink: /reference/graphql/
+---
+
+We provide all services that Talk can provide via the GraphQL API documented
+below. For a primer about GraphQL, visit http://graphql.org/.
+
+{% graphqldocs ../../client/coral-framework/graphql/introspection.json %}
\ No newline at end of file
diff --git a/docs/source/reference/server.md b/docs/source/reference/server.md
new file mode 100644
index 000000000..ce8cebae1
--- /dev/null
+++ b/docs/source/reference/server.md
@@ -0,0 +1,521 @@
+---
+title: Server Plugin API
+permalink: /reference/server/
+toc: true
+class: configuration
+---
+
+The server functionality of our plugin lives inside the `index.js` plugin folder
+that exports the configuration of our plugin.
+
+ my-plugin/
+ ├── client/
+ │ └── ... <-- client side plugin files
+ └── index.js <-- base + server plugin index
+
+## Hooks
+
+Each plugin should export a single object with all hooks available on it.
+
+_**Note: You will have access to the whole core and other plugin's typeDefs,
+context, loaders, mutators, resolvers, hooks. This is intentional, as it
+encourages composing plugins to merge functionality, like a Slack plugin which
+provides a Slack notify context function as well as having the loader for
+comments.**_
+
+The following are the hooks available:
+
+### typeDefs
+
+```graphql
+enum COLOUR {
+ RED
+ BLUE
+}
+
+type Person {
+ name: String!
+ colour: COLOUR!
+}
+
+type RootMutation {
+ createPerson(name: String!): Person
+}
+
+type RootQuery {
+ people: [Person!]
+}
+
+type Subscription {
+ leader: Person
+}
+```
+
+Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
+`typeDefs` should be a string that will be _merged_ with the existing type
+definitions. `enum`'s will be appended to, types will be appended, and new types
+will be added.
+
+### context
+
+```js
+{
+ Slack: (context) => ({
+ notify: (message) => {
+ // return a promise after we're done sending notifications.
+ }
+ })
+}
+```
+
+Any property provided here will be added to the context parameter available
+inside all resolvers, loaders, mutators, and of course, other context based
+plugins.
+
+The top level item must accept a context for the request which it should use to
+configure the context plugin before it would be mounted at `context.plugins`.
+This plugin above would mount at: `context.plugins.Slack`, or, if you're using
+[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
+
+### Sort
+
+A special context hook, `Sort` will allow plugin authors to provide new
+methods to sort data. An example is as follows:
+
+```js
+{
+ Sort: () => ({
+ Comments: { // <-- (1)
+ likes: { // <-- (2)
+ startCursor(ctx, nodes, {cursor}) { // <-- (3)
+ return cursor != null ? cursor : 0;
+ },
+ endCursor(ctx, nodes, {cursor}) { // <-- (4)
+ return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
+ },
+ sort(ctx, query, {cursor, sort}) { // <-- (5)
+ if (cursor) {
+ query = query.skip(cursor);
+ }
+
+ return query.sort({
+ 'action_counts.like': sort === 'DESC' ? -1 : 1,
+ created_at: sort === 'DESC' ? -1 : 1,
+ });
+ },
+ },
+ },
+ }),
+}
+```
+
+This has a bunch of special features:
+
+1. `Comments` is the name of the type being sorted, this is pluralized and
+ capitalized.
+2. `likes` is the `sortBy` field in lowercase.
+3. `startCursor` will retrieve the start cursor based on the current set of
+ nodes and the current cursor.
+4. `endCursor` will retrieve the end cursor based on the current set of nodes
+ and the current cursor.
+5. `sort` will mutate the `query` to apply the sort operations.
+
+All the `startCursor`, `endCursor`, and `sort` functions must be provided in
+order for the sorting to apply properly.
+
+### loaders
+
+```js
+(context) => ({
+ People: {
+ load: () => db.people.find({user: context.user})
+ }
+})
+```
+
+Loaders should be provided as a function which returns a map which is used in
+the resolvers function. These must return a promise or a value.
+
+### mutators
+
+```js
+(context) => ({
+ People: {
+ create: (name) => {
+ return db.people.insert({user: context.user, name});
+ }
+ }
+})
+```
+
+Mutators should be provided as a function which returns a map which is used in
+the resolvers function. These must return a promise or a value.
+
+### resolvers
+
+```js
+{
+ Person: {
+ name(obj, args, context) {
+ return obj.name;
+ },
+ colour(obj, args, context) {
+ // Bill likes the colour red, everyone else likes blue.
+ return obj.name === 'bill' ? 'RED' : 'BLUE';
+ }
+ },
+ RootQuery: {
+ people(obj, args, {loaders: {People}}) {
+ return People.load();
+ }
+ },
+ RootMutation: {
+ createPerson(obj, {name}, {mutators: {People}}) {
+ return People.create(name);
+ }
+ }
+}
+```
+
+Should return a resolver map as described in the
+[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers#Resolver-map).
+
+This will merge with the existing resolvers in core and from previous plugins.
+
+### hooks
+
+```js
+{
+ RootMutation: {
+ createPerson: {
+ post: async (obj, args, {plugins: {Slack}}, info, person) {
+ if (!person) {
+ return person;
+ }
+
+ await Slack.notify(`A new person just was created with name ${person.name}`);
+
+ return person;
+ }
+ }
+ }
+}
+```
+
+Hooks here are pretty special, for each resolver field, you can specify a
+pre/post hook that will execute pre and post field resolution.
+
+If your post function accepts four parameters, then it can modify the field
+result. It is *required* that the function resolves a promise (or returns) with
+the modified value or simply the original if you didn't modify it.
+
+### setupFunctions
+
+```js
+setupFunctions: {
+ leader: (options, args) => ({
+ leader: {
+ filter: (person) => person.place === 1
+ },
+ }),
+}
+```
+
+Setup functions allow you to create filters that control which pubsub.publish() events
+send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
+
+For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
+
+### tokenUserNotFound
+
+```js
+tokenUserNotFound: async ({jwt, token}) => {
+ let profile = await someExternalService(token);
+ if (!profile) {
+ return null;
+ }
+
+ let user = await UserModel.findOneAndUpdate({
+ id: profile.id
+ }, {
+ id: profile.id,
+ username: profile.username,
+ lowercaseUsername: profile.username.toLowerCase(),
+ roles: [],
+ profiles: []
+ }, {
+ setDefaultsOnInsert: true,
+ new: true,
+ upsert: true
+ });
+
+ return user;
+}
+```
+
+The `tokenUserNotFound` hook allows auth integrations to hook into the event
+when a valid token is provided but a user can't be found in the database that
+matches the provided id.
+
+The function is async, and should return the user object that was created in the
+database, or null if the user wasn't found. The `jwt` parameter of the object
+is the unpacked token, while `token` is the original jwt token string.
+
+### tags
+
+The tags hook allows a plugin to define tags that are code controlled (added
+or enabled by code). Below is an example pulled from the core off topic plugin
+on how to create a hook for the `OFF_TOPIC` name:
+
+```js
+[
+ {
+ name: 'OFF_TOPIC',
+ permissions: {
+ public: true,
+ self: true,
+ roles: []
+ },
+ models: ['COMMENTS'],
+ created_at: new Date()
+ }
+]
+```
+
+You can refer to `models/schema/tag.js` for the available schema to match when
+creating models to enable/disable specific features.
+
+### router
+
+```js
+(router) => {
+ router.get('/api/v1/people', (req, res) => {
+ res.json({people: [{name: 'Bob'}]});
+ });
+}
+```
+
+The Router hook allows you to create a function that accepts the base express
+router where you can mount any amount of middleware/routes to do any form of
+action needed by external applications.
+
+### passport
+
+```js
+const FacebookStrategy = require('passport-facebook').Strategy;
+const UsersService = require('services/users');
+const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
+
+module.exports = {
+ passport(passport) {
+ passport.use(new FacebookStrategy({
+ clientID: process.env.TALK_FACEBOOK_APP_ID,
+ clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
+ callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
+ passReqToCallback: true,
+ profileFields: ['id', 'displayName', 'picture.type(large)']
+ }, async (req, accessToken, refreshToken, profile, done) => {
+
+ let user;
+ try {
+ user = await UsersService.findOrCreateExternalUser(profile);
+ } catch (err) {
+ return done(err);
+ }
+
+ return ValidateUserLogin(profile, user, done);
+ }));
+ },
+ router(router) {
+
+ // Note that we have to import the passport instance here, it is
+ // instantiated after all the strategies have been mounted.
+ const {passport} = require('services/passport');
+
+ /**
+ * Facebook auth endpoint, this will redirect the user immediately to facebook
+ * for authorization.
+ */
+ router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
+
+ /**
+ * Facebook callback endpoint, this will send the user a html page designed to
+ * send back the user credentials upon successful login.
+ */
+ router.get('/facebook/callback', (req, res, next) => {
+
+ // Perform the facebook login flow and pass the data back through the opener.
+ passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
+ });
+ }
+};
+```
+
+### translations
+
+```js
+const path = require('path');
+
+module.exports = {
+ translations: path.join(__dirname, 'translations.yml'),
+};
+```
+
+Where the `translations.yml` contains:
+
+```yml
+en:
+ embedlink:
+ copy: "Copy Permalink"
+```
+
+Which overrides the copy for the `embedlink.copy` template. You can
+also provide other languages as well by using the correct language
+prefix.
+
+### websockets
+
+```js
+module.exports = {
+ websockets: {
+ onConnect: (connectionParams, connection) => {
+ // Do something with the connection params or connection, like
+ // logging it out, or incrementing a metric.
+ },
+ onDisconnect: (connection) => {
+ // Do something with the connection params or connection, like
+ // logging it out, or decrementing a metric.
+ },
+ },
+}
+```
+
+This `websockets` hook can be used to attach methods to the
+`onConnect` and `onDisconnect` events on a server. The intention for
+this hook is to allow administrators instrument the active websocket
+connections.
+
+### schemaLevelResolveFunction
+
+```js
+module.exports = {
+ schemaLevelResolveFunction: (root, args, ctx, info) => {
+ // The GraphQL Operation Name. Example: CoralEmbedStream_Embed
+ const name = info.operation.name !== null ? info.operation.name.value : null;
+ // Maybe increment a metric based on the operation name...
+
+ // You must _always_ return the root.
+ return root;
+ },
+};
+```
+
+The `schemaLevelResolveFunction` provides a function that is attached
+at the schema level, so that all queries that are made will go through. This
+can be used to create a better view of the graph landscape by creating metrics
+of resolved query names.
+
+## Full Example
+
+Contents of `plugins.json`:
+
+```json
+{
+ "server": [
+ "people"
+ ]
+}
+```
+
+Located in `plugins/people/index.js`:
+
+```js
+module.exports = {
+ typeDefs: `
+ enum COLOUR {
+ RED
+ BLUE
+ }
+
+ type Person {
+ name: String!
+ colour: COLOUR!
+ }
+
+ type RootMutation {
+ createPerson(name: String!): Person
+ }
+
+ type RootQuery {
+ people: [Person!]
+ }
+
+ type Subscription {
+ leader: Person
+ }
+ `,
+ context: {
+ Slack: () => ({
+ notify: (message) => {
+ // return a promise after we're done sending notifications.
+ }
+ })
+ },
+ loaders: ({user}) => ({
+ People: {
+ load: () => db.people.find({user})
+ }
+ }),
+ mutators: ({user}) => ({
+ People: {
+ create: (name) => {
+ return db.people.insert({user, name});
+ }
+ }
+ }),
+ resolvers: {
+ Person: {
+ name(obj, args, context) {
+ return obj.name;
+ },
+ colour(obj, args, context) {
+ // Bill likes the colour red, everyone else likes blue.
+ return obj.name === 'bill' ? 'RED' : 'BLUE';
+ }
+ },
+ RootQuery: {
+ people(obj, args, {loaders: {People}}) {
+ return People.load();
+ }
+ },
+ RootMutation: {
+ createPerson(obj, {name}, {mutators: {People}}) {
+ return People.create(name);
+ }
+ }
+ },
+ hooks: {
+ RootMutation: {
+ createPerson: {
+ post: async (obj, args, {plugins: {Slack}}, info, person) => {
+ if (!person) {
+ return person;
+ }
+
+ await Slack.notify(`A new person just was created with name ${person.name}`);
+
+ return person;
+ }
+ }
+ }
+ },
+ setupFunctions: {
+ leader: (options, args) => ({
+ leader: {
+ filter: (person) => person.place === 1
+ }
+ }
+ }
+};
+
+```
\ No newline at end of file
diff --git a/docs/themes/coral/layout/layout.swig b/docs/themes/coral/layout/layout.swig
index 765c7f8d3..0949267c4 100644
--- a/docs/themes/coral/layout/layout.swig
+++ b/docs/themes/coral/layout/layout.swig
@@ -10,7 +10,10 @@
{{ partial('partial/header') }}
- {{ body }}
+ {% block content %}
+ {{ body }}
+ {% endblock %}
+
{{ partial('partial/footer') }}
diff --git a/docs/themes/coral/layout/page.swig b/docs/themes/coral/layout/page.swig
index f7606381f..36ebe1509 100644
--- a/docs/themes/coral/layout/page.swig
+++ b/docs/themes/coral/layout/page.swig
@@ -1,7 +1,22 @@
+ {% if page.title %}
Suggest Edits
{{ page.title }}
+ {% endif %}
+
+ {% if page.toc === true %}
+
+ {% endif %}
{{ page.content }}
diff --git a/docs/themes/coral/layout/partial/footer.swig b/docs/themes/coral/layout/partial/footer.swig
index 9200187ee..e30d96b58 100644
--- a/docs/themes/coral/layout/partial/footer.swig
+++ b/docs/themes/coral/layout/partial/footer.swig
@@ -7,7 +7,7 @@
{{ js(['js/highlight.min.js', 'js/main.js']) }}
-
+
+
+
+
+ `;
+});
diff --git a/docs/themes/coral/scripts/toc.js b/docs/themes/coral/scripts/toc.js
new file mode 100644
index 000000000..545674d5e
--- /dev/null
+++ b/docs/themes/coral/scripts/toc.js
@@ -0,0 +1,19 @@
+/* global hexo */
+const cheerio = require('cheerio');
+
+// This helper replaces the ol tags outputted by the base toc helper with ui
+// tags.
+hexo.extend.helper.register('oltoul', function(source) {
+ const $ = cheerio.load(source);
+
+ // Sourced from https://stackoverflow.com/a/12679823
+ $(
+ $('ol')
+ .get()
+ .reverse()
+ ).each(function() {
+ $(this).replaceWith($('