added tenant + graph support

This commit is contained in:
Wyatt Johnson
2018-06-16 17:21:04 -06:00
parent 02e1236792
commit 4318e1ddbe
35 changed files with 1024 additions and 269 deletions
+20
View File
@@ -0,0 +1,20 @@
import loaders from './loaders';
import { Db } from 'mongodb';
import { Tenant } from 'talk-server/models/tenant';
export interface ContextOptions {
tenant?: Tenant;
db: Db;
}
export default class TenantContext {
public loaders: ReturnType<typeof loaders>;
public db: Db;
public tenant?: Tenant;
constructor({ tenant, db }: ContextOptions) {
this.tenant = tenant;
this.loaders = loaders(this);
this.db = db;
}
}
@@ -0,0 +1,12 @@
import DataLoader from 'dataloader';
import {
Asset,
retrieveMany as retrieveManyAssets,
} from 'talk-server/models/asset';
import Context from 'talk-server/graph/tenant/context';
export default (ctx: Context) => ({
asset: new DataLoader<string, Asset>(ids =>
retrieveManyAssets(ctx.db, ctx.tenant.id, ids)
),
});
@@ -0,0 +1,25 @@
import DataLoader from 'dataloader';
import {
Comment,
retrieveMany,
retrieveAssetConnection,
ConnectionInput,
retrieveRepliesConnection,
} from 'talk-server/models/comment';
import Context from 'talk-server/graph/tenant/context';
export default (ctx: Context) => ({
comment: new DataLoader((ids: string[]) =>
retrieveMany(ctx.db, ctx.tenant.id, ids)
),
forAsset: (assetID: string, input: ConnectionInput) =>
retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input),
forParent: (assetID: string, parentID: string, input: ConnectionInput) =>
retrieveRepliesConnection(
ctx.db,
ctx.tenant.id,
assetID,
parentID,
input
),
});
@@ -0,0 +1,10 @@
import Assets from './assets';
import Comments from './comments';
import Users from './users';
import Context from 'talk-server/graph/tenant/context';
export default (ctx: Context) => ({
Assets: Assets(ctx),
Comments: Comments(ctx),
Users: Users(ctx),
});
@@ -0,0 +1,9 @@
import DataLoader from 'dataloader';
import { User, retrieveMany } from 'talk-server/models/user';
import Context from 'talk-server/graph/tenant/context';
export default (ctx: Context) => ({
user: new DataLoader<string, User>(ids =>
retrieveMany(ctx.db, ctx.tenant.id, ids)
),
});
@@ -0,0 +1,13 @@
import { graphqlExpress } from 'apollo-server-express';
import schema from './schema';
import TenantContext from './context';
import { Db } from 'mongodb';
import { Tenant } from 'talk-server/models/tenant';
export default (db: Db) =>
graphqlExpress(async req => {
return {
schema,
context: new TenantContext({ db, tenant: { id: '1' } as Tenant }),
};
});
@@ -0,0 +1,8 @@
import { Asset } from 'talk-server/models/asset';
import Context from 'talk-server/graph/tenant/context';
import { ConnectionInput } from 'talk-server/models/comment';
export default {
comments: async (asset: Asset, input: ConnectionInput, ctx: Context) =>
ctx.loaders.Comments.forAsset(asset.id, input),
};
@@ -0,0 +1,9 @@
import { Comment, ConnectionInput } from 'talk-server/models/comment';
import Context from 'talk-server/graph/tenant/context';
export default {
author: async (comment: Comment, _: any, ctx: Context) =>
ctx.loaders.Users.user.load(comment.author_id),
replies: async (comment: Comment, input: ConnectionInput, ctx: Context) =>
ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input),
};
@@ -0,0 +1,72 @@
import { DateTime } from 'luxon';
import { GraphQLScalarType } from 'graphql';
import { Kind } from 'graphql/language';
import { Cursor } from 'talk-server/models/connection';
function parseIntegerCursor(value: string): number {
try {
const cursor = parseInt(value);
return cursor;
} catch (err) {
return null;
}
}
function parseCursor(value: string): Cursor {
if (value.endsWith('Z')) {
const date = DateTime.fromISO(value, {});
if (!date.isValid) {
return parseIntegerCursor(value);
}
return date.toJSDate();
}
return parseIntegerCursor(value);
}
export default new GraphQLScalarType({
name: 'Cursor',
description: 'Cursor represents a paginating cursor.',
serialize(value) {
switch (typeof value) {
case 'object':
if (value instanceof Date) {
return value.toISOString();
} else if (value instanceof DateTime) {
return value.toISO();
}
return null;
case 'number':
return value.toString();
case 'string':
return value;
default:
return null;
}
},
parseValue(value) {
if (typeof value === 'string') {
return parseCursor(value);
}
return null;
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
// This handles an empty string.
if (!ast.value || ast.value.length === 0) {
return null;
}
return parseCursor(ast.value);
case Kind.INT:
return parseIntegerCursor(ast.value);
default:
return null;
}
},
});
@@ -0,0 +1,11 @@
import Asset from './asset';
import Comment from './comment';
import Cursor from './cursor';
import Query from './query';
export default {
Asset,
Comment,
Cursor,
Query,
};
@@ -0,0 +1,12 @@
import Context from 'talk-server/graph/tenant/context';
import { Asset } from 'talk-server/models/asset';
export default {
asset: async (
_: any,
{ id, url }: { id?: string; url: string },
ctx: Context
): Promise<Asset> => {
return ctx.loaders.Assets.asset.load(id);
},
};
@@ -0,0 +1,25 @@
import {
addMockFunctionsToSchema,
addResolveFunctionsToSchema,
} from 'graphql-tools';
import resolvers from 'talk-server/graph/tenant/resolvers';
import { getGraphQLProjectConfig } from 'graphql-config';
// Load the configuration from the provided `.graphqlconfig` file.
const config = getGraphQLProjectConfig(__dirname, 'tenant');
// Get the GraphQLSchema from the configuration.
const schema = config.getSchema();
// Attach the resolvers to the schema.
addResolveFunctionsToSchema({ schema, resolvers });
// // Attach resolvers to the schema.
// addMockFunctionsToSchema({
// schema,
// mocks: {
// Cursor: () => new Date().toISOString(),
// },
// }); // FIXME: remove mocks
export default schema;
@@ -0,0 +1,460 @@
################################################################################
## Custom Scalar Types
################################################################################
"""
Time represented as an ISO8601 string.
"""
scalar Time
"""
Cursor represents a paginating cursor.
"""
scalar Cursor
################################################################################
## Settings
################################################################################
# The moderation mode of the site.
enum MODERATION_MODE {
"""
Comments posted while in `PRE` mode will be labeled with a `PREMOD`
status and will require a moderator decision before being visible.
"""
PRE
"""
Comments posted while in `POST` will be visible immediately.
"""
POST
}
"""
Wordlist describes all the available wordlists.
"""
type Wordlist {
"""
banned words will by default reject the comment if it is found.
"""
banned: [String!]!
"""
suspect words will simply flag the comment.
"""
suspect: [String!]!
}
# Settings stores the global settings for a given installation.
type Settings {
"""
moderation is the moderation mode for all Asset's on the site.
"""
moderation: MODERATION_MODE!
"""
Enables a requirement for email confirmation before a user can login.
"""
requireEmailConfirmation: Boolean!
"""
infoBoxEnable will enable the Info Box content visible above the question
box.
"""
infoBoxEnable: Boolean!
"""
infoBoxContent is the content of the Info Box.
"""
infoBoxContent: String
"""
questionBoxEnable will enable the Question Box's content to be visible above
the comment box.
"""
questionBoxEnable: Boolean!
"""
questionBoxContent is the content of the Question Box.
"""
questionBoxContent: String
"""
questionBoxIcon is the icon for the Question Box.
"""
questionBoxIcon: String
"""
premodLinksEnable will put all comments that contain links into premod.
"""
premodLinksEnable: Boolean!
"""
autoCloseStream when true will auto close the stream when the `closeTimeout`
amount of seconds have been reached.
"""
autoCloseStream: Boolean!
"""
customCssUrl is the URL of the custom CSS used to display on the frontend.
"""
customCssUrl: String
"""
closedTimeout is the amount of seconds from the created_at timestamp that a
given asset will be considered closed.
"""
closedTimeout: Int!
"""
closedMessage is the message shown to the user when the given Asset is
closed.
"""
closedMessage: String
"""
disableCommenting will disable commenting site-wide.
"""
disableCommenting: Boolean!
"""
disableCommentingMessage will be shown above the comment stream while
commenting is disabled site-wide.
"""
disableCommentingMessage: String
"""
editCommentWindowLength is the length of time (in milliseconds) after a
comment is posted that it can still be edited by the author.
"""
editCommentWindowLength: Int!
"""
charCountEnable is true when the character count restriction is enabled.
"""
charCountEnable: Boolean!
"""
charCount is the maximum number of characters a comment may be.
"""
charCount: Int
"""
organizationName is the name of the organization.
"""
organizationName: String
"""
organizationContactEmail is the email of the organization.
"""
organizationContactEmail: String
"""
wordlist will return a given list of words.
"""
wordlist: Wordlist!
"""
domains will return a given list of whitelisted domains.
"""
domains: [String!]!
}
################################################################################
## User
################################################################################
"""
User is someone that leaves Comments, and logs in.
"""
type User {
"""
id is the identifier of the User.
"""
id: ID!
"""
username is the name of the User visible to other Users.
"""
username: String!
}
################################################################################
## Comment
################################################################################
enum COMMENT_STATUS {
NONE
ACCEPTED
}
"""
Comment is a comment left by a User on an Asset or another Comment as a reply.
"""
type Comment {
"""
id is the identifier of the Comment.
"""
id: ID!
"""
body is the content of the Comment.
"""
body: String
"""
author is the User that authored the Comment.
"""
author: User
"""
status represents the Comment's current Status.
"""
status: COMMENT_STATUS!
"""
replyCount is the number of replies. Only direct replies to this Comment
are counted. Deleted comments are included in this count.
"""
replyCount: Int
"""
replies will return the replies to this comment.
"""
replies(
first: Int = 10
orderBy: COMMENT_SORT = CREATED_AT_DESC
after: Cursor
): CommentsConnection
}
type PageInfo {
"""
Cursor of first node in subset.
"""
startCursor: Cursor
"""
Cursor of last node in subset.
"""
endCursor: Cursor
"""
Indicates that there are more nodes after this subset.
"""
hasNextPage: Boolean!
}
"""
CommentEdge represents a unique Comment in a CommentConnection.
"""
type CommentEdge {
"""
node is the Comment for this edge.
"""
node: Comment
"""
"""
cursor: Cursor
}
"""
CommentsConnection represents a subset of a comment list.
"""
type CommentsConnection {
"""
edges are a subset of CommentEdge's.
"""
edges: [CommentEdge!]!
"""
pageInfo is
"""
pageInfo: PageInfo!
}
################################################################################
## Asset
################################################################################
enum COMMENT_SORT {
CREATED_AT_DESC
CREATED_AT_ASC
REPLIES_DESC
RESPECT_DESC
}
"""
Asset is an Article or Page where Comments are written on by Users.
"""
type Asset {
"""
id is the identifier of the Asset.
"""
id: ID!
"""
url is the url that the Asset is located on.
"""
url: String!
"""
title is the title of the scraped Asset.
"""
title: String
"""
comments are the comments on the Asset.
"""
comments(
first: Int = 10
orderBy: COMMENT_SORT = CREATED_AT_DESC
after: Cursor
): CommentsConnection
"""
author is the authors listed in the meta tags for the Asset.
"""
author: String
"""
closedAt is the Time that the Asset is closed for commenting.
"""
closedAt: Time
"""
isClosed returns true when the Asset is currently closed for commenting.
"""
isClosed: Boolean!
"""
createdAt is the date that the Asset was created at.
"""
createdAt: Time!
}
"""
AssetsConnection represents a subset of a Asset list.
"""
type AssetsConnection {
"""
Indicates that there are more Assets after this subset.
"""
hasNextPage: Boolean!
"""
Cursor of first Asset in subset.
"""
startCursor: Cursor
"""
Cursor of last Asset in subset.
"""
endCursor: Cursor
"""
Subset of Assets.
"""
nodes: [Asset!]!
}
################################################################################
## Query
################################################################################
type Query {
"""
comment returns a specific comment.
"""
comment(id: ID!): Comment
"""
assets returns a AssetsConnection.
"""
assets(cursor: Cursor, limit: Int = 10): AssetsConnection
"""
asset is the Asset specified by its ID.
"""
asset(id: ID!): Asset
"""
me is the current logged in User.
"""
me: User
"""
settings is the Settings for a given Tenant.
"""
settings: Settings!
}
################################################################################
## Mutations
################################################################################
##################
## createComment
##################
"""
CreateCommentInput provides the input for the createComment Mutation.
"""
input CreateCommentInput {
"""
assetID is the ID of the Asset where we are creating a comment on.
"""
assetID: ID!
"""
parentID is the optional ID of the Comment that we are replying to.
"""
parentID: ID
"""
body is the Comment body, the content of the Comment.
"""
body: String!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
"""
CreateCommentPayload contains the created Comment after the createComment
mutation.
"""
type CreateCommentPayload {
"""
comment is the possibly created comment.
"""
comment: Comment
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
##################
## Mutation
##################
type Mutation {
"""
createComment will create a Comment as the current logged in User.
"""
createComment(input: CreateCommentInput!): CreateCommentPayload
}
################################################################################
## Subscriptions
################################################################################
type Subscription {
commentCreated(assetID: ID!): Comment
}