[CORL-878] Upgrade dependencies (#2867)

* chore: upgrade fluent

* chore: upgrade metascraper

* chore: upgrade akismet-api

* chore: upgrade apollo-server-express

* chore: upgrade archiver

* chore: upgrade bull

* chore: upgrade express, cheerio, content-security-policy-builder

* chore: upgrade convict

* chore: upgrade cors, cron

* chore: upgrade csv-stringify

* chore: upgrade dompurify

* chore: upgrade dotenv

* chore: upgrade express-static-gzip

* chore: upgrade fs-extra

* chore: upgrade graphql-js

* chore: upgrade graphql packages

* chore: upgrade html-minifier

* chore: upgrade html-to-text

* chore: upgrade ioredis

* chore: upgrade joi

* chore: upgrade jsdom

* chore: upgrade jsonwebtoken

* chore: upgrade juice

* chore: upgrade jwks-rsa ad linkifyjs

* chore: upgrade lodash

* chore: upgrade luxon

* chore: upgrade metascraper

* chore: upgrade mongodb

* chore: upgrade ms

* chore: upgrade node and node-fetch types

* chore: upgrade nodemailer nunjucks and typescript-eslint

* chore: Upgrade passport

* upgrade: prom-client react-helmet source-map-support stack-utils

* chore: upgrade uuid

* chore: upgrade @babel packages

* chore: upgrade types

* chore: upgrade autoprefixer

* chore: upgrade jest

* chore: upgrade ts-jest

* chore: remove linkify.d.ts

* chore: upgrade bowser

* chore: case-sensitive-paths-webpack-plugin

* chore: upgrade classnames

* chore: upgrade commander

* chore: upgrade comment-json

* chore: upgrade cross-spawn compression-webpack-plugin del

* chore: upgrade build and watch related dependencies

* chore: upgrade css-vars-ponyfill

* chore: upgrade eslint and css-vars-ponyfill

* chore: upgrade enzyme and eventemitter2

* fix: form bug

* chore: upgrade farce

* chore: upgrade final form

* chore: upgrade react-popper

* chore: upgrade flat and fork-ts-checker-webpack-plugin

* chore: upgrade husky and gulp related, intersection observer

* chore: upgrade lint-staged

* chore: upgrade marked, loader-utils, mini-css-extract-plugin

* chore: upgrade postcss-nested, proxy-polyfill, pstree.remy

* chore: upgrade prettier

* chore: fix prettier changes, upgrade react

* chore: mute createFactory deprecated message

* chore: upgrade react-copy-to-clipbard, react-axe, react-dom, react-test-renderer, react-timeago

* chore: upgrade react-transistion-group, react-responsive

* chore: upgrade types

* chore: upgrade react-dev-utils, react-error-overlay regenerator-runtime

* chore: upgrade types, sinon, sockjs-client, strip-ansi

* chore: upgrade types, fonts

* chore: upgrade nunjucks, ts-node, typescript-snapshot-plugin, wait-for-expect

* chore: upgrade eslint packages

* chore: upgrade fluent, types

* chore: upgrade jsdom dep

* chore: upgrade mongo

* chore: upgrade deps

* chore: upgrade typescript, recompose

* chore: upgrade prettier

* chore: remove obsolete prettier config

* chore: upgrade jsdom types

* chore: upgrade typescript-eslint

* chore: upgrad deps

* chore: upgrade deps

* chore: upgrade relay related modules

* chore: upgrade docz WIP

* chore: upgrade docz

* chore: add guard

* chore: remove obsolete line

* chore: comment

* chore: refactors

* fix: hook count change error
This commit is contained in:
Vinh
2020-04-15 18:15:31 +02:00
committed by GitHub
parent e180cf87d7
commit c9a0ab8848
996 changed files with 29214 additions and 18254 deletions
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
@@ -27,11 +27,9 @@ export interface ConfirmRequestBody {
userID?: string;
}
export const ConfirmRequestBodySchema = Joi.object()
.keys({
userID: Joi.string().default(undefined),
})
.optionalKeys(["userID"]);
export const ConfirmRequestBodySchema = Joi.object().keys({
userID: Joi.string().optional(),
});
export const confirmRequestHandler = ({
redis,
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
@@ -20,10 +20,7 @@ export interface ForgotBody {
}
export const ForgotBodySchema = Joi.object().keys({
email: Joi.string()
.trim()
.lowercase()
.email(),
email: Joi.string().trim().lowercase().email(),
});
export type ForgotOptions = Pick<
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
@@ -14,10 +14,7 @@ export interface LinkBody {
}
export const LinkBodySchema = Joi.object().keys({
email: Joi.string()
.trim()
.lowercase()
.email(),
email: Joi.string().trim().lowercase().email(),
password: Joi.string(),
});
@@ -1,5 +1,5 @@
import Joi from "joi";
import uuid from "uuid/v4";
import Joi from "@hapi/joi";
import { v4 as uuid } from "uuid";
import { AppOptions } from "coral-server/app";
import { handleSuccessfulLogin } from "coral-server/app/middleware/passport";
@@ -23,10 +23,7 @@ export interface SignupBody {
export const SignupBodySchema = Joi.object().keys({
username: Joi.string().trim(),
password: Joi.string(),
email: Joi.string()
.trim()
.lowercase()
.email(),
email: Joi.string().trim().lowercase().email(),
});
export type SignupOptions = Pick<
+15 -24
View File
@@ -1,8 +1,8 @@
import Joi from "joi";
import uuid from "uuid/v4";
import Joi from "@hapi/joi";
import { v4 as uuid } from "uuid";
import { LanguageCode, LOCALES } from "coral-common/helpers/i18n/locales";
import { Omit } from "coral-common/types";
import { AppOptions } from "coral-server/app";
import { validate } from "coral-server/app/request/body";
import { RequestLimiter } from "coral-server/app/request/limiter";
@@ -99,23 +99,17 @@ export interface TenantInstallBody {
}
const TenantInstallBodySchema = Joi.object().keys({
tenant: Joi.object()
.keys({
organization: Joi.object().keys({
name: Joi.string().trim(),
url: Joi.string()
.trim()
.uri(),
contactEmail: Joi.string()
.trim()
.lowercase()
.email(),
}),
locale: Joi.string()
.default(null)
.valid(LOCALES),
})
.optionalKeys("locale"),
tenant: Joi.object().keys({
organization: Joi.object().keys({
name: Joi.string().trim(),
url: Joi.string().trim().uri(),
contactEmail: Joi.string().trim().lowercase().email(),
}),
locale: Joi.string()
.default(null)
.valid(...LOCALES)
.optional(),
}),
site: Joi.object().keys({
name: Joi.string().trim(),
allowedOrigins: Joi.array().items(
@@ -127,10 +121,7 @@ const TenantInstallBodySchema = Joi.object().keys({
user: Joi.object().keys({
username: Joi.string().trim(),
password: Joi.string(),
email: Joi.string()
.trim()
.lowercase()
.email(),
email: Joi.string().trim().lowercase().email(),
}),
});
@@ -17,7 +17,7 @@ export type CountOptions = Pick<AppOptions, "mongo" | "tenantCache" | "i18n">;
*/
export const countHandler = ({ mongo, i18n }: CountOptions): RequestHandler => {
const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window);
const DOMPurify = createDOMPurify(window as any);
return async (req, res, next) => {
try {
+1 -1
View File
@@ -123,7 +123,7 @@ export const listenAndServe = (
app: Express,
port: number
): Promise<http.Server> =>
new Promise(resolve => {
new Promise((resolve) => {
// Listen on the designated port.
const httpServer = app.listen(port, () => resolve(httpServer));
});
+2 -2
View File
@@ -139,8 +139,8 @@ export function generateFrameOptions(req: Request, allowedOrigins: string[]) {
// We need to find the domain that is asking so we can respond with the right
// result, sort of like CORS!
const allowFrom = allowedOrigins
.map(domain => getOrigin(domain))
.find(origin => origin === parentsOrigin);
.map((domain) => getOrigin(domain))
.find((origin) => origin === parentsOrigin);
if (!allowFrom) {
return "deny";
}
@@ -8,7 +8,6 @@ import {
graphqlExpress,
} from "apollo-server-express/dist/expressApollo";
import { Omit } from "coral-common/types";
import { Config } from "coral-server/config";
import {
ErrorWrappingExtension,
@@ -1,4 +1,2 @@
export { default as graphqlMiddleware } from "./graphqlMiddleware";
export {
default as persistedQueryMiddleware,
} from "./persistedQueryMiddleware";
export { default as persistedQueryMiddleware } from "./persistedQueryMiddleware";
+1 -3
View File
@@ -6,9 +6,7 @@ export interface InstalledMiddlewareOptions {
redirectIfInstalled?: boolean;
}
const DefaultInstalledMiddlewareOptions: Required<
InstalledMiddlewareOptions
> = {
const DefaultInstalledMiddlewareOptions: Required<InstalledMiddlewareOptions> = {
redirectIfInstalled: false,
redirectURL: "/install",
};
@@ -1,6 +1,6 @@
import Joi from "@hapi/joi";
import { CookieOptions, NextFunction, RequestHandler, Response } from "express";
import { Redis } from "ioredis";
import Joi from "joi";
import jwt from "jsonwebtoken";
import { DateTime } from "luxon";
import passport, { Authenticator } from "passport";
@@ -64,12 +64,10 @@ interface LogoutToken {
exp?: number;
}
const LogoutTokenSchema = Joi.object()
.keys({
jti: Joi.string().default(undefined),
exp: Joi.number().default(undefined),
})
.optionalKeys(["jti", "exp"]);
const LogoutTokenSchema = Joi.object().keys({
jti: Joi.string().optional(),
exp: Joi.number().optional(),
});
export async function handleLogout(redis: Redis, req: Request, res: Response) {
// Extract the token from the request.
@@ -159,7 +157,7 @@ const generateCookieOptions = (
// anyways, so don't bother setting `None` when we're not secure. The only
// time we aren't behind HTTPS is when we're testing/in development where the
// the setting for `SameSite: Lax` would be OK.
sameSite: req.secure ? "None" : "Lax",
sameSite: req.secure ? "none" : "lax",
expires: expiresIn,
});
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { Db } from "mongodb";
import { Strategy as LocalStrategy } from "passport-local";
@@ -27,10 +27,7 @@ const verifyFactory = (
// Validate that the email address and password are reasonable.
const email = Joi.attempt(
emailInput,
Joi.string()
.trim()
.lowercase()
.email()
Joi.string().trim().lowercase().email()
);
const password = Joi.attempt(passwordInput, Joi.string());
@@ -1,4 +1,5 @@
import { Db } from "mongodb";
import { Strategy as BaseStrategy, StrategyCreated } from "passport";
import { Strategy } from "passport-strategy";
import { Config } from "coral-server/config";
@@ -27,12 +28,12 @@ export interface OAuth2StrategyOptions {
export default abstract class OAuth2Strategy<
T extends OAuth2Integration,
U extends Strategy
U extends BaseStrategy
> extends Strategy {
public abstract name: string;
protected config: Config;
protected mongo: Db;
protected cache: TenantCacheAdapter<U>;
protected cache: TenantCacheAdapter<Strategy>;
private authenticateOptions: Record<string, any>;
constructor({
@@ -118,17 +119,21 @@ export default abstract class OAuth2Strategy<
let strategy = this.cache.get(tenant.id);
if (!strategy) {
strategy = this.createStrategy(tenant, integration as Required<T>);
strategy = this.createStrategy(
tenant,
integration as Required<T>
) as StrategyCreated<U>;
// Augment the strategy with the request method bindings.
strategy.error = this.error.bind(this);
strategy.fail = this.fail.bind(this);
strategy.pass = this.pass.bind(this);
strategy.redirect = this.redirect.bind(this);
strategy.success = this.success.bind(this);
this.cache.set(tenant.id, strategy);
}
// Augment the strategy with the request method bindings.
strategy.error = this.error.bind(this);
strategy.fail = this.fail.bind(this);
strategy.pass = this.pass.bind(this);
strategy.redirect = this.redirect.bind(this);
strategy.success = this.success.bind(this);
strategy.authenticate(req, {
session: false,
...this.authenticateOptions,
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import jwt from "jsonwebtoken";
import jwks, {
CertSigningKey,
@@ -58,21 +58,24 @@ export const OIDCIDTokenSchema = Joi.object()
sub: Joi.string().required(),
iss: Joi.string().required(),
aud: Joi.string().required(),
email: Joi.string().default(undefined),
email: Joi.string(),
email_verified: Joi.boolean().default(false),
picture: Joi.string().default(undefined),
name: Joi.string().default(undefined),
nickname: Joi.string().default(undefined),
preferred_username: Joi.string().default(undefined),
picture: Joi.string(),
name: Joi.string(),
nickname: Joi.string(),
preferred_username: Joi.string(),
})
.optionalKeys([
"picture",
"email",
"email_verified",
"name",
"nickname",
"preferred_username",
]);
.fork(
[
"picture",
"email",
"email_verified",
"name",
"nickname",
"preferred_username",
],
(s) => s.optional()
);
export interface StrategyItem {
strategy: OAuth2Strategy;
@@ -80,7 +83,7 @@ export interface StrategyItem {
}
export function isOIDCToken(token: OIDCIDToken | object): token is OIDCIDToken {
const { error } = Joi.validate(token, OIDCIDTokenSchema, {
const { error } = OIDCIDTokenSchema.validate(token, {
// OIDC ID tokens may contain many other fields we haven't seen.. We Just
// need to check to see that it contains at least the fields we need.
allowUnknown: true,
@@ -31,8 +31,6 @@ it("validates a jwt token", async () => {
expect(isJWTToken(token)).toBeTruthy();
expect((token as JWTToken).exp).toBe(
DateTime.fromJSDate(now)
.plus({ seconds: 100 })
.toSeconds()
DateTime.fromJSDate(now).plus({ seconds: 100 }).toSeconds()
);
});
@@ -1,5 +1,5 @@
import Joi from "@hapi/joi";
import { Redis } from "ioredis";
import Joi from "joi";
import { isNil } from "lodash";
import { Db } from "mongodb";
@@ -36,7 +36,7 @@ export const JWTTokenSchema = Joi.object().keys({
});
export function isJWTToken(token: JWTToken | object): token is JWTToken {
const { error } = Joi.validate(token, JWTTokenSchema, {
const { error } = JWTTokenSchema.validate(token, {
allowUnknown: true,
});
return isNil(error);
@@ -14,7 +14,7 @@ import {
OIDCIDToken,
} from "../oidc";
export type OIDCIDToken = OIDCIDToken;
export { OIDCIDToken } from "../oidc";
export type OIDCVerifierOptions = Pick<
AppOptions,
@@ -1,5 +1,5 @@
import Joi from "@hapi/joi";
import { Redis } from "ioredis";
import Joi from "joi";
import { isNil, throttle } from "lodash";
import { DateTime } from "luxon";
import { Db } from "mongodb";
@@ -57,31 +57,27 @@ export interface SSOToken {
}
export function isSSOToken(token: SSOToken | object): token is SSOToken {
const { error } = Joi.validate(token, SSOTokenSchema, { allowUnknown: true });
const { error } = SSOTokenSchema.validate(token, { allowUnknown: true });
return isNil(error);
}
export const SSOUserProfileSchema = Joi.object()
.keys({
id: Joi.string().required(),
email: Joi.string()
.lowercase()
.required(),
username: Joi.string().required(),
badges: Joi.array().items(Joi.string()),
role: Joi.string().only(Object.values(GQLUSER_ROLE)),
url: Joi.string().uri(),
})
.optionalKeys(["badges", "role", "url"]);
export const SSOUserProfileSchema = Joi.object().keys({
id: Joi.string().required(),
email: Joi.string().lowercase().required(),
username: Joi.string().required(),
badges: Joi.array().items(Joi.string()).optional(),
role: Joi.string()
.valid(...Object.values(GQLUSER_ROLE))
.optional(),
url: Joi.string().uri().optional(),
});
export const SSOTokenSchema = Joi.object()
.keys({
jti: Joi.string().default(undefined),
exp: Joi.number().default(undefined),
iat: Joi.number().default(undefined),
user: SSOUserProfileSchema.required(),
})
.optionalKeys(["jti", "exp", "iat"]);
export const SSOTokenSchema = Joi.object().keys({
jti: Joi.string().optional(),
exp: Joi.number().optional(),
iat: Joi.number().optional(),
user: SSOUserProfileSchema.required(),
});
export async function findOrCreateSSOUser(
mongo: Db,
@@ -196,7 +192,7 @@ export function getRelevantSSOKeys(
kid?: string
): Secret[] {
// Collect all the current valid keys.
const keys = integration.keys.filter(k => {
const keys = integration.keys.filter((k) => {
if (k.inactiveAt && now >= k.inactiveAt) {
return false;
}
@@ -215,7 +211,7 @@ export function getRelevantSSOKeys(
// The token has a kid, so if we have a matching token, we should use it. If
// we don't have a matching kid, we can't possibly verify it, so throw an
// error.
const key = keys.find(k => k.kid === kid);
const key = keys.find((k) => k.kid === kid);
if (!key) {
throw new TokenInvalidError(
tokenString,
+1 -1
View File
@@ -1,4 +1,4 @@
import uuid from "uuid/v1";
import { v1 as uuid } from "uuid";
import { TenantNotFoundError } from "coral-server/errors";
import logger from "coral-server/logger";
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`throws an error for missing fields 1`] = `"child \\"d\\" fails because [\\"d\\" is required]"`;
exports[`throws an error for missing fields 1`] = `"\\"d\\" is required"`;
+1 -1
View File
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
import { validate } from "coral-server/app/request/body";
+3 -3
View File
@@ -1,4 +1,4 @@
import Joi from "joi";
import Joi from "@hapi/joi";
/**
* validate will strip unknown fields and perform validation against it. It will
@@ -7,9 +7,9 @@ import Joi from "joi";
* @param schema the Joi schema to validate against
* @param body the body to parse and strip of unknown fields
*/
export const validate = (schema: Joi.SchemaLike, body: any) => {
export const validate = (schema: Joi.Schema, body: any) => {
// Extract the schema from the request.
const { value, error: err } = Joi.validate(body, schema, {
const { value, error: err } = schema.validate(body, {
stripUnknown: true,
presence: "required",
abortEarly: false,
+3 -3
View File
@@ -4,7 +4,6 @@ import { Redis } from "ioredis";
import { DateTime } from "luxon";
import ms from "ms";
import { Omit } from "coral-common/types";
import { Config } from "coral-server/config";
import { RateLimitExceeded } from "coral-server/errors";
import { Request } from "coral-server/types/express";
@@ -41,8 +40,9 @@ export class Limiter {
}
private key(key: string, resource?: string, operation?: string): string {
return `limiter[${this.prefix}][${resource || this.resource}][${operation ||
this.operation}][${key}]`;
return `limiter[${this.prefix}][${resource || this.resource}][${
operation || this.operation
}][${key}]`;
}
public async test(
+3 -9
View File
@@ -1,5 +1,5 @@
import Joi from "@hapi/joi";
import convict from "convict";
import Joi from "joi";
import { parseConnectionString } from "mongodb-core";
import ms from "ms";
import os from "os";
@@ -13,7 +13,7 @@ import { InternalError } from "./errors";
convict.addFormat({
name: "mongo-uri",
validate: (url: string) => {
parseConnectionString(url, err => {
parseConnectionString(url, (err) => {
if (err) {
throw new InternalError(err, "invalid mongo-uri");
}
@@ -51,13 +51,7 @@ convict.addFormat({
convict.addFormat({
name: "ms",
validate: (val: number) => {
Joi.assert(
val,
Joi.number()
.positive()
.integer()
.required()
);
Joi.assert(val, Joi.number().positive().integer().required());
},
coerce: (val: string): number => ms(val),
});
+13 -16
View File
@@ -90,23 +90,20 @@ const processNotificationDigesting = (
);
// Group the digests.
const digests = user.digests.reduce(
(acc, entry) => {
const digest = acc.find(d => d.template === entry.template.name);
if (digest) {
digest.contexts.push(entry.template.context);
} else {
acc.push({
template: entry.template.name,
partial: path.basename(entry.template.name),
contexts: [entry.template.context],
});
}
const digests = user.digests.reduce((acc, entry) => {
const digest = acc.find((d) => d.template === entry.template.name);
if (digest) {
digest.contexts.push(entry.template.context);
} else {
acc.push({
template: entry.template.name,
partial: path.basename(entry.template.name),
contexts: [entry.template.context],
});
}
return acc;
},
[] as DigestElement[]
);
return acc;
}, [] as DigestElement[]);
// TODO: sort the digest template elements by the digest order.
+2 -2
View File
@@ -1,5 +1,5 @@
import { CronCommand, CronJob } from "cron";
import uuid from "uuid";
import { v1 as uuid } from "uuid";
import { createTimer } from "coral-server/helpers";
import logger, { Logger } from "coral-server/logger";
@@ -39,7 +39,7 @@ export class ScheduledJob<T extends {} = {}> {
private command(command: ScheduledJobCommand<T>): CronCommand {
return async () => {
const log = this.log.child({ scheduledExecutionID: uuid.v1() }, true);
const log = this.log.child({ scheduledExecutionID: uuid() }, true);
log.info("now starting scheduled job");
const timer = createTimer();
try {
+6 -6
View File
@@ -2,7 +2,7 @@
import { FluentBundle } from "@fluent/bundle/compat";
import { MongoError } from "mongodb";
import uuid from "uuid";
import { v1 as uuid } from "uuid";
import { VError } from "verror";
import { ALLOWED_USERNAME_CHANGE_TIMEFRAME_DURATION } from "coral-common/constants";
@@ -169,7 +169,7 @@ export class CoralError extends VError {
this.name = new.target.name;
// Assign a unique ID to this error.
const id = uuid.v1();
const id = uuid();
this.status = status;
// Capture the context for the error.
@@ -307,10 +307,10 @@ export class UsernameAlreadySetError extends CoralError {
export class UsernameUpdatedWithinWindowError extends CoralError {
constructor(lastUpdate: Date) {
const { scaled, unit } = reduceSeconds(
ALLOWED_USERNAME_CHANGE_TIMEFRAME_DURATION,
[TIME.DAY]
);
const {
scaled,
unit,
} = reduceSeconds(ALLOWED_USERNAME_CHANGE_TIMEFRAME_DURATION, [TIME.DAY]);
super({
code: ERROR_CODES.USERNAME_UPDATED_WITHIN_WINDOW,
context: {
+1 -1
View File
@@ -1,4 +1,4 @@
import uuid from "uuid/v4";
import { v4 as uuid } from "uuid";
import logger from "coral-server/logger";
+8 -11
View File
@@ -28,22 +28,19 @@ export class NotifierCoralEventListener
* events are the events that this listener handles. These are parsed from the
* notification categories.
*/
public readonly events = categories.reduce(
(events, category) => {
for (const event of category.events) {
if (!events.includes(event)) {
events.push(event);
}
public readonly events = categories.reduce((events, category) => {
for (const event of category.events) {
if (!events.includes(event)) {
events.push(event);
}
}
return events;
},
[] as CoralEventType[]
);
return events;
}, [] as CoralEventType[]);
public initialize: CoralEventPublisherFactory<
NotifierCoralEventListenerPayloads
> = ({ tenant: { id } }) => async input => {
> = ({ tenant: { id } }) => async (input) => {
await this.queue.add({ tenantID: id, input });
};
}
@@ -29,7 +29,9 @@ export class PerspectiveCoralEventListener
public initialize: CoralEventPublisherFactory<
PerspectiveCoralEventListenerPayloads
> = ctx => async ({ data: { newStatus, commentID, commentRevisionID } }) => {
> = (ctx) => async ({
data: { newStatus, commentID, commentRevisionID },
}) => {
const {
tenant: {
integrations: { perspective },
@@ -63,7 +65,9 @@ export class PerspectiveCoralEventListener
}
// Get the target revision.
const revision = comment.revisions.find(r => r.id === commentRevisionID);
const revision = comment.revisions.find(
(r) => r.id === commentRevisionID
);
if (!revision) {
return;
}
@@ -76,7 +76,7 @@ export class SlackCoralEventListener
public initialize: CoralEventPublisherFactory<
SlackCoralEventListenerPayloads
> = ctx => async payload => {
> = (ctx) => async (payload) => {
const {
tenant: { id: tenantID, slack },
} = ctx;
@@ -87,7 +87,7 @@ export class SlackCoralEventListener
// Or there are no slack channels,
slack.channels.length === 0 ||
// Or each channel isn't enabled or configured right.
slack.channels.every(c => !c.enabled || !c.hookURL)
slack.channels.every((c) => !c.enabled || !c.hookURL)
) {
// Exit out then.
return;
+3 -3
View File
@@ -22,7 +22,7 @@ export class WebhookCoralEventListener
public initialize: CoralEventPublisherFactory<
WebhookCoralEventListenerPayloads
> = ({ id: contextID, tenant }) => async event => {
> = ({ id: contextID, tenant }) => async (event) => {
const log = logger.child(
{
tenantID: tenant.id,
@@ -33,7 +33,7 @@ export class WebhookCoralEventListener
);
// Based on the incoming event, determine which endpoints we should send.
const endpoints = tenant.webhooks.endpoints.filter(endpoint => {
const endpoints = tenant.webhooks.endpoints.filter((endpoint) => {
// If the endpoint is disabled, don't include it.
if (!endpoint.enabled) {
return false;
@@ -72,7 +72,7 @@ export class WebhookCoralEventListener
// For each of these endpoints that need a delivery of these notifications,
// queue up the job that will send it.
await Promise.all(
endpoints.map(endpoint =>
endpoints.map((endpoint) =>
this.queue.add({
tenantID: tenant.id,
contextID,
+1 -1
View File
@@ -89,7 +89,7 @@ export class CoralEventPublisherBroker {
const publishers = this.registry.get(payload.type)!;
// Begin resolving these publishers.
return Promise.all(publishers.map(publisher => publisher(payload)));
return Promise.all(publishers.map((publisher) => publisher(payload)));
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { RedisPubSub } from "graphql-redis-subscriptions";
import { Db } from "mongodb";
import uuid from "uuid";
import { v1 as uuid } from "uuid";
import { LanguageCode } from "coral-common/helpers/i18n/locales";
import { Config } from "coral-server/config";
@@ -81,7 +81,7 @@ export default class GraphContext {
public readonly user?: User;
constructor(options: GraphContextOptions) {
this.id = options.id || uuid.v1();
this.id = options.id || uuid();
this.now = options.now || new Date();
this.lang = options.lang || options.i18n.getDefaultLang();
this.disableCaching = options.disableCaching || false;
+1 -1
View File
@@ -81,7 +81,7 @@ const auth: DirectiveResolverFn<
(!permit && conditions.length > 0) ||
// If the permit was specified, and some of the conditions for the user
// aren't in the list of permitted conditions, then error.
(permit && conditions.some(condition => !permit.includes(condition)))
(permit && conditions.some((condition) => !permit.includes(condition)))
) {
// Compute the resource that the user was attempting to access.
const resource = calculateLocationKey(info);
+2 -8
View File
@@ -51,15 +51,9 @@ const rate: DirectiveResolverFn<
const key = `${tenant.id}:rl:${user.id}:${info.operation.operation}.${resource}`;
// Perform the rate limiting check.
const [[, tries]] = await redis
.multi()
.incr(key)
.expire(key, seconds)
.exec();
const [[, tries]] = await redis.multi().incr(key).expire(key, seconds).exec();
if (tries && tries > max) {
const resetsAt = DateTime.fromJSDate(now)
.plus({ seconds })
.toJSDate();
const resetsAt = DateTime.fromJSDate(now).plus({ seconds }).toJSDate();
throw new RateLimitExceeded(key, max, resetsAt, tries);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export async function mapFieldsetToErrorCodes<T>(
continue;
}
if (errorMap[param].some(code => err.code === code)) {
if (errorMap[param].some((code) => err.code === code)) {
err.param = param;
break;
}
@@ -1,5 +1,5 @@
import { ApolloError } from "apollo-server-core";
import { GraphQLError } from "graphql";
import { GraphQLFormattedError } from "graphql";
import { GraphQLExtension, GraphQLResponse } from "graphql-extensions";
import { merge } from "lodash";
@@ -9,10 +9,14 @@ import {
InternalError,
} from "coral-server/errors";
import GraphContext from "coral-server/graph/context";
import { getOriginalError } from "./helpers";
function hoistCoralErrorExtensions(ctx: GraphContext, err: GraphQLError): void {
function hoistCoralErrorExtensions(
ctx: GraphContext,
err: GraphQLFormattedError
): void {
// Grab or wrap the originalError so that it's a CoralError.
const originalError = extractOriginalError(err, ctx);
const originalError = getWrappedOriginalError(err, ctx);
if (!originalError) {
return;
}
@@ -25,7 +29,7 @@ function hoistCoralErrorExtensions(ctx: GraphContext, err: GraphQLError): void {
// Hoist the message from the original error into the message of the base
// error.
err.message = extensions.message;
(err as any).message = extensions.message;
// Re-hoist the extensions.
merge(err.extensions, extensions);
@@ -39,8 +43,8 @@ function hoistCoralErrorExtensions(ctx: GraphContext, err: GraphQLError): void {
* @param err the error to have their original error extracted from.
* @param ctx the Context to extract the environment state.
*/
function extractOriginalError(
err: GraphQLError,
function getWrappedOriginalError(
err: GraphQLFormattedError,
ctx: GraphContext
): CoralError | undefined {
if (err instanceof ApolloError) {
@@ -49,23 +53,25 @@ function extractOriginalError(
return;
}
if (!err.originalError) {
const originalError = getOriginalError(err);
if (!originalError) {
// Only errors that have an originalError need to be hoisted.
return;
}
if (err.originalError instanceof CoralError) {
return err.originalError;
if (originalError instanceof CoralError) {
return originalError;
}
if (ctx.config.get("env") !== "production") {
return new InternalDevelopmentError(
err.originalError,
originalError,
"wrapped internal development error"
);
}
return new InternalError(err.originalError, "wrapped internal error");
return new InternalError(originalError, "wrapped internal error");
}
/**
@@ -76,8 +82,8 @@ function extractOriginalError(
*/
export function enrichError(
ctx: GraphContext,
err: GraphQLError
): GraphQLError {
err: GraphQLFormattedError
): GraphQLFormattedError {
if (err.extensions) {
// Delete the exception field from the error extension, we never need to
// provide that data.
@@ -85,7 +91,7 @@ export function enrichError(
delete err.extensions.exception;
}
if (err.originalError) {
if (getOriginalError(err)) {
// Hoist the extensions onto the error.
hoistCoralErrorExtensions(ctx, err);
}
@@ -104,7 +110,7 @@ export class ErrorWrappingExtension implements GraphQLExtension<GraphContext> {
...o,
graphqlResponse: {
...o.graphqlResponse,
errors: o.graphqlResponse.errors.map(err =>
errors: o.graphqlResponse.errors.map((err) =>
enrichError(o.context, err)
),
},
@@ -1,4 +1,4 @@
import { DocumentNode, ExecutionArgs, GraphQLError } from "graphql";
import { DocumentNode, ExecutionArgs, GraphQLFormattedError } from "graphql";
import {
EndHandler,
GraphQLExtension,
@@ -11,7 +11,7 @@ import logger from "coral-server/logger";
import { getOperationMetadata, getPersistedQueryMetadata } from "./helpers";
export function logError(ctx: GraphContext, err: GraphQLError) {
export function logError(ctx: GraphContext, err: GraphQLFormattedError) {
ctx.logger.error({ err }, "graphql query error");
}
@@ -102,7 +102,7 @@ export class LoggerExtension implements GraphQLExtension<GraphContext> {
context: GraphContext;
}): void {
if (response.graphqlResponse.errors) {
response.graphqlResponse.errors.forEach(err =>
response.graphqlResponse.errors.forEach((err) =>
logError(response.context, err)
);
}
@@ -1,5 +1,6 @@
import {
DocumentNode,
GraphQLFormattedError,
OperationDefinitionNode,
OperationTypeNode,
} from "graphql";
@@ -66,3 +67,16 @@ export const getPersistedQueryMetadata = ({
operation,
operationName,
});
/**
* getOriginalError tries to return the original error from a
* formatted GraphQL error.
*
* @param err A GraphQL Formatted Error
*/
export const getOriginalError = (err: GraphQLFormattedError) => {
if ((err as any).originalError) {
return (err as any).originalError as Error;
}
return null;
};
+2 -2
View File
@@ -11,8 +11,8 @@ export default (ctx: GraphContext) => ({
string,
GQLDiscoveredOIDCConfiguration | null
>(
issuers =>
Promise.all(issuers.map(issuer => discoverOIDCConfiguration(issuer))),
(issuers) =>
Promise.all(issuers.map((issuer) => discoverOIDCConfiguration(issuer))),
{
// Disable caching for the DataLoader if the Context is designed to be
// long lived.
+1 -1
View File
@@ -68,7 +68,7 @@ const primeCommentsFromConnection = (ctx: Context) => (
) => {
if (!ctx.disableCaching) {
// For each of the nodes, prime the comment loader.
connection.nodes.forEach(comment => {
connection.nodes.forEach((comment) => {
ctx.loaders.Comments.visible.prime(comment.id, comment);
});
}
+1 -1
View File
@@ -12,7 +12,7 @@ import { QueryToSitesArgs } from "coral-server/graph/schema/__generated__/types"
export default (ctx: TenantContext) => ({
site: new DataLoader<string, Site | null>(
ids => retrieveManySites(ctx.mongo, ctx.tenant.id, ids),
(ids) => retrieveManySites(ctx.mongo, ctx.tenant.id, ids),
{
cache: !ctx.disableCaching,
}
+2 -2
View File
@@ -145,7 +145,7 @@ const primeStoriesFromConnection = (ctx: GraphContext) => (
) => {
if (!ctx.disableCaching) {
// For each of these nodes, prime the story loader.
connection.nodes.forEach(story => {
connection.nodes.forEach((story) => {
ctx.loaders.Stories.story.prime(story.id, story);
});
}
@@ -186,7 +186,7 @@ export default (ctx: GraphContext) => ({
}
),
story: new DataLoader<string, Story | null>(
ids => retrieveManyStories(ctx.mongo, ctx.tenant.id, ids),
(ids) => retrieveManyStories(ctx.mongo, ctx.tenant.id, ids),
{
// Disable caching for the DataLoader if the Context is designed to be
// long lived.
+2 -2
View File
@@ -88,7 +88,7 @@ const primeUsersFromConnection = (ctx: Context) => (
) => {
if (!ctx.disableCaching) {
// For each of the nodes, prime the user loader.
connection.nodes.forEach(user => {
connection.nodes.forEach((user) => {
ctx.loaders.Users.user.prime(user.id, user);
});
}
@@ -98,7 +98,7 @@ const primeUsersFromConnection = (ctx: Context) => (
export default (ctx: Context) => {
const user = new DataLoader<string, User | null>(
ids => retrieveManyUsers(ctx.mongo, ctx.tenant.id, ids),
(ids) => retrieveManyUsers(ctx.mongo, ctx.tenant.id, ids),
{
// Disable caching for the DataLoader if the Context is designed to be
// long lived.
+3 -2
View File
@@ -27,7 +27,7 @@ export class SingletonResolver<T> {
return this.cache;
}
const promise = this.resolver().then(result => {
const promise = this.resolver().then((result) => {
return result;
});
@@ -41,5 +41,6 @@ export class SingletonResolver<T> {
export function createManyBatchLoadFn<U, V>(
batchLoadFn: (input: U) => Promise<V>
) {
return (inputs: U[]) => Promise.all(inputs.map(input => batchLoadFn(input)));
return (inputs: U[]) =>
Promise.all(inputs.map((input) => batchLoadFn(input)));
}
+2 -2
View File
@@ -168,7 +168,7 @@ export const Comments = (ctx: GraphContext) => ({
GQLTAG.FEATURED,
ctx.now
)
.then(comment =>
.then((comment) =>
comment.status !== GQLCOMMENT_STATUS.APPROVED
? approveComment(
ctx.mongo,
@@ -182,7 +182,7 @@ export const Comments = (ctx: GraphContext) => ({
)
: comment
)
.then(comment => {
.then((comment) => {
// Publish that the comment was featured.
publishCommentFeatured(ctx.broker, comment);
-2
View File
@@ -1,5 +1,3 @@
import { Omit } from "coral-common/types";
/**
* validateMaximumLength will limit the value of an optional string to the
* specified amount, otherwise will throw an error.
@@ -5,12 +5,10 @@ import {
const disabled = { enabled: false };
export const AuthIntegrations: GQLAuthIntegrationsTypeResolver<
GQLAuthIntegrations
> = {
local: auth => auth.local || disabled,
sso: auth => auth.sso || disabled,
oidc: auth => auth.oidc || disabled,
google: auth => auth.google || disabled,
facebook: auth => auth.facebook || disabled,
export const AuthIntegrations: GQLAuthIntegrationsTypeResolver<GQLAuthIntegrations> = {
local: (auth) => auth.local || disabled,
sso: (auth) => auth.sso || disabled,
oidc: (auth) => auth.oidc || disabled,
google: (auth) => auth.google || disabled,
facebook: (auth) => auth.facebook || disabled,
};
+1 -1
View File
@@ -8,5 +8,5 @@ export type BanStatusInput = user.ConsolidatedBanStatus & {
export const BanStatus: Required<GQLBanStatusTypeResolver<BanStatusInput>> = {
active: ({ active }) => active,
history: ({ history, userID }) =>
history.map(status => ({ ...status, userID })),
history.map((status) => ({ ...status, userID })),
};
@@ -1,9 +1,9 @@
import { GQLBanStatusHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
export const BanStatusHistory: Required<
GQLBanStatusHistoryTypeResolver<user.BanStatusHistory>
> = {
export const BanStatusHistory: Required<GQLBanStatusHistoryTypeResolver<
user.BanStatusHistory
>> = {
active: ({ active }) => active,
createdBy: ({ createdBy }, input, ctx) => {
if (createdBy) {
@@ -2,9 +2,7 @@ import { GQLCloseCommentingTypeResolver } from "coral-server/graph/schema/__gene
import * as settings from "coral-server/models/settings";
import { translate } from "coral-server/services/i18n";
export const CloseCommenting: GQLCloseCommentingTypeResolver<
settings.CloseCommenting
> = {
export const CloseCommenting: GQLCloseCommentingTypeResolver<settings.CloseCommenting> = {
message: (closeCommenting, input, ctx) => {
if (closeCommenting.message) {
return closeCommenting.message;
+7 -6
View File
@@ -44,15 +44,16 @@ export const maybeLoadOnlyID = (
};
export const Comment: GQLCommentTypeResolver<comment.Comment> = {
body: c => (c.revisions.length > 0 ? getLatestRevision(c).body : null),
body: (c) => (c.revisions.length > 0 ? getLatestRevision(c).body : null),
// Send the whole comment back when you request revisions. This way, we get to
// know the comment ID. The field mapping is handled by the CommentRevision
// resolver.
revision: c =>
revision: (c) =>
c.revisions.length > 0
? { revision: getLatestRevision(c), comment: c }
: null,
revisionHistory: c => c.revisions.map(revision => ({ revision, comment: c })),
revisionHistory: (c) =>
c.revisions.map((revision) => ({ revision, comment: c })),
editing: ({ revisions, createdAt }, input, ctx) => ({
// When there is more than one body history, then the comment has been
// edited.
@@ -84,7 +85,7 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
);
},
// Action Counts are encoded, decode them for use with the GraphQL system.
actionCounts: c => decodeActionCounts(c.actionCounts),
actionCounts: (c) => decodeActionCounts(c.actionCounts),
flags: ({ id }, { first, after }, ctx) =>
ctx.loaders.CommentActions.connection({
first: defaultTo(first, 10),
@@ -96,8 +97,8 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
}),
viewerActionPresence: (c, input, ctx) =>
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
parentCount: c => (hasAncestors(c) ? c.ancestorIDs.length : 0),
depth: c => (hasAncestors(c) ? c.ancestorIDs.length : 0),
parentCount: (c) => (hasAncestors(c) ? c.ancestorIDs.length : 0),
depth: (c) => (hasAncestors(c) ? c.ancestorIDs.length : 0),
rootParent: (c, input, ctx, info) =>
hasAncestors(c)
? maybeLoadOnlyID(ctx, info, c.ancestorIDs[c.ancestorIDs.length - 1])
@@ -3,9 +3,7 @@ import { GQLCommentCreatedPayloadTypeResolver } from "coral-server/graph/schema/
import { maybeLoadOnlyID } from "./Comment";
import { CommentCreatedInput } from "./Subscription/commentCreated";
export const CommentCreatedPayload: GQLCommentCreatedPayloadTypeResolver<
CommentCreatedInput
> = {
export const CommentCreatedPayload: GQLCommentCreatedPayloadTypeResolver<CommentCreatedInput> = {
comment: ({ commentID }, args, ctx, info) =>
maybeLoadOnlyID(ctx, info, commentID),
};
@@ -3,9 +3,7 @@ import { GQLCommentEnteredModerationQueuePayloadTypeResolver } from "coral-serve
import { maybeLoadOnlyID } from "./Comment";
import { CommentEnteredModerationQueueInput } from "./Subscription/commentEnteredModerationQueue";
export const CommentEnteredModerationQueuePayload: GQLCommentEnteredModerationQueuePayloadTypeResolver<
CommentEnteredModerationQueueInput
> = {
export const CommentEnteredModerationQueuePayload: GQLCommentEnteredModerationQueuePayloadTypeResolver<CommentEnteredModerationQueueInput> = {
comment: ({ commentID }, args, ctx, info) =>
maybeLoadOnlyID(ctx, info, commentID),
};
@@ -3,9 +3,7 @@ import { GQLCommentLeftModerationQueuePayloadTypeResolver } from "coral-server/g
import { maybeLoadOnlyID } from "./Comment";
import { CommentLeftModerationQueueInput } from "./Subscription/commentLeftModerationQueue";
export const CommentLeftModerationQueuePayload: GQLCommentLeftModerationQueuePayloadTypeResolver<
CommentLeftModerationQueueInput
> = {
export const CommentLeftModerationQueuePayload: GQLCommentLeftModerationQueuePayloadTypeResolver<CommentLeftModerationQueueInput> = {
comment: ({ commentID }, args, ctx, info) =>
maybeLoadOnlyID(ctx, info, commentID),
};
@@ -2,9 +2,7 @@ import * as actions from "coral-server/models/action/moderation/comment";
import { GQLCommentModerationActionTypeResolver } from "../schema/__generated__/types";
export const CommentModerationAction: GQLCommentModerationActionTypeResolver<
actions.CommentModerationAction
> = {
export const CommentModerationAction: GQLCommentModerationActionTypeResolver<actions.CommentModerationAction> = {
revision: async (action, input, ctx) => {
const comment = await ctx.loaders.Comments.visible.load(action.commentID);
if (!comment) {
@@ -3,9 +3,7 @@ import { GQLCommentReleasedPayloadTypeResolver } from "coral-server/graph/schema
import { maybeLoadOnlyID } from "./Comment";
import { CommentReleasedInput } from "./Subscription/commentReleased";
export const CommentReleasedPayload: GQLCommentReleasedPayloadTypeResolver<
CommentReleasedInput
> = {
export const CommentReleasedPayload: GQLCommentReleasedPayloadTypeResolver<CommentReleasedInput> = {
comment: ({ commentID }, args, ctx, info) =>
maybeLoadOnlyID(ctx, info, commentID),
};
@@ -3,9 +3,7 @@ import { GQLCommentReplyCreatedPayloadTypeResolver } from "coral-server/graph/sc
import { maybeLoadOnlyID } from "./Comment";
import { CommentReplyCreatedInput } from "./Subscription/commentReplyCreated";
export const CommentReplyCreatedPayload: GQLCommentReplyCreatedPayloadTypeResolver<
CommentReplyCreatedInput
> = {
export const CommentReplyCreatedPayload: GQLCommentReplyCreatedPayloadTypeResolver<CommentReplyCreatedInput> = {
comment: ({ commentID }, args, ctx, info) =>
maybeLoadOnlyID(ctx, info, commentID),
};
@@ -7,14 +7,14 @@ export interface WrappedCommentRevision {
comment: Comment;
}
export const CommentRevision: Required<
GQLCommentRevisionTypeResolver<WrappedCommentRevision>
> = {
id: w => w.revision.id,
comment: w => w.comment,
actionCounts: w => decodeActionCounts(w.revision.actionCounts),
body: w => w.revision.body,
export const CommentRevision: Required<GQLCommentRevisionTypeResolver<
WrappedCommentRevision
>> = {
id: (w) => w.revision.id,
comment: (w) => w.comment,
actionCounts: (w) => decodeActionCounts(w.revision.actionCounts),
body: (w) => w.revision.body,
// Defaults to an empty object if not set on the revision.
metadata: w => w.revision.metadata || {},
createdAt: w => w.revision.createdAt,
metadata: (w) => w.revision.metadata || {},
createdAt: (w) => w.revision.createdAt,
};
@@ -3,9 +3,7 @@ import { GQLCommentStatusUpdatedPayloadTypeResolver } from "coral-server/graph/s
import { maybeLoadOnlyID } from "./Comment";
import { CommentStatusUpdatedInput } from "./Subscription/commentStatusUpdated";
export const CommentStatusUpdatedPayload: GQLCommentStatusUpdatedPayloadTypeResolver<
CommentStatusUpdatedInput
> = {
export const CommentStatusUpdatedPayload: GQLCommentStatusUpdatedPayloadTypeResolver<CommentStatusUpdatedInput> = {
moderator: ({ moderatorID }, args, ctx) =>
moderatorID ? ctx.loaders.Users.user.load(moderatorID) : null,
comment: ({ commentID }, args, ctx, info) =>
@@ -2,9 +2,7 @@ import { GQLDisableCommentingTypeResolver } from "coral-server/graph/schema/__ge
import * as settings from "coral-server/models/settings";
import { translate } from "coral-server/services/i18n";
export const DisableCommenting: GQLDisableCommentingTypeResolver<
settings.DisableCommenting
> = {
export const DisableCommenting: GQLDisableCommentingTypeResolver<settings.DisableCommenting> = {
message: (disableCommenting, input, ctx) => {
if (disableCommenting.message) {
return disableCommenting.message;
@@ -5,9 +5,7 @@ import {
import { reconstructTenantURLResolver } from "./util";
export const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
GQLFacebookAuthIntegration
> = {
export const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<GQLFacebookAuthIntegration> = {
callbackURL: reconstructTenantURLResolver("/api/auth/facebook/callback"),
redirectURL: reconstructTenantURLResolver("/api/auth/facebook"),
};
@@ -5,9 +5,7 @@ import {
import { reconstructTenantURLResolver } from "./util";
export const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
GQLGoogleAuthIntegration
> = {
export const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<GQLGoogleAuthIntegration> = {
callbackURL: reconstructTenantURLResolver("/api/auth/google/callback"),
redirectURL: reconstructTenantURLResolver("/api/auth/google"),
};
@@ -5,9 +5,7 @@ import * as settings from "coral-server/models/settings";
export type LiveConfigurationInput = settings.LiveConfiguration;
export const LiveConfiguration: GQLLiveConfigurationTypeResolver<
LiveConfigurationInput
> = {
export const LiveConfiguration: GQLLiveConfigurationTypeResolver<LiveConfigurationInput> = {
configurable: (source, args, ctx) =>
Boolean(!ctx.config.get("disable_live_updates")),
enabled: (source, args, ctx) => {
@@ -15,9 +15,7 @@ export interface ModerationQueueInput {
count: number | null;
}
export const ModerationQueue: GQLModerationQueueTypeResolver<
ModerationQueueInput
> = {
export const ModerationQueue: GQLModerationQueueTypeResolver<ModerationQueueInput> = {
id: ({ selector, connection: { filter } }) => {
// NOTE: (wyattjoh) when the queues change shape in the future, investigate adding more dynamicness to this id generation
if (filter && filter.storyID) {
@@ -134,9 +134,7 @@ export const moderationQueuesResolver: QueryToModerationQueuesResolver = async (
return sharedModerationInputResolver(source, args, ctx);
};
export const ModerationQueues: GQLModerationQueuesTypeResolver<
ModerationQueuesInput
> = {
export const ModerationQueues: GQLModerationQueuesTypeResolver<ModerationQueuesInput> = {
unmoderated: mergeModerationInputFilters(
{
status: { $in: UNMODERATED_STATUSES },
@@ -1,9 +1,9 @@
import { GQLModeratorNoteTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
export const ModeratorNote: Required<
GQLModeratorNoteTypeResolver<user.ModeratorNote>
> = {
export const ModeratorNote: Required<GQLModeratorNoteTypeResolver<
user.ModeratorNote
>> = {
createdBy: ({ createdBy }, input, ctx) => {
return ctx.loaders.Users.user.load(createdBy);
},
@@ -5,9 +5,7 @@ import {
import { reconstructTenantURLResolver } from "./util";
export const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
GQLOIDCAuthIntegration
> = {
export const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<GQLOIDCAuthIntegration> = {
callbackURL: reconstructTenantURLResolver("/api/auth/oidc/callback"),
redirectURL: reconstructTenantURLResolver("/api/auth/oidc"),
};
@@ -5,10 +5,10 @@ export type PremodStatusInput = user.ConsolidatedPremodStatus & {
userID: string;
};
export const PremodStatus: Required<
GQLPremodStatusTypeResolver<PremodStatusInput>
> = {
export const PremodStatus: Required<GQLPremodStatusTypeResolver<
PremodStatusInput
>> = {
active: ({ active }) => active,
history: ({ history, userID }) =>
history.map(status => ({ ...status, userID })),
history.map((status) => ({ ...status, userID })),
};
@@ -1,9 +1,9 @@
import { GQLPremodStatusHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
export const PremodStatusHistory: Required<
GQLPremodStatusHistoryTypeResolver<user.PremodStatusHistory>
> = {
export const PremodStatusHistory: Required<GQLPremodStatusHistoryTypeResolver<
user.PremodStatusHistory
>> = {
active: ({ active }) => active,
createdBy: ({ createdBy }, input, ctx) => {
if (createdBy) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { GQLProfileTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
const resolveType: GQLProfileTypeResolver<user.Profile> = profile => {
const resolveType: GQLProfileTypeResolver<user.Profile> = (profile) => {
switch (profile.type) {
case "local":
return "LocalProfile";
+1 -1
View File
@@ -8,5 +8,5 @@ export interface QueueInput {
}
export const Queue: Required<GQLQueueTypeResolver<QueueInput>> = {
counts: t => t.counts(),
counts: (t) => t.counts(),
};
+5 -5
View File
@@ -15,9 +15,9 @@ const get = (fn: (ctx: GraphContext) => QueueInput) => (
) => fn(ctx);
export const Queues: Required<GQLQueuesTypeResolver> = {
mailer: get(ctx => ctx.mailerQueue),
scraper: get(ctx => ctx.scraperQueue),
notifier: get(ctx => ctx.notifierQueue),
webhook: get(ctx => ctx.webhookQueue),
rejector: get(ctx => ctx.rejectorQueue),
mailer: get((ctx) => ctx.mailerQueue),
scraper: get((ctx) => ctx.scraperQueue),
notifier: get((ctx) => ctx.notifierQueue),
webhook: get((ctx) => ctx.webhookQueue),
rejector: get((ctx) => ctx.rejectorQueue),
};
@@ -4,9 +4,9 @@ export interface RecentCommentHistoryInput {
userID: string;
}
export const RecentCommentHistory: Required<
GQLRecentCommentHistoryTypeResolver<RecentCommentHistoryInput>
> = {
export const RecentCommentHistory: Required<GQLRecentCommentHistoryTypeResolver<
RecentCommentHistoryInput
>> = {
statuses: ({ userID }, args, ctx) =>
ctx.loaders.Comments.authorStatusCounts.load(userID),
};
@@ -4,12 +4,10 @@ import { GQLSSOAuthIntegrationTypeResolver } from "coral-server/graph/schema/__g
function getActiveSSOKey(keys: settings.Secret[]) {
// Any key that has been rotated cannot be the active key.
return keys.find(key => !key.rotatedAt);
return keys.find((key) => !key.rotatedAt);
}
export const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<
settings.SSOAuthIntegration
> = {
export const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<settings.SSOAuthIntegration> = {
key: ({ keys }) => {
const key = getActiveSSOKey(keys);
if (key) {
@@ -3,9 +3,7 @@ import {
GQLSlackConfigurationTypeResolver,
} from "coral-server/graph/schema/__generated__/types";
export const SlackConfiguration: GQLSlackConfigurationTypeResolver<
GQLSlackConfiguration
> = {
export const SlackConfiguration: GQLSlackConfigurationTypeResolver<GQLSlackConfiguration> = {
// TODO: Remove this when we create a migration to generate slack channels on existing tenants
channels: ({ channels = [] }) => channels,
};
+1 -1
View File
@@ -22,7 +22,7 @@ export const Story: GQLStoryTypeResolver<story.Story> = {
: GQLSTORY_STATUS.OPEN,
isClosed: (s, input, ctx) => story.isStoryClosed(ctx.tenant, s, ctx.now),
closedAt: (s, input, ctx) => story.getStoryClosedAt(ctx.tenant, s) || null,
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
commentActionCounts: (s) => decodeActionCounts(s.commentCounts.action),
commentCounts: (s): CommentCountsInput => s,
// Merge tenant settings into the story settings so we can easily inherit the
// options if they exist.
@@ -7,14 +7,12 @@ import {
GQLStorySettingsTypeResolver,
} from "../schema/__generated__/types";
export const StorySettings: GQLStorySettingsTypeResolver<
story.StorySettings
> = {
live: s => s.live || {},
export const StorySettings: GQLStorySettingsTypeResolver<story.StorySettings> = {
live: (s) => s.live || {},
moderation: (s, input, ctx) => s.moderation || ctx.tenant.moderation,
premodLinksEnable: (s, input, ctx) =>
s.premodLinksEnable || ctx.tenant.premodLinksEnable,
messageBox: s => {
messageBox: (s) => {
if (s.messageBox) {
return s.messageBox;
}
@@ -17,14 +17,15 @@ export type CommentCreatedSubscription = SubscriptionType<
CommentCreatedInput
>;
export const commentCreated: SubscriptionToCommentCreatedResolver<
CommentCreatedInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_CREATED, {
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
export const commentCreated: SubscriptionToCommentCreatedResolver<CommentCreatedInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_CREATED,
{
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -22,22 +22,23 @@ export type CommentEnteredModerationQueueSubscription = SubscriptionType<
CommentEnteredModerationQueueInput
>;
export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerationQueueResolver<
CommentEnteredModerationQueueInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE, {
filter: (source, { storyID, queue }) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
return false;
}
export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerationQueueResolver<CommentEnteredModerationQueueInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE,
{
filter: (source, { storyID, queue }) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
return false;
}
// If we're filtering by queue, then only send back comments from the
// specific queue.
if (queue && source.queue !== queue) {
return false;
}
// If we're filtering by queue, then only send back comments from the
// specific queue.
if (queue && source.queue !== queue) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -17,14 +17,15 @@ export type CommentFeaturedSubscription = SubscriptionType<
CommentFeaturedInput
>;
export const commentFeatured: SubscriptionToCommentFeaturedResolver<
CommentFeaturedInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_FEATURED, {
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
export const commentFeatured: SubscriptionToCommentFeaturedResolver<CommentFeaturedInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_FEATURED,
{
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -21,22 +21,23 @@ export type CommentLeftModerationQueueSubscription = SubscriptionType<
CommentLeftModerationQueueInput
>;
export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueueResolver<
CommentLeftModerationQueueInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_LEFT_MODERATION_QUEUE, {
filter: (source, { storyID, queue }) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
return false;
}
export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueueResolver<CommentLeftModerationQueueInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_LEFT_MODERATION_QUEUE,
{
filter: (source, { storyID, queue }) => {
// If we're filtering by storyID, then only send back comments with the
// specific storyID.
if (storyID && source.storyID !== storyID) {
return false;
}
// If we're filtering by queue, then only send back comments from the
// specific queue.
if (queue && source.queue !== queue) {
return false;
}
// If we're filtering by queue, then only send back comments from the
// specific queue.
if (queue && source.queue !== queue) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -17,14 +17,15 @@ export type CommentReleasedSubscription = SubscriptionType<
CommentReleasedInput
>;
export const commentReleased: SubscriptionToCommentReleasedResolver<
CommentReleasedInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_RELEASED, {
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
export const commentReleased: SubscriptionToCommentReleasedResolver<CommentReleasedInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_RELEASED,
{
filter: (source, { storyID }) => {
if (source.storyID !== storyID) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -17,14 +17,15 @@ export type CommentReplyCreatedSubscription = SubscriptionType<
CommentReplyCreatedInput
>;
export const commentReplyCreated: SubscriptionToCommentReplyCreatedResolver<
CommentReplyCreatedInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_REPLY_CREATED, {
filter: (source, { ancestorID }) => {
if (!source.ancestorIDs.includes(ancestorID)) {
return false;
}
export const commentReplyCreated: SubscriptionToCommentReplyCreatedResolver<CommentReplyCreatedInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_REPLY_CREATED,
{
filter: (source, { ancestorID }) => {
if (!source.ancestorIDs.includes(ancestorID)) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -23,16 +23,17 @@ export type CommentStatusUpdatedSubscription = SubscriptionType<
CommentStatusUpdatedInput
>;
export const commentStatusUpdated: SubscriptionToCommentStatusUpdatedResolver<
CommentStatusUpdatedInput
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_STATUS_UPDATED, {
filter: (source, { id }) => {
// If we're filtering by id, then only send back updates for the specified
// comment.
if (id && source.commentID !== id) {
return false;
}
export const commentStatusUpdated: SubscriptionToCommentStatusUpdatedResolver<CommentStatusUpdatedInput> = createIterator(
SUBSCRIPTION_CHANNELS.COMMENT_STATUS_UPDATED,
{
filter: (source, { id }) => {
// If we're filtering by id, then only send back updates for the specified
// comment.
if (id && source.commentID !== id) {
return false;
}
return true;
},
});
return true;
},
}
);
@@ -102,6 +102,6 @@ export function createIterator<
createTenantAsyncIterator(channel),
createFilterFn(filter)
),
resolve: payload => payload,
resolve: (payload) => payload,
};
}
@@ -20,9 +20,7 @@ export const Subscription: GQLSubscriptionTypeResolver = {
export { CommentFeaturedInput } from "./commentFeatured";
export { CommentCreatedInput } from "./commentCreated";
export {
CommentEnteredModerationQueueInput,
} from "./commentEnteredModerationQueue";
export { CommentEnteredModerationQueueInput } from "./commentEnteredModerationQueue";
export { CommentLeftModerationQueueInput } from "./commentLeftModerationQueue";
export { CommentReleasedInput } from "./commentReleased";
export { CommentReplyCreatedInput } from "./commentReplyCreated";
@@ -5,11 +5,11 @@ export type SuspensionStatusInput = user.ConsolidatedSuspensionStatus & {
userID: string;
};
export const SuspensionStatus: Required<
GQLSuspensionStatusTypeResolver<SuspensionStatusInput>
> = {
export const SuspensionStatus: Required<GQLSuspensionStatusTypeResolver<
SuspensionStatusInput
>> = {
active: ({ active }) => active,
until: ({ until }) => until,
history: ({ history, userID }) =>
history.map(status => ({ ...status, userID })),
history.map((status) => ({ ...status, userID })),
};
@@ -1,9 +1,9 @@
import { GQLSuspensionStatusHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
export const SuspensionStatusHistory: Required<
GQLSuspensionStatusHistoryTypeResolver<user.SuspensionStatusHistory>
> = {
export const SuspensionStatusHistory: Required<GQLSuspensionStatusHistoryTypeResolver<
user.SuspensionStatusHistory
>> = {
active: ({ from }, input, ctx) =>
from.start <= ctx.now && from.finish > ctx.now,
from: ({ from }) => from,
@@ -13,9 +13,9 @@ export type UserStatusInput = user.UserStatus & {
userID: string;
};
export const UserStatus: Required<
GQLUserStatusTypeResolver<UserStatusInput>
> = {
export const UserStatus: Required<GQLUserStatusTypeResolver<
UserStatusInput
>> = {
current: (status, input, ctx) => {
const consolidatedStatus = user.consolidateUserStatus(status, ctx.now);
const statuses: GQLUSER_STATUS[] = [];
@@ -1,9 +1,9 @@
import { GQLUsernameHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types";
import * as user from "coral-server/models/user";
export const UsernameHistory: Required<
GQLUsernameHistoryTypeResolver<user.UsernameHistory>
> = {
export const UsernameHistory: Required<GQLUsernameHistoryTypeResolver<
user.UsernameHistory
>> = {
createdBy: ({ createdBy }, input, ctx) => {
if (createdBy) {
return ctx.loaders.Users.user.load(createdBy);
@@ -5,9 +5,9 @@ export type UsernameStatusInput = user.ConsolidatedUsernameStatus & {
userID: string;
};
export const UsernameStatus: Required<
GQLUsernameStatusTypeResolver<UsernameStatusInput>
> = {
export const UsernameStatus: Required<GQLUsernameStatusTypeResolver<
UsernameStatusInput
>> = {
history: ({ history, userID }) =>
history.map(status => ({ ...status, userID })),
history.map((status) => ({ ...status, userID })),
};
@@ -2,9 +2,7 @@ import * as tenant from "coral-server/models/tenant";
import { GQLWebhookEndpointTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const WebhookEndpoint: GQLWebhookEndpointTypeResolver<
tenant.Endpoint
> = {
export const WebhookEndpoint: GQLWebhookEndpointTypeResolver<tenant.Endpoint> = {
signingSecret: ({ signingSecrets }) =>
signingSecrets[signingSecrets.length - 1],
};
+77 -44
View File
@@ -6,86 +6,119 @@ import Cursor from "./cursor";
describe("parseLiteral", () => {
it("parses a date from a string", () => {
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "2018-07-16T18:34:26.744Z",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "2018-07-16T18:34:26.744Z",
},
null
)
).toBeInstanceOf(Date);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "this-should-fail",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "this-should-fail",
},
null
)
).toEqual(null);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "",
},
null
)
).toEqual(null);
});
it("parses a number from a string", () => {
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "20",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "20",
},
null
)
).toEqual(20);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "0",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "0",
},
null
)
).toEqual(0);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "null",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "null",
},
null
)
).toEqual(null);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "0",
})
Cursor.parseLiteral(
{
kind: Kind.STRING,
value: "0",
},
null
)
).toEqual(0);
});
it("parses a number from a number", () => {
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "20",
})
Cursor.parseLiteral(
{
kind: Kind.INT,
value: "20",
},
null
)
).toEqual(20);
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "0",
})
Cursor.parseLiteral(
{
kind: Kind.INT,
value: "0",
},
null
)
).toEqual(0);
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "",
})
Cursor.parseLiteral(
{
kind: Kind.INT,
value: "",
},
null
)
).toEqual(null);
});
it("does not parse unknown kinds", () => {
expect(
Cursor.parseLiteral({
kind: Kind.FLOAT,
value: "0.0",
})
Cursor.parseLiteral(
{
kind: Kind.FLOAT,
value: "0.0",
},
null
)
).toEqual(null);
});
});
+21 -12
View File
@@ -5,27 +5,36 @@ import Locale from "./locale";
describe("parseLiteral", () => {
it("parses a valid locale from a string", () => {
expect(
Locale.parseLiteral({
kind: Kind.STRING,
value: "en-US",
})
Locale.parseLiteral(
{
kind: Kind.STRING,
value: "en-US",
},
null
)
).toBe("en-US");
});
it("parses an unsupported locale from a string", () => {
expect(() =>
Locale.parseLiteral({
kind: Kind.STRING,
value: "xyz",
})
Locale.parseLiteral(
{
kind: Kind.STRING,
value: "xyz",
},
null
)
).toThrow();
});
it("throws when not a string", () => {
expect(() =>
Locale.parseLiteral({
kind: Kind.INT,
value: "4",
})
Locale.parseLiteral(
{
kind: Kind.INT,
value: "4",
},
null
)
).toThrow();
});
});
@@ -15,7 +15,7 @@ import {
} from "subscriptions-transport-ws";
import { ACCESS_TOKEN_PARAM, CLIENT_ID_PARAM } from "coral-common/constants";
import { Omit, RequireProperty } from "coral-common/types";
import { RequireProperty } from "coral-common/types";
import { AppOptions } from "coral-server/app";
import { getHostname } from "coral-server/app/helpers/hostname";
import {
@@ -195,7 +195,7 @@ export function formatResponse(
if (value.errors && value.errors.length > 0) {
return {
...value,
errors: value.errors.map(err => {
errors: value.errors.map((err) => {
const enriched = enrichError(context, err);
// Log the error out.

Some files were not shown because too many files have changed in this diff Show More