mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
[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:
@@ -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<
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import Joi from "joi";
|
||||
import Joi from "@hapi/joi";
|
||||
|
||||
import { validate } from "coral-server/app/request/body";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import uuid from "uuid/v4";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import logger from "coral-server/logger";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -8,5 +8,5 @@ export interface QueueInput {
|
||||
}
|
||||
|
||||
export const Queue: Required<GQLQueueTypeResolver<QueueInput>> = {
|
||||
counts: t => t.counts(),
|
||||
counts: (t) => t.counts(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user