mirror of
https://github.com/wassname/talk.git
synced 2026-07-31 12:50:48 +08:00
[CORL 133] API Review (#2197)
* refactor: removed unused subscription code
* refactor: removed management api's
* refactor: cleanup of connections
* refactor: refactored comments edge
* refactor: simplified connection resolving
* feat: added story connection edge
* fix: added story index
* feat: added user pagination and user edge
* fix: added filter to comment query
* fix: removed unused resolvers
* fix: creating a comment reply should require auth
* refactor: cleanup of graph files
* feat: removed display name, made username non-unique
* fix: fixed tests
* fix: fixed tests
* fix: added more api docs
* fix: fixed bug with installer
* refactor: fixes and updates
* fix: added linting for graphql, fixed schema
* feat: added docker build tests
* fix: upped output timeout
* fix: fixed stacktraces in production builds
* fix: removed `git add`
- `git add` was causing issues with
partial staged changs on files
* feat: improved error messaging for auth
* refactor: cleaned up queue names
* fix: merge error
This commit is contained in:
@@ -22,7 +22,7 @@ export interface AuthDirectiveArgs {
|
||||
function calculateAuthConditions(user: User): GQLUSER_AUTH_CONDITIONS[] {
|
||||
const conditions: GQLUSER_AUTH_CONDITIONS[] = [];
|
||||
|
||||
if (!user.username && !user.displayName) {
|
||||
if (!user.username) {
|
||||
conditions.push(GQLUSER_AUTH_CONDITIONS.MISSING_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./ErrorWrappingExtension";
|
||||
export * from "./LoggerExtension";
|
||||
@@ -1,62 +0,0 @@
|
||||
import { GraphQLOptions } from "apollo-server-express";
|
||||
import { Handler } from "express";
|
||||
import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql";
|
||||
|
||||
// TODO: when https://github.com/apollographql/apollo-server/pull/1907 is merged, update this import path
|
||||
import {
|
||||
ExpressGraphQLOptionsFunction,
|
||||
graphqlExpress,
|
||||
} from "apollo-server-express/dist/expressApollo";
|
||||
|
||||
import { Omit } from "talk-common/types";
|
||||
import { Config } from "talk-server/config";
|
||||
|
||||
import { ErrorWrappingExtension } from "./extensions/ErrorWrappingExtension";
|
||||
import { LoggerExtension } from "./extensions/LoggerExtension";
|
||||
|
||||
// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57
|
||||
const NoIntrospection = (context: ValidationContext) => ({
|
||||
Field(node: FieldDefinitionNode) {
|
||||
if (node.name.value === "__schema" || node.name.value === "__type") {
|
||||
context.reportError(
|
||||
new GraphQLError(
|
||||
"GraphQL introspection is not allowed in production, but the query contained __schema or __type.",
|
||||
[node]
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const graphqlMiddleware = (
|
||||
config: Config,
|
||||
requestOptions: ExpressGraphQLOptionsFunction
|
||||
): Handler => {
|
||||
// Create a new baseOptions that will be merged into the new options.
|
||||
const baseOptions: Omit<GraphQLOptions, "schema"> = {
|
||||
// Disable the debug mode, as we already add in our logging function.
|
||||
debug: false,
|
||||
// Include extensions.
|
||||
extensions: [
|
||||
() => new ErrorWrappingExtension(),
|
||||
() => new LoggerExtension(),
|
||||
],
|
||||
};
|
||||
|
||||
if (config.get("env") === "production" && !config.get("enable_graphiql")) {
|
||||
// Disable introspection in production.
|
||||
baseOptions.validationRules = [NoIntrospection];
|
||||
}
|
||||
|
||||
// Generate the actual middleware.
|
||||
return graphqlExpress(async (req, res) => {
|
||||
// Resolve the options for the GraphQL middleware.
|
||||
const options = await requestOptions(req, res);
|
||||
|
||||
// Provide the options.
|
||||
return {
|
||||
...options,
|
||||
...baseOptions,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { execute, GraphQLSchema, subscribe } from "graphql";
|
||||
import http from "http";
|
||||
import { SubscriptionServer } from "subscriptions-transport-ws";
|
||||
|
||||
export interface SubscriptionMiddlewareOptions {
|
||||
schema: GraphQLSchema;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export function handleSubscriptions(
|
||||
server: http.Server,
|
||||
{ schema, path }: SubscriptionMiddlewareOptions
|
||||
): SubscriptionServer {
|
||||
// Configure some options for the subscription system.
|
||||
const options = {
|
||||
schema,
|
||||
execute,
|
||||
subscribe,
|
||||
};
|
||||
|
||||
// Configure the socket options for the websocket server. It needs to handle
|
||||
// upgrade requests on that route.
|
||||
const socketOption = {
|
||||
server,
|
||||
path,
|
||||
};
|
||||
|
||||
return new SubscriptionServer(options, socketOption);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { RedisPubSub } from "graphql-redis-subscriptions";
|
||||
import { Config } from "talk-server/config";
|
||||
import { createRedisClient } from "talk-server/services/redis";
|
||||
|
||||
export async function createPubSub(config: Config): Promise<RedisPubSub> {
|
||||
// Create the Redis clients for the PubSub server.
|
||||
const publisher = await createRedisClient(config);
|
||||
const subscriber = await createRedisClient(config);
|
||||
|
||||
// Create the new PubSub manager.
|
||||
return new RedisPubSub({
|
||||
publisher,
|
||||
subscriber,
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-server/config";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import { I18n } from "talk-server/services/i18n";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
export interface ManagementContextOptions {
|
||||
mongo: Db;
|
||||
config: Config;
|
||||
i18n: I18n;
|
||||
req?: Request;
|
||||
}
|
||||
|
||||
export default class ManagementContext extends CommonContext {
|
||||
public readonly mongo: Db;
|
||||
|
||||
constructor({ req, mongo, config, i18n }: ManagementContextOptions) {
|
||||
super({ req, config, i18n });
|
||||
|
||||
this.mongo = mongo;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { GraphQLSchema } from "graphql";
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-server/config";
|
||||
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
import { I18n } from "talk-server/services/i18n";
|
||||
import ManagementContext from "./context";
|
||||
|
||||
export interface ManagementGraphQLMiddlewareOptions {
|
||||
schema: GraphQLSchema;
|
||||
config: Config;
|
||||
mongo: Db;
|
||||
i18n: I18n;
|
||||
}
|
||||
|
||||
export default ({
|
||||
schema,
|
||||
config,
|
||||
mongo,
|
||||
i18n,
|
||||
}: ManagementGraphQLMiddlewareOptions) =>
|
||||
graphqlMiddleware(config, async (req: Request) => ({
|
||||
schema,
|
||||
context: new ManagementContext({ req, mongo, config, i18n }),
|
||||
}));
|
||||
@@ -1,9 +0,0 @@
|
||||
import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/management/schema/__generated__/types";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
Time,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { IResolvers } from "graphql-tools";
|
||||
|
||||
import { loadSchema } from "talk-common/graphql";
|
||||
import resolvers from "talk-server/graph/management/resolvers";
|
||||
|
||||
export default function getManagementSchema() {
|
||||
return loadSchema("management", resolvers as IResolvers);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
################################################################################
|
||||
## Custom Scalar Types
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Time represented as an ISO8601 string.
|
||||
"""
|
||||
scalar Time
|
||||
|
||||
################################################################################
|
||||
## Tenant
|
||||
################################################################################
|
||||
|
||||
type Tenant {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
"""
|
||||
organizationName: String
|
||||
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
"""
|
||||
organizationContactEmail: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Query
|
||||
################################################################################
|
||||
|
||||
type Query {
|
||||
tenant(id: ID!): Tenant
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { GraphQLSchema } from "graphql";
|
||||
|
||||
export interface Schemas {
|
||||
management: GraphQLSchema;
|
||||
tenant: GraphQLSchema;
|
||||
}
|
||||
@@ -1,30 +1,27 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-server/config";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import CommonContext, {
|
||||
CommonContextOptions,
|
||||
} from "talk-server/graph/common/context";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
import { TaskQueue } from "talk-server/queue";
|
||||
import { MailerQueue } from "talk-server/queue/tasks/mailer";
|
||||
import { ScraperQueue } from "talk-server/queue/tasks/scraper";
|
||||
import { JWTSigningConfig } from "talk-server/services/jwt";
|
||||
import { AugmentedRedis } from "talk-server/services/redis";
|
||||
import TenantCache from "talk-server/services/tenant/cache";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
import { I18n } from "talk-server/services/i18n";
|
||||
import loaders from "./loaders";
|
||||
import mutators from "./mutators";
|
||||
|
||||
export interface TenantContextOptions {
|
||||
export interface TenantContextOptions extends CommonContextOptions {
|
||||
mongo: Db;
|
||||
redis: AugmentedRedis;
|
||||
tenant: Tenant;
|
||||
tenantCache: TenantCache;
|
||||
queue: TaskQueue;
|
||||
config: Config;
|
||||
mailerQueue: MailerQueue;
|
||||
scraperQueue: ScraperQueue;
|
||||
signingConfig?: JWTSigningConfig;
|
||||
req?: Request;
|
||||
user?: User;
|
||||
i18n: I18n;
|
||||
}
|
||||
|
||||
export default class TenantContext extends CommonContext {
|
||||
@@ -32,33 +29,24 @@ export default class TenantContext extends CommonContext {
|
||||
public readonly tenantCache: TenantCache;
|
||||
public readonly mongo: Db;
|
||||
public readonly redis: AugmentedRedis;
|
||||
public readonly queue: TaskQueue;
|
||||
public readonly mailerQueue: MailerQueue;
|
||||
public readonly scraperQueue: ScraperQueue;
|
||||
public readonly loaders: ReturnType<typeof loaders>;
|
||||
public readonly mutators: ReturnType<typeof mutators>;
|
||||
public readonly user?: User;
|
||||
public readonly signingConfig?: JWTSigningConfig;
|
||||
|
||||
constructor({
|
||||
req,
|
||||
user,
|
||||
tenant,
|
||||
mongo,
|
||||
redis,
|
||||
config,
|
||||
tenantCache,
|
||||
queue,
|
||||
signingConfig,
|
||||
i18n,
|
||||
}: TenantContextOptions) {
|
||||
super({ user, req, config, i18n, lang: tenant.locale });
|
||||
constructor(options: TenantContextOptions) {
|
||||
super({ ...options, lang: options.tenant.locale });
|
||||
|
||||
this.tenant = tenant;
|
||||
this.tenantCache = tenantCache;
|
||||
this.user = user;
|
||||
this.mongo = mongo;
|
||||
this.redis = redis;
|
||||
this.queue = queue;
|
||||
this.signingConfig = signingConfig;
|
||||
this.tenant = options.tenant;
|
||||
this.tenantCache = options.tenantCache;
|
||||
this.user = options.user;
|
||||
this.mongo = options.mongo;
|
||||
this.redis = options.redis;
|
||||
this.scraperQueue = options.scraperQueue;
|
||||
this.mailerQueue = options.mailerQueue;
|
||||
this.signingConfig = options.signingConfig;
|
||||
this.loaders = loaders(this);
|
||||
this.mutators = mutators(this);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
CommentModerationActionFilter,
|
||||
retrieveCommentModerationActionConnection,
|
||||
retrieveCommentModerationActions,
|
||||
} from "talk-server/models/action/moderation/comment";
|
||||
import { UserToCommentModerationActionHistoryArgs } from "../schema/__generated__/types";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
commentModerationActions: (filter: CommentModerationActionFilter) =>
|
||||
retrieveCommentModerationActions(ctx.mongo, ctx.tenant.id, filter),
|
||||
commentModerationActionsConnection: (
|
||||
{ first = 10, after }: UserToCommentModerationActionHistoryArgs,
|
||||
moderatorID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: {
|
||||
moderatorID,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
CommentToStatusHistoryArgs,
|
||||
UserToCommentModerationActionHistoryArgs,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { retrieveCommentModerationActionConnection } from "talk-server/models/action/moderation/comment";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
forModerator: (
|
||||
{ first = 10, after }: UserToCommentModerationActionHistoryArgs,
|
||||
moderatorID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: {
|
||||
moderatorID,
|
||||
},
|
||||
}),
|
||||
forComment: (
|
||||
{ first = 10, after }: CommentToStatusHistoryArgs,
|
||||
commentID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: {
|
||||
commentID,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import DataLoader from "dataloader";
|
||||
import { isNil, omitBy } from "lodash";
|
||||
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
@@ -33,9 +34,9 @@ import { SingletonResolver } from "./util";
|
||||
const primeCommentsFromConnection = (ctx: Context) => (
|
||||
connection: Readonly<Connection<Readonly<Comment>>>
|
||||
) => {
|
||||
// For each of the edges, prime the comment loader.
|
||||
connection.edges.forEach(({ node }) => {
|
||||
ctx.loaders.Comments.comment.prime(node.id, node);
|
||||
// For each of the nodes, prime the comment loader.
|
||||
connection.nodes.forEach(comment => {
|
||||
ctx.loaders.Comments.comment.prime(comment.id, comment);
|
||||
});
|
||||
|
||||
return connection;
|
||||
@@ -45,22 +46,35 @@ export default (ctx: Context) => ({
|
||||
comment: new DataLoader((ids: string[]) =>
|
||||
retrieveManyComments(ctx.mongo, ctx.tenant.id, ids)
|
||||
),
|
||||
forFilter: ({ first = 10, after, filter }: QueryToCommentsArgs) =>
|
||||
forFilter: ({ first = 10, after, storyID, status }: QueryToCommentsArgs) =>
|
||||
retrieveCommentConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
filter,
|
||||
filter: omitBy(
|
||||
{
|
||||
storyID,
|
||||
status,
|
||||
},
|
||||
isNil
|
||||
),
|
||||
}).then(primeCommentsFromConnection(ctx)),
|
||||
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
|
||||
(commentIDs: string[]) =>
|
||||
retrieveManyUserActionPresence(
|
||||
(commentIDs: string[]) => {
|
||||
if (!ctx.user) {
|
||||
// This should only ever be accessed when a user is logged in. It should
|
||||
// be safe to get the user here, but we'll throw an error anyways just
|
||||
// in case.
|
||||
throw new Error("can't get action presense of an undefined user");
|
||||
}
|
||||
|
||||
return retrieveManyUserActionPresence(
|
||||
ctx.mongo,
|
||||
ctx.tenant.id,
|
||||
// This should only ever be accessed when a user is logged in.
|
||||
ctx.user!.id,
|
||||
ctx.user.id,
|
||||
commentIDs
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
forUser: (
|
||||
userID: string,
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
import DataLoader from "dataloader";
|
||||
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { GQLStoryMetadata } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
GQLSTORY_STATUS,
|
||||
GQLStoryMetadata,
|
||||
QueryToStoriesArgs,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Connection } from "talk-server/models/helpers/connection";
|
||||
import {
|
||||
FindOrCreateStoryInput,
|
||||
retrieveManyStories,
|
||||
retrieveStoryConnection,
|
||||
Story,
|
||||
StoryConnectionInput,
|
||||
} from "talk-server/models/story";
|
||||
import { findOrCreate } from "talk-server/services/stories";
|
||||
import { scraper } from "talk-server/services/stories/scraper";
|
||||
|
||||
const statusFilter = (
|
||||
status?: GQLSTORY_STATUS
|
||||
): StoryConnectionInput["filter"] => {
|
||||
switch (status) {
|
||||
case GQLSTORY_STATUS.OPEN:
|
||||
return {
|
||||
closedAt: null,
|
||||
};
|
||||
case GQLSTORY_STATUS.CLOSED:
|
||||
return {
|
||||
closedAt: { $lte: new Date() },
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* primeStoriesFromConnection will prime a given context with the stories
|
||||
* retrieved via a connection.
|
||||
*
|
||||
* @param ctx graph context to use to prime the loaders.
|
||||
*/
|
||||
const primeStoriesFromConnection = (ctx: TenantContext) => (
|
||||
connection: Readonly<Connection<Readonly<Story>>>
|
||||
) => {
|
||||
// For each of these nodes, prime the story loader.
|
||||
connection.nodes.forEach(story => {
|
||||
ctx.loaders.Stories.story.prime(story.id, story);
|
||||
});
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
findOrCreate: new DataLoader(
|
||||
(inputs: FindOrCreateStoryInput[]) =>
|
||||
Promise.all(
|
||||
inputs.map(input =>
|
||||
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.queue.scraper)
|
||||
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.scraperQueue)
|
||||
)
|
||||
),
|
||||
{
|
||||
@@ -26,6 +67,15 @@ export default (ctx: TenantContext) => ({
|
||||
story: new DataLoader<string, Story | null>(ids =>
|
||||
retrieveManyStories(ctx.mongo, ctx.tenant.id, ids)
|
||||
),
|
||||
connection: ({ first = 10, after, status }: QueryToStoriesArgs) =>
|
||||
retrieveStoryConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: {
|
||||
// Merge the status filter into the connection filter.
|
||||
...statusFilter(status),
|
||||
},
|
||||
}).then(primeStoriesFromConnection(ctx)),
|
||||
debugScrapeMetadata: new DataLoader<string, GQLStoryMetadata | null>(urls =>
|
||||
Promise.all(urls.map(url => scraper.scrape(url)))
|
||||
),
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import DataLoader from "dataloader";
|
||||
import { isNil, omitBy } from "lodash";
|
||||
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
import { retrieveManyUsers, User } from "talk-server/models/user";
|
||||
import { QueryToUsersArgs } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Connection } from "talk-server/models/helpers/connection";
|
||||
import {
|
||||
retrieveManyUsers,
|
||||
retrieveUserConnection,
|
||||
User,
|
||||
} from "talk-server/models/user";
|
||||
|
||||
/**
|
||||
* primeUsersFromConnection will prime a given context with the users retrieved
|
||||
* via a connection.
|
||||
*
|
||||
* @param ctx graph context to use to prime the loaders.
|
||||
*/
|
||||
const primeUsersFromConnection = (ctx: Context) => (
|
||||
connection: Readonly<Connection<Readonly<User>>>
|
||||
) => {
|
||||
// For each of the nodes, prime the user loader.
|
||||
connection.nodes.forEach(user => {
|
||||
ctx.loaders.Users.user.prime(user.id, user);
|
||||
});
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
export default (ctx: Context) => {
|
||||
const user = new DataLoader<string, User | null>(ids =>
|
||||
@@ -14,5 +39,11 @@ export default (ctx: Context) => {
|
||||
|
||||
return {
|
||||
user,
|
||||
connection: ({ first = 10, after, role }: QueryToUsersArgs) =>
|
||||
retrieveUserConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: omitBy({ role }, isNil),
|
||||
}).then(primeUsersFromConnection(ctx)),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
|
||||
import Actions from "./Actions";
|
||||
import Auth from "./Auth";
|
||||
import CommentModerationActions from "./CommentModerationActions";
|
||||
import Comments from "./Comments";
|
||||
import Stories from "./Stories";
|
||||
import Users from "./Users";
|
||||
|
||||
export default (ctx: Context) => ({
|
||||
Auth: Auth(ctx),
|
||||
Actions: Actions(ctx),
|
||||
CommentModerationActions: CommentModerationActions(ctx),
|
||||
Stories: Stories(ctx),
|
||||
Comments: Comments(ctx),
|
||||
Users: Users(ctx),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { GraphQLSchema } from "graphql";
|
||||
|
||||
import { graphqlBatchMiddleware } from "talk-server/app/middleware/graphqlBatch";
|
||||
import { Config } from "talk-server/config";
|
||||
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
export interface TenantGraphQLMiddlewareOptions {
|
||||
schema: GraphQLSchema;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export default async ({ schema, config }: TenantGraphQLMiddlewareOptions) =>
|
||||
graphqlBatchMiddleware(
|
||||
graphqlMiddleware(config, async (req: Request) => {
|
||||
if (!req.talk) {
|
||||
throw new Error("talk was not set");
|
||||
}
|
||||
|
||||
const { context } = req.talk;
|
||||
if (!context) {
|
||||
throw new Error("context was not set");
|
||||
}
|
||||
|
||||
const { tenant } = context;
|
||||
if (!tenant) {
|
||||
throw new Error("tenant was not set");
|
||||
}
|
||||
|
||||
// Return the graph options.
|
||||
return {
|
||||
schema,
|
||||
context: tenant,
|
||||
};
|
||||
})
|
||||
);
|
||||
+1
-1
@@ -23,7 +23,7 @@ import {
|
||||
|
||||
import { validateMaximumLength } from "./util";
|
||||
|
||||
export const Comment = (ctx: TenantContext) => ({
|
||||
export const Comments = (ctx: TenantContext) => ({
|
||||
create: ({
|
||||
clientMutationId,
|
||||
...comment
|
||||
+7
-17
@@ -10,14 +10,12 @@ import {
|
||||
GQLScrapeStoryInput,
|
||||
GQLUpdateStoryInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import * as story from "talk-server/models/story";
|
||||
import { Story } from "talk-server/models/story";
|
||||
import { create, merge, remove, update } from "talk-server/services/stories";
|
||||
import { scrape } from "talk-server/services/stories/scraper";
|
||||
|
||||
export const Story = (ctx: TenantContext) => ({
|
||||
create: async (
|
||||
input: GQLCreateStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
export const Stories = (ctx: TenantContext) => ({
|
||||
create: async (input: GQLCreateStoryInput): Promise<Readonly<Story> | null> =>
|
||||
mapFieldsetToErrorCodes(
|
||||
create(
|
||||
ctx.mongo,
|
||||
@@ -33,9 +31,7 @@ export const Story = (ctx: TenantContext) => ({
|
||||
],
|
||||
}
|
||||
),
|
||||
update: async (
|
||||
input: GQLUpdateStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
update: async (input: GQLUpdateStoryInput): Promise<Readonly<Story> | null> =>
|
||||
mapFieldsetToErrorCodes(
|
||||
update(ctx.mongo, ctx.tenant, input.id, omitBy(input.story, isNull)),
|
||||
{
|
||||
@@ -45,9 +41,7 @@ export const Story = (ctx: TenantContext) => ({
|
||||
],
|
||||
}
|
||||
),
|
||||
merge: async (
|
||||
input: GQLMergeStoriesInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
merge: async (input: GQLMergeStoriesInput): Promise<Readonly<Story> | null> =>
|
||||
merge(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
@@ -55,12 +49,8 @@ export const Story = (ctx: TenantContext) => ({
|
||||
input.destinationID,
|
||||
input.sourceIDs
|
||||
),
|
||||
remove: async (
|
||||
input: GQLRemoveStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
remove: async (input: GQLRemoveStoryInput): Promise<Readonly<Story> | null> =>
|
||||
remove(ctx.mongo, ctx.tenant, input.id, input.includeComments),
|
||||
scrape: async (
|
||||
input: GQLScrapeStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
scrape: async (input: GQLScrapeStoryInput): Promise<Readonly<Story> | null> =>
|
||||
scrape(ctx.mongo, ctx.tenant.id, input.id),
|
||||
});
|
||||
+6
-14
@@ -1,7 +1,7 @@
|
||||
import { ERROR_CODES } from "talk-common/errors";
|
||||
import { mapFieldsetToErrorCodes } from "talk-server/graph/common/errors";
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import * as user from "talk-server/models/user";
|
||||
import { User } from "talk-server/models/user";
|
||||
import {
|
||||
createToken,
|
||||
deactivateToken,
|
||||
@@ -9,13 +9,11 @@ import {
|
||||
setPassword,
|
||||
setUsername,
|
||||
updateAvatar,
|
||||
updateDisplayName,
|
||||
updateEmail,
|
||||
updatePassword,
|
||||
updateRole,
|
||||
updateUsername,
|
||||
} from "talk-server/services/users";
|
||||
|
||||
import {
|
||||
GQLCreateTokenInput,
|
||||
GQLDeactivateTokenInput,
|
||||
@@ -24,16 +22,15 @@ import {
|
||||
GQLSetUsernameInput,
|
||||
GQLUpdatePasswordInput,
|
||||
GQLUpdateUserAvatarInput,
|
||||
GQLUpdateUserDisplayNameInput,
|
||||
GQLUpdateUserEmailInput,
|
||||
GQLUpdateUserRoleInput,
|
||||
GQLUpdateUserUsernameInput,
|
||||
} from "../schema/__generated__/types";
|
||||
|
||||
export const User = (ctx: TenantContext) => ({
|
||||
export const Users = (ctx: TenantContext) => ({
|
||||
setUsername: async (
|
||||
input: GQLSetUsernameInput
|
||||
): Promise<Readonly<user.User> | null> =>
|
||||
): Promise<Readonly<User> | null> =>
|
||||
mapFieldsetToErrorCodes(
|
||||
setUsername(ctx.mongo, ctx.tenant, ctx.user!, input.username),
|
||||
{
|
||||
@@ -42,13 +39,10 @@ export const User = (ctx: TenantContext) => ({
|
||||
ERROR_CODES.USERNAME_CONTAINS_INVALID_CHARACTERS,
|
||||
ERROR_CODES.USERNAME_EXCEEDS_MAX_LENGTH,
|
||||
ERROR_CODES.USERNAME_TOO_SHORT,
|
||||
ERROR_CODES.DUPLICATE_USERNAME,
|
||||
],
|
||||
}
|
||||
),
|
||||
setEmail: async (
|
||||
input: GQLSetEmailInput
|
||||
): Promise<Readonly<user.User> | null> =>
|
||||
setEmail: async (input: GQLSetEmailInput): Promise<Readonly<User> | null> =>
|
||||
mapFieldsetToErrorCodes(
|
||||
setEmail(ctx.mongo, ctx.tenant, ctx.user!, input.email),
|
||||
{
|
||||
@@ -62,11 +56,11 @@ export const User = (ctx: TenantContext) => ({
|
||||
),
|
||||
setPassword: async (
|
||||
input: GQLSetPasswordInput
|
||||
): Promise<Readonly<user.User> | null> =>
|
||||
): Promise<Readonly<User> | null> =>
|
||||
setPassword(ctx.mongo, ctx.tenant, ctx.user!, input.password),
|
||||
updatePassword: async (
|
||||
input: GQLUpdatePasswordInput
|
||||
): Promise<Readonly<user.User> | null> =>
|
||||
): Promise<Readonly<User> | null> =>
|
||||
updatePassword(ctx.mongo, ctx.tenant, ctx.user!, input.password),
|
||||
createToken: async (input: GQLCreateTokenInput) =>
|
||||
createToken(
|
||||
@@ -81,8 +75,6 @@ export const User = (ctx: TenantContext) => ({
|
||||
deactivateToken(ctx.mongo, ctx.tenant, ctx.user!, input.id),
|
||||
updateUserUsername: async (input: GQLUpdateUserUsernameInput) =>
|
||||
updateUsername(ctx.mongo, ctx.tenant, input.userID, input.username),
|
||||
updateUserDisplayName: async (input: GQLUpdateUserDisplayNameInput) =>
|
||||
updateDisplayName(ctx.mongo, ctx.tenant, input.userID, input.displayName),
|
||||
updateUserEmail: async (input: GQLUpdateUserEmailInput) =>
|
||||
updateEmail(ctx.mongo, ctx.tenant, input.userID, input.email),
|
||||
updateUserAvatar: async (input: GQLUpdateUserAvatarInput) =>
|
||||
@@ -1,15 +1,15 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
|
||||
import { Actions } from "./Actions";
|
||||
import { Comment } from "./Comment";
|
||||
import { Comments } from "./Comments";
|
||||
import { Settings } from "./Settings";
|
||||
import { Story } from "./Story";
|
||||
import { User } from "./User";
|
||||
import { Stories } from "./Stories";
|
||||
import { Users } from "./Users";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
Actions: Actions(ctx),
|
||||
Comment: Comment(ctx),
|
||||
Comments: Comments(ctx),
|
||||
Settings: Settings(ctx),
|
||||
Story: Story(ctx),
|
||||
User: User(ctx),
|
||||
Stories: Stories(ctx),
|
||||
Users: Users(ctx),
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as comment from "talk-server/models/comment";
|
||||
import { getLatestRevision } from "talk-server/models/comment";
|
||||
import { createConnection } from "talk-server/models/helpers/connection";
|
||||
|
||||
import { getCommentEditableUntilDate } from "talk-server/services/comments";
|
||||
|
||||
import TenantContext from "../context";
|
||||
import { getURLWithCommentID } from "./util";
|
||||
|
||||
@@ -55,13 +55,14 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
|
||||
}),
|
||||
author: (c, input, ctx) => ctx.loaders.Users.user.load(c.authorID),
|
||||
statusHistory: ({ id }, input, ctx) =>
|
||||
ctx.loaders.Actions.commentModerationActions({
|
||||
commentID: id,
|
||||
}),
|
||||
ctx.loaders.CommentModerationActions.forComment(input, id),
|
||||
replies: (c, input, ctx) =>
|
||||
// If there is at least one reply, then use the connection loader, otherwise
|
||||
// return a blank connection.
|
||||
c.replyCount > 0
|
||||
? ctx.loaders.Comments.forParent(c.storyID, c.id, input)
|
||||
: createConnection(),
|
||||
// Action Counts are encoded, decode them for use with the GraphQL system.
|
||||
actionCounts: c => decodeActionCounts(c.actionCounts),
|
||||
myActionPresence: (c, input, ctx) =>
|
||||
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as actions from "talk-server/models/action/moderation/comment";
|
||||
|
||||
import { GQLCommentModerationActionTypeResolver } from "../schema/__generated__/types";
|
||||
|
||||
export const CommentModerationAction: GQLCommentModerationActionTypeResolver<
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GQLCommentRevisionTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as comment from "talk-server/models/comment";
|
||||
import { Comment, Revision } from "talk-server/models/comment";
|
||||
|
||||
export interface WrappedCommentRevision {
|
||||
revision: comment.Revision;
|
||||
comment: comment.Comment;
|
||||
revision: Revision;
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export const CommentRevision: Required<
|
||||
|
||||
@@ -8,8 +8,6 @@ import { reconstructTenantURLResolver } from "./util";
|
||||
export const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
|
||||
GQLFacebookAuthIntegration
|
||||
> = {
|
||||
callbackURL: reconstructTenantURLResolver(
|
||||
"/api/tenant/auth/facebook/callback"
|
||||
),
|
||||
redirectURL: reconstructTenantURLResolver("/api/tenant/auth/facebook"),
|
||||
callbackURL: reconstructTenantURLResolver("/api/auth/facebook/callback"),
|
||||
redirectURL: reconstructTenantURLResolver("/api/auth/facebook"),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,6 @@ import { reconstructTenantURLResolver } from "./util";
|
||||
export const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
|
||||
GQLGoogleAuthIntegration
|
||||
> = {
|
||||
callbackURL: reconstructTenantURLResolver("/api/tenant/auth/google/callback"),
|
||||
redirectURL: reconstructTenantURLResolver("/api/tenant/auth/google"),
|
||||
callbackURL: reconstructTenantURLResolver("/api/auth/google/callback"),
|
||||
redirectURL: reconstructTenantURLResolver("/api/auth/google"),
|
||||
};
|
||||
|
||||
@@ -2,26 +2,24 @@ import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__gener
|
||||
|
||||
export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
editComment: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.edit(input),
|
||||
comment: await ctx.mutators.Comments.edit(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createComment: async (source, { input }, ctx) => ({
|
||||
edge: {
|
||||
// Depending on the sort we can't determine the accurate cursor in a
|
||||
// performant way, so we return null instead. It seems that Relay does
|
||||
// not directly use this value.
|
||||
cursor: null,
|
||||
node: await ctx.mutators.Comment.create(input),
|
||||
// performant way, so we return an empty string.
|
||||
cursor: "",
|
||||
node: await ctx.mutators.Comments.create(input),
|
||||
},
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentReply: async (source, { input }, ctx) => ({
|
||||
edge: {
|
||||
// Depending on the sort we can't determine the accurate cursor in a
|
||||
// performant way, so we return null instead. It seems that Relay does
|
||||
// not directly use this value.
|
||||
cursor: null,
|
||||
node: await ctx.mutators.Comment.create(input),
|
||||
// performant way, so we return an empty string.
|
||||
cursor: "",
|
||||
node: await ctx.mutators.Comments.create(input),
|
||||
},
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
@@ -30,23 +28,23 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentReaction: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createReaction(input),
|
||||
comment: await ctx.mutators.Comments.createReaction(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
removeCommentReaction: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.removeReaction(input),
|
||||
comment: await ctx.mutators.Comments.removeReaction(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentDontAgree: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createDontAgree(input),
|
||||
comment: await ctx.mutators.Comments.createDontAgree(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
removeCommentDontAgree: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.removeDontAgree(input),
|
||||
comment: await ctx.mutators.Comments.removeDontAgree(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createCommentFlag: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.createFlag(input),
|
||||
comment: await ctx.mutators.Comments.createFlag(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
regenerateSSOKey: async (source, { input }, ctx) => ({
|
||||
@@ -54,23 +52,23 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Story.create(input),
|
||||
story: await ctx.mutators.Stories.create(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Story.update(input),
|
||||
story: await ctx.mutators.Stories.update(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
mergeStories: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Story.merge(input),
|
||||
story: await ctx.mutators.Stories.merge(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
removeStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Story.remove(input),
|
||||
story: await ctx.mutators.Stories.remove(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
scrapeStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Story.scrape(input),
|
||||
story: await ctx.mutators.Stories.scrape(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
acceptComment: async (source, { input }, ctx) => ({
|
||||
@@ -82,47 +80,43 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
setUsername: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.setUsername(input),
|
||||
user: await ctx.mutators.Users.setUsername(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
setEmail: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.setEmail(input),
|
||||
user: await ctx.mutators.Users.setEmail(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
setPassword: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.setPassword(input),
|
||||
user: await ctx.mutators.Users.setPassword(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updatePassword: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updatePassword(input),
|
||||
user: await ctx.mutators.Users.updatePassword(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createToken: async (source, { input }, ctx) => ({
|
||||
...(await ctx.mutators.User.createToken(input)),
|
||||
...(await ctx.mutators.Users.createToken(input)),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
deactivateToken: async (source, { input }, ctx) => ({
|
||||
...(await ctx.mutators.User.deactivateToken(input)),
|
||||
...(await ctx.mutators.Users.deactivateToken(input)),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserUsername: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updateUserUsername(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserDisplayName: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updateUserDisplayName(input),
|
||||
user: await ctx.mutators.Users.updateUserUsername(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserEmail: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updateUserEmail(input),
|
||||
user: await ctx.mutators.Users.updateUserEmail(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserAvatar: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updateUserAvatar(input),
|
||||
user: await ctx.mutators.Users.updateUserAvatar(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserRole: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.User.updateUserRole(input),
|
||||
user: await ctx.mutators.Users.updateUserRole(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,6 @@ import { reconstructTenantURLResolver } from "./util";
|
||||
export const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
GQLOIDCAuthIntegration
|
||||
> = {
|
||||
callbackURL: reconstructTenantURLResolver("/api/tenant/auth/oidc/callback"),
|
||||
redirectURL: reconstructTenantURLResolver("/api/tenant/auth/oidc"),
|
||||
callbackURL: reconstructTenantURLResolver("/api/auth/oidc/callback"),
|
||||
redirectURL: reconstructTenantURLResolver("/api/auth/oidc"),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GQLProfileTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import * as user from "talk-server/models/user";
|
||||
|
||||
const resolveType: GQLProfileTypeResolver<user.Profile> = profile => {
|
||||
|
||||
@@ -2,8 +2,11 @@ import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generate
|
||||
|
||||
import { sharedModerationInputResolver } from "./ModerationQueues";
|
||||
|
||||
export const Query: GQLQueryTypeResolver<void> = {
|
||||
export const Query: Required<GQLQueryTypeResolver<void>> = {
|
||||
story: (source, args, ctx) => ctx.loaders.Stories.findOrCreate.load(args),
|
||||
stories: (source, args, ctx) => ctx.loaders.Stories.connection(args),
|
||||
user: (source, args, ctx) => ctx.loaders.Users.user.load(args.id),
|
||||
users: (source, args, ctx) => ctx.loaders.Users.connection(args),
|
||||
comment: (source, { id }, ctx) =>
|
||||
id ? ctx.loaders.Comments.comment.load(id) : null,
|
||||
comments: (source, args, ctx) => ctx.loaders.Comments.forFilter(args),
|
||||
|
||||
@@ -4,5 +4,5 @@ import * as user from "talk-server/models/user";
|
||||
export const User: GQLUserTypeResolver<user.User> = {
|
||||
comments: ({ id }, input, ctx) => ctx.loaders.Comments.forUser(id, input),
|
||||
commentModerationActionHistory: ({ id }, input, ctx) =>
|
||||
ctx.loaders.Actions.commentModerationActionsConnection(input, id),
|
||||
ctx.loaders.CommentModerationActions.forModerator(input, id),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Cursor from "talk-server/graph/common/scalars/cursor";
|
||||
import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { AcceptCommentPayload } from "./AcceptCommentPayload";
|
||||
@@ -29,17 +28,17 @@ const Resolvers: GQLResolver = {
|
||||
CommentModerationAction,
|
||||
CommentRevision,
|
||||
Cursor,
|
||||
Mutation,
|
||||
ModerationQueue,
|
||||
ModerationQueues,
|
||||
OIDCAuthIntegration,
|
||||
FacebookAuthIntegration,
|
||||
GoogleAuthIntegration,
|
||||
ModerationQueue,
|
||||
ModerationQueues,
|
||||
Mutation,
|
||||
OIDCAuthIntegration,
|
||||
Profile,
|
||||
Query,
|
||||
RejectCommentPayload,
|
||||
Time,
|
||||
Story,
|
||||
Time,
|
||||
User,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GraphQLResolveInfo } from "graphql";
|
||||
import graphqlFields from "graphql-fields";
|
||||
import { pull } from "lodash";
|
||||
import { parseQuery, stringifyQuery } from "talk-common/utils";
|
||||
import { URL } from "url";
|
||||
|
||||
import { parseQuery, stringifyQuery } from "talk-common/utils";
|
||||
import { constructTenantURL, reconstructURL } from "talk-server/app/url";
|
||||
|
||||
import TenantContext from "../context";
|
||||
|
||||
@@ -602,22 +602,30 @@ type FacebookAuthIntegration {
|
||||
##########################
|
||||
|
||||
type AuthIntegrations {
|
||||
"""
|
||||
local stores configuration related to email/password based logins.
|
||||
"""
|
||||
local: LocalAuthIntegration!
|
||||
sso: SSOAuthIntegration!
|
||||
oidc: OIDCAuthIntegration!
|
||||
google: GoogleAuthIntegration!
|
||||
facebook: FacebookAuthIntegration!
|
||||
}
|
||||
|
||||
"""
|
||||
AuthDisplayNameConfiguration allows configuration related to Display Names.
|
||||
"""
|
||||
type AuthDisplayNameConfiguration {
|
||||
"""
|
||||
enabled when true will allow the display name to be used by other
|
||||
AuthIntegrations.
|
||||
sso stores configuration related to Single Sign On based logins.
|
||||
"""
|
||||
enabled: Boolean!
|
||||
sso: SSOAuthIntegration!
|
||||
|
||||
"""
|
||||
oidc stores configuration related to OpenID Connect based logins.
|
||||
"""
|
||||
oidc: OIDCAuthIntegration!
|
||||
|
||||
"""
|
||||
google stores configuration related to Google based logins.
|
||||
"""
|
||||
google: GoogleAuthIntegration!
|
||||
|
||||
"""
|
||||
facebook stores configuration related to Facebook based logins.
|
||||
"""
|
||||
facebook: FacebookAuthIntegration!
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -630,12 +638,6 @@ type Auth {
|
||||
authentication solutions.
|
||||
"""
|
||||
integrations: AuthIntegrations!
|
||||
|
||||
"""
|
||||
displayName contains configuration related to the use of Display Names across
|
||||
AuthIntegrations.
|
||||
"""
|
||||
displayName: AuthDisplayNameConfiguration! @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -1068,11 +1070,6 @@ type User {
|
||||
"""
|
||||
username: String
|
||||
|
||||
"""
|
||||
displayName is provided optionally when enabled and available.
|
||||
"""
|
||||
displayName: String
|
||||
|
||||
"""
|
||||
email is the current email address for the User.
|
||||
"""
|
||||
@@ -1114,12 +1111,52 @@ type User {
|
||||
commentModerationActionHistory(
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
): CommentModerationActionConnection! @auth(role: [MODERATOR, ADMIN])
|
||||
): CommentModerationActionConnection! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
tokens lists the access tokens associated with the account.
|
||||
"""
|
||||
tokens: [Token!]! @auth(role: [ADMIN], userIDField: "id")
|
||||
tokens: [Token!]! @auth(roles: [ADMIN], userIDField: "id")
|
||||
|
||||
"""
|
||||
createdAt is the time that the User was created at.
|
||||
"""
|
||||
createdAt: Time! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
}
|
||||
|
||||
"""
|
||||
UserEdge represents a unique User in a UsersConnection.
|
||||
"""
|
||||
type UserEdge {
|
||||
"""
|
||||
node is the User for this edge.
|
||||
"""
|
||||
node: User!
|
||||
|
||||
"""
|
||||
cursor is used in pagination.
|
||||
"""
|
||||
cursor: Cursor!
|
||||
}
|
||||
|
||||
"""
|
||||
UsersConnection represents a subset of a stories list.
|
||||
"""
|
||||
type UsersConnection {
|
||||
"""
|
||||
edges are a subset of UserEdge's.
|
||||
"""
|
||||
edges: [UserEdge!]!
|
||||
|
||||
"""
|
||||
nodes is a list of User's.
|
||||
"""
|
||||
nodes: [User!]!
|
||||
|
||||
"""
|
||||
pageInfo is information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -1201,9 +1238,9 @@ type CommentModerationActionEdge {
|
||||
node: CommentModerationAction!
|
||||
|
||||
"""
|
||||
|
||||
cursor is used in pagination.
|
||||
"""
|
||||
cursor: Cursor
|
||||
cursor: Cursor!
|
||||
}
|
||||
|
||||
type CommentModerationActionConnection {
|
||||
@@ -1213,7 +1250,12 @@ type CommentModerationActionConnection {
|
||||
edges: [CommentModerationActionEdge!]!
|
||||
|
||||
"""
|
||||
pageInfo is
|
||||
nodes is a list of CommentModerationAction's.
|
||||
"""
|
||||
nodes: [CommentModerationAction!]!
|
||||
|
||||
"""
|
||||
pageInfo is information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
@@ -1295,7 +1337,10 @@ type Comment {
|
||||
the history of moderator actions performed on the Comment, with the most
|
||||
recent last.
|
||||
"""
|
||||
statusHistory: [CommentModerationAction!]! @auth(role: [MODERATOR, ADMIN])
|
||||
statusHistory(
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
): CommentModerationActionConnection! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
parentCount is the number of direct parents for this Comment. Currently this
|
||||
@@ -1399,9 +1444,9 @@ type CommentEdge {
|
||||
node: Comment!
|
||||
|
||||
"""
|
||||
|
||||
cursor is used in pagination.
|
||||
"""
|
||||
cursor: Cursor
|
||||
cursor: Cursor!
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1414,7 +1459,12 @@ type CommentsConnection {
|
||||
edges: [CommentEdge!]!
|
||||
|
||||
"""
|
||||
pageInfo is
|
||||
nodes is a list of Comment's.
|
||||
"""
|
||||
nodes: [Comment!]!
|
||||
|
||||
"""
|
||||
pageInfo is information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
@@ -1517,6 +1567,21 @@ type StoryMetadata {
|
||||
section: String
|
||||
}
|
||||
|
||||
"""
|
||||
STORY_STATUS represents filtering states that a Story can be in.
|
||||
"""
|
||||
enum STORY_STATUS {
|
||||
"""
|
||||
OPEN represents when a given Story is open for commenting.
|
||||
"""
|
||||
OPEN
|
||||
|
||||
"""
|
||||
CLOSED represents when a given Story is not open for commenting.
|
||||
"""
|
||||
CLOSED
|
||||
}
|
||||
|
||||
"""
|
||||
Story is an Article or Page where Comments are written on by Users.
|
||||
"""
|
||||
@@ -1537,7 +1602,8 @@ type Story {
|
||||
metadata: StoryMetadata
|
||||
|
||||
"""
|
||||
scrapedAt is the Time that the Story had it's metadata scraped at.
|
||||
scrapedAt is the Time that the Story had it's metadata scraped at. If the time
|
||||
is null, the Story has not been scraped yet.
|
||||
"""
|
||||
scrapedAt: Time
|
||||
|
||||
@@ -1563,7 +1629,8 @@ type Story {
|
||||
moderationQueues: ModerationQueues! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
closedAt is the Time that the Story is closed for commenting.
|
||||
closedAt is the Time that the Story is closed for commenting. If null or in
|
||||
the future, the story is not yet closed.
|
||||
"""
|
||||
closedAt: Time
|
||||
|
||||
@@ -1588,21 +1655,39 @@ type Story {
|
||||
moderation: MODERATION_MODE!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## CommentsFilterInput
|
||||
################################################################################
|
||||
|
||||
input CommentsFilterInput {
|
||||
"""
|
||||
StoryEdge represents a unique Story in a StoriesConnection.
|
||||
"""
|
||||
type StoryEdge {
|
||||
"""
|
||||
storyID when specified, will filter to show only Comment's on that Story.
|
||||
node is the Story for this edge.
|
||||
"""
|
||||
storyID: ID
|
||||
node: Story!
|
||||
|
||||
"""
|
||||
status when specified, will filter to show only Comment's with that
|
||||
COMMENT_STATUS.
|
||||
cursor is used in pagination.
|
||||
"""
|
||||
status: COMMENT_STATUS
|
||||
cursor: Cursor!
|
||||
}
|
||||
|
||||
"""
|
||||
StoriesConnection represents a subset of a stories list.
|
||||
"""
|
||||
type StoriesConnection {
|
||||
"""
|
||||
edges are a subset of StoryEdge's.
|
||||
"""
|
||||
edges: [StoryEdge!]!
|
||||
|
||||
"""
|
||||
nodes is a list of Story's.
|
||||
"""
|
||||
nodes: [Story!]!
|
||||
|
||||
"""
|
||||
pageInfo is information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -1616,22 +1701,46 @@ type Query {
|
||||
comment(id: ID!): Comment
|
||||
|
||||
"""
|
||||
comments returns a filtered comments connection that can be paginated.
|
||||
comments returns a filtered comments connection that can be paginated. This is
|
||||
a fairly expensive edge to filter against, moderation queues should utlilize
|
||||
the dedicated edges for more optimized responses.
|
||||
"""
|
||||
comments(
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
filter: CommentsFilterInput!
|
||||
storyID: ID
|
||||
status: COMMENT_STATUS
|
||||
): CommentsConnection @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
story is the Story specified by its ID/URL.
|
||||
story is a specific article that can be identified by either an ID or a URL.
|
||||
"""
|
||||
story(id: ID, url: String): Story
|
||||
|
||||
"""
|
||||
stories returns filtered stories that can be paginated.
|
||||
"""
|
||||
stories(
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
status: STORY_STATUS
|
||||
): StoriesConnection @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
user will return the user referenced by their ID.
|
||||
"""
|
||||
user(id: ID!): User @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
users returns filtered users that can be paginated.
|
||||
"""
|
||||
users(first: Int = 10, after: Cursor, role: USER_ROLE): UsersConnection!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
me is the current logged in User. If no user is currently logged in, it will
|
||||
return null.
|
||||
return null. This is the only nullable field that can be returned that depends
|
||||
on the authentication state that will not throw an error.
|
||||
"""
|
||||
me: User
|
||||
|
||||
@@ -1982,19 +2091,30 @@ input SettingsFacebookAuthIntegrationInput {
|
||||
}
|
||||
|
||||
input SettingsAuthIntegrationsInput {
|
||||
"""
|
||||
local stores configuration related to email/password based logins.
|
||||
"""
|
||||
local: SettingsLocalAuthIntegrationInput
|
||||
sso: SettingsSSOAuthIntegrationInput
|
||||
oidc: SettingsOIDCAuthIntegrationInput
|
||||
google: SettingsGoogleAuthIntegrationInput
|
||||
facebook: SettingsFacebookAuthIntegrationInput
|
||||
}
|
||||
|
||||
input SettingsAuthDisplayNameInput {
|
||||
"""
|
||||
enabled when true will allow the display name to be used by other
|
||||
AuthIntegrations.
|
||||
sso stores configuration related to Single Sign On based logins.
|
||||
"""
|
||||
enabled: Boolean!
|
||||
sso: SettingsSSOAuthIntegrationInput
|
||||
|
||||
"""
|
||||
oidc stores configuration related to OpenID Connect based logins.
|
||||
"""
|
||||
oidc: SettingsOIDCAuthIntegrationInput
|
||||
|
||||
"""
|
||||
google stores configuration related to Google based logins.
|
||||
"""
|
||||
google: SettingsGoogleAuthIntegrationInput
|
||||
|
||||
"""
|
||||
facebook stores configuration related to Facebook based logins.
|
||||
"""
|
||||
facebook: SettingsFacebookAuthIntegrationInput
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -2002,12 +2122,6 @@ Auth contains all the settings related to authentication and
|
||||
authorization.
|
||||
"""
|
||||
input SettingsAuthInput {
|
||||
"""
|
||||
"
|
||||
displayName allows configuration related to Display Names.
|
||||
"""
|
||||
displayName: SettingsAuthDisplayNameInput
|
||||
|
||||
"""
|
||||
integrations are the set of configurations for the variations of
|
||||
authentication solutions.
|
||||
@@ -3047,40 +3161,6 @@ type UpdateUserUsernamePayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateUserDisplayName
|
||||
##################
|
||||
|
||||
input UpdateUserDisplayNameInput {
|
||||
"""
|
||||
userID is the ID of the User that should have their display name updated.
|
||||
"""
|
||||
userID: ID!
|
||||
|
||||
"""
|
||||
displayName is the desired display name to set for the User. If set to `null`
|
||||
or not provided, it will be unset.
|
||||
"""
|
||||
displayName: String
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type UpdateUserDisplayNamePayload {
|
||||
"""
|
||||
user is the possibly modified User.
|
||||
"""
|
||||
user: User!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateUserEmail
|
||||
##################
|
||||
@@ -3195,7 +3275,9 @@ type Mutation {
|
||||
createCommentReply will create a Comment as the current logged in User that is
|
||||
in reply to another Comment.
|
||||
"""
|
||||
createCommentReply(input: CreateCommentReplyInput!): CreateCommentReplyPayload
|
||||
createCommentReply(
|
||||
input: CreateCommentReplyInput!
|
||||
): CreateCommentReplyPayload @auth
|
||||
|
||||
"""
|
||||
editComment will allow the author of a comment to change the body within the
|
||||
@@ -3346,14 +3428,6 @@ type Mutation {
|
||||
input: UpdateUserUsernameInput!
|
||||
): UpdateUserUsernamePayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateUserDisplayName allows administrators to update a given User's display
|
||||
name to the one provided.
|
||||
"""
|
||||
updateUserDisplayName(
|
||||
input: UpdateUserDisplayNameInput!
|
||||
): UpdateUserDisplayNamePayload @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateUserEmail allows administrators to update a given User's email address
|
||||
to the one provided.
|
||||
|
||||
Reference in New Issue
Block a user