[CORL-687] Webhooks (#2738)

* feat: initial webhook impl

* feat: added support for key rotation

* feat: harmonized fetcher

* feat: added expired secrets cleaning

* feat: event system refactor

* feat: added story event

* feat: simplfiied webhook handler

* feat: added ref's to locations where user events can be added

* feat: added UI to support webhooks

* fix: renaming some Webhook -> WebhookEndpoint

* fix: review comments to adjuist flow

* feat: added localizations

* fix: linting, updated snapshots

* fix: adapted for new fluent

* fix: rearranged folders

* fix: linting

* feat: added webhooks documentation

* feat: improved toc generation

* feat: added some tests to webhooks

* fix: chain transition hooks

* feat: added tests around webhook ui

* fix: renamed events

* fix: adjusted circle markdown linting

* fix: adjusted doctoc script call

* review: review fixes

* review: review comments

* review: adjusted signing secret confirmation

* review: adjusted styles to harmonize button usage

* fix: updated snapshots and tests

* review: move form out of webhooks

Moved the form out of the webhooks by relocating the layout used for the
route associated with the configure routes.

* fix: fixed bugs and snapshots with tests

* feat: revised slack message format to use block api

* fix: fixed a small text bug

Co-authored-by: Vinh <vinh@vinh.tech>
Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
Wyatt Johnson
2020-02-18 13:25:48 -05:00
committed by GitHub
co-authored by Vinh Kim Gardner
parent 34ba2da88d
commit e42c2b925d
137 changed files with 5633 additions and 1020 deletions
+6 -20
View File
@@ -4,21 +4,18 @@ import uuid from "uuid";
import { LanguageCode } from "coral-common/helpers/i18n/locales";
import { Config } from "coral-server/config";
import {
createPublisher,
Publisher,
} from "coral-server/graph/subscriptions/publisher";
import CoralEventListenerBroker, {
CoralEventPublisherBroker,
} from "coral-server/events/publisher";
import logger, { Logger } from "coral-server/logger";
import { PersistedQuery } from "coral-server/models/queries";
import { Tenant } from "coral-server/models/tenant";
import { User } from "coral-server/models/user";
import { MailerQueue } from "coral-server/queue/tasks/mailer";
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
import { ScraperQueue } from "coral-server/queue/tasks/scraper";
import { I18n } from "coral-server/services/i18n";
import { JWTSigningConfig } from "coral-server/services/jwt";
import { AugmentedRedis } from "coral-server/services/redis";
import createSlackPublisher from "coral-server/services/slack/publisher";
import TenantCache from "coral-server/services/tenant/cache";
import { Request } from "coral-server/types/express";
@@ -41,16 +38,17 @@ export interface GraphContextOptions {
i18n: I18n;
mailerQueue: MailerQueue;
mongo: Db;
notifierQueue: NotifierQueue;
pubsub: RedisPubSub;
redis: AugmentedRedis;
scraperQueue: ScraperQueue;
tenant: Tenant;
tenantCache: TenantCache;
broker: CoralEventListenerBroker;
}
export default class GraphContext {
public readonly config: Config;
public readonly broker: CoralEventPublisherBroker;
public readonly disableCaching: boolean;
public readonly i18n: I18n;
public readonly id: string;
@@ -61,7 +59,6 @@ export default class GraphContext {
public readonly mongo: Db;
public readonly mutators: ReturnType<typeof mutators>;
public readonly now: Date;
public readonly publisher: Publisher;
public readonly pubsub: RedisPubSub;
public readonly redis: AugmentedRedis;
public readonly scraperQueue: ScraperQueue;
@@ -100,18 +97,7 @@ export default class GraphContext {
this.signingConfig = options.signingConfig;
this.clientID = options.clientID;
this.publisher = createPublisher({
pubsub: this.pubsub,
slackPublisher: createSlackPublisher(
this.mongo,
this.config,
this.tenant
),
notifierQueue: options.notifierQueue,
tenantID: this.tenant.id,
clientID: this.clientID,
});
this.broker = options.broker.instance(this);
this.loaders = loaders(this);
this.mutators = mutators(this);
}
+8 -1
View File
@@ -81,7 +81,14 @@ const primeStoriesFromConnection = (ctx: GraphContext) => (
export default (ctx: GraphContext) => ({
findOrCreate: new DataLoader(
createManyBatchLoadFn((input: FindOrCreateStory) =>
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.scraperQueue, ctx.now)
findOrCreate(
ctx.mongo,
ctx.tenant,
ctx.broker,
input,
ctx.scraperQueue,
ctx.now
)
),
{
// TODO: (wyattjoh) see if there's something we can do to improve the cache key
+2 -2
View File
@@ -12,7 +12,7 @@ export const Actions = (ctx: GraphContext) => ({
ctx.mongo,
ctx.redis,
ctx.config,
ctx.publisher,
ctx.broker,
ctx.tenant,
input.commentID,
input.commentRevisionID,
@@ -24,7 +24,7 @@ export const Actions = (ctx: GraphContext) => ({
ctx.mongo,
ctx.redis,
ctx.config,
ctx.publisher,
ctx.broker,
ctx.tenant,
input.commentID,
input.commentRevisionID,
+11 -16
View File
@@ -45,7 +45,7 @@ export const Comments = (ctx: GraphContext) => ({
ctx.mongo,
ctx.redis,
ctx.config,
ctx.publisher,
ctx.broker,
ctx.tenant,
ctx.user!,
{ authorID: ctx.user!.id, ...comment },
@@ -68,7 +68,7 @@ export const Comments = (ctx: GraphContext) => ({
ctx.mongo,
ctx.redis,
ctx.config,
ctx.publisher,
ctx.broker,
ctx.tenant,
ctx.user!,
{
@@ -92,7 +92,7 @@ export const Comments = (ctx: GraphContext) => ({
createReaction(
ctx.mongo,
ctx.redis,
ctx.publisher,
ctx.broker,
ctx.tenant,
ctx.user!,
{
@@ -102,7 +102,7 @@ export const Comments = (ctx: GraphContext) => ({
ctx.now
),
removeReaction: ({ commentID }: GQLRemoveCommentReactionInput) =>
removeReaction(ctx.mongo, ctx.redis, ctx.publisher, ctx.tenant, ctx.user!, {
removeReaction(ctx.mongo, ctx.redis, ctx.broker, ctx.tenant, ctx.user!, {
commentID,
}),
createDontAgree: ({
@@ -113,7 +113,7 @@ export const Comments = (ctx: GraphContext) => ({
createDontAgree(
ctx.mongo,
ctx.redis,
ctx.publisher,
ctx.broker,
ctx.tenant,
ctx.user!,
{
@@ -128,14 +128,9 @@ export const Comments = (ctx: GraphContext) => ({
ctx.now
),
removeDontAgree: ({ commentID }: GQLRemoveCommentDontAgreeInput) =>
removeDontAgree(
ctx.mongo,
ctx.redis,
ctx.publisher,
ctx.tenant,
ctx.user!,
{ commentID }
),
removeDontAgree(ctx.mongo, ctx.redis, ctx.broker, ctx.tenant, ctx.user!, {
commentID,
}),
createFlag: ({
commentID,
commentRevisionID,
@@ -145,7 +140,7 @@ export const Comments = (ctx: GraphContext) => ({
createFlag(
ctx.mongo,
ctx.redis,
ctx.publisher,
ctx.broker,
ctx.tenant,
ctx.user!,
{
@@ -179,7 +174,7 @@ export const Comments = (ctx: GraphContext) => ({
ctx.mongo,
ctx.redis,
ctx.config,
ctx.publisher,
ctx.broker,
ctx.tenant,
commentID,
commentRevisionID,
@@ -190,7 +185,7 @@ export const Comments = (ctx: GraphContext) => ({
)
.then(comment => {
// Publish that the comment was featured.
publishCommentFeatured(ctx.publisher, comment);
publishCommentFeatured(ctx.broker, comment);
// Return it to the next step.
return comment;
+55 -1
View File
@@ -2,19 +2,33 @@ import GraphContext from "coral-server/graph/context";
import { Tenant } from "coral-server/models/tenant";
import {
createAnnouncement,
createWebhookEndpoint,
deleteAnnouncement,
deleteWebhookEndpoint,
disableFeatureFlag,
disableWebhookEndpoint,
enableFeatureFlag,
enableWebhookEndpoint,
regenerateSSOKey,
rotateWebhookEndpointSecret,
update,
updateWebhookEndpoint,
} from "coral-server/services/tenant";
import {
GQLCreateAnnouncementInput,
GQLCreateWebhookEndpointInput,
GQLDeleteWebhookEndpointInput,
GQLDisableWebhookEndpointInput,
GQLEnableWebhookEndpointInput,
GQLFEATURE_FLAG,
GQLRotateWebhookEndpointSecretInput,
GQLUpdateSettingsInput,
GQLUpdateWebhookEndpointInput,
} from "coral-server/graph/schema/__generated__/types";
import { WithoutMutationID } from "./util";
export const Settings = ({
mongo,
redis,
@@ -23,7 +37,9 @@ export const Settings = ({
config,
now,
}: GraphContext) => ({
update: (input: GQLUpdateSettingsInput): Promise<Tenant | null> =>
update: (
input: WithoutMutationID<GQLUpdateSettingsInput>
): Promise<Tenant | null> =>
update(mongo, redis, tenantCache, config, tenant, input.settings),
regenerateSSOKey: (): Promise<Tenant | null> =>
regenerateSSOKey(mongo, redis, tenantCache, tenant, now),
@@ -35,4 +51,42 @@ export const Settings = ({
createAnnouncement(mongo, redis, tenantCache, tenant, input, now),
deleteAnnouncement: () =>
deleteAnnouncement(mongo, redis, tenantCache, tenant),
createWebhookEndpoint: (
input: WithoutMutationID<GQLCreateWebhookEndpointInput>
) =>
createWebhookEndpoint(
mongo,
redis,
config,
tenantCache,
tenant,
input,
now
),
enableWebhookEndpoint: (
input: WithoutMutationID<GQLEnableWebhookEndpointInput>
) => enableWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
disableWebhookEndpoint: (
input: WithoutMutationID<GQLDisableWebhookEndpointInput>
) => disableWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
updateWebhookEndpoint: ({
id,
...input
}: WithoutMutationID<GQLUpdateWebhookEndpointInput>) =>
updateWebhookEndpoint(mongo, redis, config, tenantCache, tenant, id, input),
deleteWebhookEndpoint: (
input: WithoutMutationID<GQLDeleteWebhookEndpointInput>
) => deleteWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
rotateWebhookEndpointSecret: (
input: WithoutMutationID<GQLRotateWebhookEndpointSecretInput>
) =>
rotateWebhookEndpointSecret(
mongo,
redis,
tenantCache,
tenant,
input.id,
input.inactiveIn,
now
),
});
@@ -32,6 +32,7 @@ export const Stories = (ctx: GraphContext) => ({
create(
ctx.mongo,
ctx.tenant,
ctx.broker,
ctx.config,
input.story.id,
input.story.url,
@@ -86,7 +86,7 @@ export const storyModerationInputResolver = (
*
* @param source the source of the type, not used
* @param args the args of the type, not used
* @param ctx the TenantContext that will be used to get the shared counts
* @param ctx the GraphContext that will be used to get the shared counts
*/
export const sharedModerationInputResolver = async (
source: any,
@@ -106,7 +106,7 @@ export const sharedModerationInputResolver = async (
*
* @param source the source of the payload, not used
* @param args the args of the payload containing potentially a Story ID
* @param ctx the TenantContext for which we can use to retrieve the shared data
* @param ctx the GraphContext for which we can use to retrieve the shared data
*/
export const moderationQueuesResolver: QueryToModerationQueuesResolver = async (
source,
+54 -2
View File
@@ -33,9 +33,13 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
user: await ctx.mutators.Users.updateNotificationSettings(input),
clientMutationId,
}),
updateSettings: async (source, { input }, ctx) => ({
updateSettings: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
settings: await ctx.mutators.Settings.update(input),
clientMutationId: input.clientMutationId,
clientMutationId,
}),
createCommentReaction: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Comments.createReaction(input),
@@ -252,4 +256,52 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
site: await ctx.mutators.Sites.update(input),
clientMutationId: input.clientMutationId,
}),
createWebhookEndpoint: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
...(await ctx.mutators.Settings.createWebhookEndpoint(input)),
clientMutationId,
}),
updateWebhookEndpoint: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.updateWebhookEndpoint(input),
clientMutationId,
}),
disableWebhookEndpoint: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.disableWebhookEndpoint(input),
clientMutationId,
}),
enableWebhookEndpoint: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.enableWebhookEndpoint(input),
clientMutationId,
}),
deleteWebhookEndpoint: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.deleteWebhookEndpoint(input),
clientMutationId,
}),
rotateWebhookEndpointSecret: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSecret(input),
clientMutationId,
}),
};
+3
View File
@@ -1,3 +1,5 @@
import { getWebhookEndpoint } from "coral-server/models/tenant";
import { GQLQueryTypeResolver } from "coral-server/graph/schema/__generated__/types";
import { moderationQueuesResolver } from "./ModerationQueues";
@@ -25,4 +27,5 @@ export const Query: Required<GQLQueryTypeResolver<void>> = {
ctx.loaders.Stories.activeStories(limit),
sites: (source, args, ctx) => ctx.loaders.Sites.connection(args),
site: (source, { id }, ctx) => (id ? ctx.loaders.Sites.site.load(id) : null),
webhookEndpoint: (source, { id }, ctx) => getWebhookEndpoint(ctx.tenant, id),
};
@@ -2,7 +2,7 @@ import * as settings from "coral-server/models/settings";
import { GQLSSOAuthIntegrationTypeResolver } from "coral-server/graph/schema/__generated__/types";
function getActiveSSOKey(keys: settings.SSOKey[]) {
function getActiveSSOKey(keys: settings.Secret[]) {
// Any key that has been rotated cannot be the active key.
return keys.find(key => !key.rotatedAt);
}
@@ -6,6 +6,7 @@ import {
import {
GQLFEATURE_FLAG,
GQLSettingsTypeResolver,
GQLWEBHOOK_EVENT_NAME,
} from "coral-server/graph/schema/__generated__/types";
const filterValidFeatureFlags = () => {
@@ -27,4 +28,5 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
const sites = await ctx.loaders.Sites.connection({});
return sites.edges.length > 1;
},
webhookEvents: () => Object.values(GQLWEBHOOK_EVENT_NAME),
};
@@ -17,3 +17,13 @@ export const Subscription: GQLSubscriptionTypeResolver = {
commentFeatured,
commentReleased,
};
export { CommentFeaturedInput } from "./commentFeatured";
export { CommentCreatedInput } from "./commentCreated";
export {
CommentEnteredModerationQueueInput,
} from "./commentEnteredModerationQueue";
export { CommentLeftModerationQueueInput } from "./commentLeftModerationQueue";
export { CommentReleasedInput } from "./commentReleased";
export { CommentReplyCreatedInput } from "./commentReplyCreated";
export { CommentStatusUpdatedInput } from "./commentStatusUpdated";
@@ -0,0 +1,10 @@
import * as tenant from "coral-server/models/tenant";
import { GQLWebhookEndpointTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const WebhookEndpoint: GQLWebhookEndpointTypeResolver<
tenant.Endpoint
> = {
signingSecret: ({ signingSecrets }) =>
signingSecrets[signingSecrets.length - 1],
};
+2
View File
@@ -50,6 +50,7 @@ import { User } from "./User";
import { UsernameHistory } from "./UsernameHistory";
import { UsernameStatus } from "./UsernameStatus";
import { UserStatus } from "./UserStatus";
import { WebhookEndpoint } from "./WebhookEndpoint";
const Resolvers: GQLResolver = {
ApproveCommentPayload,
@@ -101,6 +102,7 @@ const Resolvers: GQLResolver = {
UserStatus,
Settings,
SlackConfiguration,
WebhookEndpoint,
};
export default Resolvers;
+338 -1
View File
@@ -1002,7 +1002,7 @@ type SlackChannel {
triggers are the filters of types of comments that will be sent to
the correlated channel
"""
triggers: SlackChannelTriggers
triggers: SlackChannelTriggers!
}
################################################################################
@@ -1204,6 +1204,79 @@ type StaffConfiguration {
label: String!
}
type WebhookDelivery {
success: Boolean!
status: Int!
statusText: String!
request: String!
response: String!
createdAt: Time!
}
enum WEBHOOK_EVENT_NAME {
STORY_CREATED
}
"""
TODO: merge with SSOKey with PR #2732
"""
type Secret {
"""
secret is the actual underlying secret used to verify the tokens with.
"""
secret: String!
"""
createdAt is the date that the key was created at.
"""
createdAt: Time!
}
type WebhookEndpoint {
"""
id is the unique identifier for this specific endpoint.
"""
id: ID!
"""
enabled when true will enable events to be sent to this endpoint.
"""
enabled: Boolean!
"""
url is the URL that we will POST event data to.
"""
url: String!
"""
signingSecret is the current secret used to sign the events sent out.
"""
signingSecret: Secret!
"""
deliveries store the deliveries for each event sent for the last 50 events.
"""
deliveries: [WebhookDelivery!]!
"""
all is true when all events are subscribed to.
"""
all: Boolean!
"""
events are the specific event names that this endpoint is configured to send
for.
"""
events: [WEBHOOK_EVENT_NAME!]!
}
type WebhookConfiguration {
"""
endpoints is all the configured endpoints that should receive events.
"""
endpoints: [WebhookEndpoint!]!
}
"""
NewCommenterConfiguration specifies the features that apply to new commenters
"""
@@ -1266,6 +1339,16 @@ type Settings {
"""
domain: String! @auth(roles: [ADMIN])
"""
webhooks store the webhook configuration.
"""
webhooks: WebhookConfiguration! @auth(roles: [ADMIN])
"""
webhookEvents returns all the events that can trigger webhooks.
"""
webhookEvents: [WEBHOOK_EVENT_NAME!]! @auth(roles: [ADMIN])
"""
staticURI if configured, is the static URI used to serve static files from.
"""
@@ -2891,6 +2974,11 @@ type Query {
activeStories(limit: Int = 10 @constraint(max: 25)): [Story!]!
@auth(roles: [ADMIN])
@rate(max: 2, seconds: 1)
"""
webhookEndpint will return a specific WebhookEndpoint if it exists.
"""
webhookEndpoint(id: ID!): WebhookEndpoint @auth(roles: [ADMIN])
}
################################################################################
@@ -4768,6 +4856,212 @@ type DeleteModeratorNotePayload {
user: User!
}
##################
# createWebhookEndpoint
##################
input CreateWebhookEndpointInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
url is the URL that Coral will POST event data to.
"""
url: String!
"""
all is true when all events are subscribed to.
"""
all: Boolean!
"""
events are the specific event names that this endpoint is configured to send
for.
"""
events: [WEBHOOK_EVENT_NAME!]!
}
type CreateWebhookEndpointPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just created.
"""
endpoint: WebhookEndpoint!
"""
settings is the updated settings also containing the new endpoint.
"""
settings: Settings!
}
##################
# updateWebhookEndpoint
##################
input UpdateWebhookEndpointInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the WebhookEndpoint being updated.
"""
id: ID!
"""
url is the URL that Coral will POST event data to.
"""
url: String
"""
all is true when all events are subscribed to.
"""
all: Boolean
"""
events are the specific event names that this endpoint is configured to send
for.
"""
events: [WEBHOOK_EVENT_NAME!]
}
type UpdateWebhookEndpointPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just created.
"""
endpoint: WebhookEndpoint!
}
##################
# rotateWebhookEndpointSecret
##################
input RotateWebhookEndpointSecretInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the WebhookEndpoint being updated.
"""
id: ID!
"""
inactiveIn is the number of seconds that the current active Secret should be
kept active.
"""
inactiveIn: Int!
}
type RotateWebhookEndpointSecretPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just updated.
"""
endpoint: WebhookEndpoint
}
##################
# disableWebhookEndpoint
##################
input DisableWebhookEndpointInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the WebhookEndpoint being disabled.
"""
id: ID!
}
type DisableWebhookEndpointPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just disabled.
"""
endpoint: WebhookEndpoint
}
##################
# enableWebhookEndpoint
##################
input EnableWebhookEndpointInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the WebhookEndpoint being enabled.
"""
id: ID!
}
type EnableWebhookEndpointPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just enabled.
"""
endpoint: WebhookEndpoint
}
##################
# deleteWebhookEndpoint
##################
input DeleteWebhookEndpointInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the WebhookEndpoint being deleted.
"""
id: ID!
}
type DeleteWebhookEndpointPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
endpoint is the endpoint that we just deleted.
"""
endpoint: WebhookEndpoint
}
##################
# setEmail
##################
@@ -5899,6 +6193,49 @@ type Mutation {
createSite(input: CreateSiteInput!): CreateSitePayload! @auth(roles: [ADMIN])
updateSite(input: UpdateSiteInput!): UpdateSitePayload! @auth(roles: [ADMIN])
"""
createWebhookEndpoint will create a new WebhookEndpoint.
"""
createWebhookEndpoint(
input: CreateWebhookEndpointInput!
): CreateWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
updateWebhookEndpoint will update a WebhookEndpoint.
"""
updateWebhookEndpoint(
input: UpdateWebhookEndpointInput!
): UpdateWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
enableWebhookEndpoint will enable a WebhookEndpoint to recieve new events.
"""
enableWebhookEndpoint(
input: EnableWebhookEndpointInput!
): EnableWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
disableWebhookEndpoint will disable a WebhookEndpoint from recieving new
events.
"""
disableWebhookEndpoint(
input: DisableWebhookEndpointInput!
): DisableWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
deleteWebhookEndpoint will delete a WebhookEndpoint.
"""
deleteWebhookEndpoint(
input: DeleteWebhookEndpointInput!
): DeleteWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
rotateWebhookEndpointSecret will roll the current active secret to a new key.
"""
rotateWebhookEndpointSecret(
input: RotateWebhookEndpointSecretInput!
): RotateWebhookEndpointSecretPayload! @auth(roles: [ADMIN])
}
##################
@@ -1,57 +0,0 @@
import { RedisPubSub } from "graphql-redis-subscriptions";
import { createSubscriptionChannelName } from "coral-server/graph/resolvers/Subscription/helpers";
import { SUBSCRIPTION_INPUT } from "coral-server/graph/resolvers/Subscription/types";
import logger from "coral-server/logger";
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
import { SlackPublisher } from "coral-server/services/slack/publisher";
export type Publisher = (input: SUBSCRIPTION_INPUT) => Promise<void>;
export interface PublisherOptions {
pubsub: RedisPubSub;
slackPublisher: SlackPublisher;
notifierQueue: NotifierQueue;
tenantID: string;
clientID?: string;
}
/**
* createPublisher will create a new Publisher that can be used to send events
* over the pubsub broker to facilitate live updates and notifications.
*
* TODO: Update
*
* @param options options object
* @param options.pubsub the pubsub broker to be used to facilitate the publish action
* @param options.slackPublisher the slack publisher instance
* @param options.notifierQueue the queue
* @param options.tenantID the ID of the Tenant where the event will be published with
* @param options.clientID the ID of the client to de-duplicate mutation responses
*/
export const createPublisher = ({
pubsub,
slackPublisher,
notifierQueue,
tenantID,
clientID,
}: PublisherOptions): Publisher => async input => {
const { channel, payload } = input;
logger.trace({ channel, tenantID, clientID }, "publishing event");
// Start the publishing operation out to all affected subscribers.
await Promise.all([
// Publish to the underlying pubsub system for subscriptions.
pubsub.publish(createSubscriptionChannelName(tenantID, channel), {
...payload,
clientID,
}),
slackPublisher(channel, payload),
// Notify the notifications queue so we can offload notification processing
// to it.
notifierQueue.add({ tenantID, input }),
]);
};