mirror of
https://github.com/wassname/talk.git
synced 2026-08-02 13:10:23 +08:00
added tenant + graph support
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user