[CORL-678] Transition to eslint (#2634)

* chore: setup eslint

* chore: tslint checks with types & check for import order

* chore: complete eslint transition

* fix: tests

* fix: linting after rebase, faster lint for lint-staged

* chore: remove line

* fix: lint rules

* feat: add a11y linter and fix errors

* fix: tests
This commit is contained in:
Vinh
2019-10-15 22:56:38 +00:00
committed by Wyatt Johnson
parent b0e0ba6633
commit 3bfcc509d2
569 changed files with 2592 additions and 1925 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ export function getHostname(req: IncomingMessage) {
}
// IPv6 literal support
const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0;
const offset = host.startsWith("[") ? host.indexOf("]") + 1 : 0;
const index = host.indexOf(":", offset);
return index !== -1 ? host.substring(0, index) : host;
+1 -1
View File
@@ -104,7 +104,7 @@ export const listenAndServe = (
app: Express,
port: number
): Promise<http.Server> =>
new Promise(async resolve => {
new Promise(resolve => {
// Listen on the designated port.
const httpServer = app.listen(port, () => resolve(httpServer));
});
+1 -1
View File
@@ -20,8 +20,8 @@ const wrapError = (err: Error) =>
* API response.
*
* @param err the CoralError that should be serialized
* @param req the request
* @param bundles the translation bundles
* @param tenant the optional tenant to use when selecting the language
*/
const serializeError = (err: CoralError, req: Request, bundles?: I18n) => {
// Get the translation bundle.
@@ -201,8 +201,8 @@ export async function handleOAuth2Callback(
// Send back the details!
res.redirect(path + `#accessToken=${token}`);
} catch (err) {
res.redirect(path + `#error=${encodeURIComponent(err.message)}`);
} catch (e) {
res.redirect(path + `#error=${encodeURIComponent(e.message)}`);
}
}
@@ -227,8 +227,8 @@ export const wrapOAuth2Authn = (
async (err: Error | null, user: User | null) => {
try {
await handleOAuth2Callback(err, user, signingConfig, req, res);
} catch (err) {
return next(err);
} catch (e) {
return next(e);
}
}
)(req, res, next);
@@ -262,8 +262,8 @@ export const wrapAuthn = (
try {
// Pass the login off to be signed.
await handleSuccessfulLogin(user, signingConfig, req, res, next);
} catch (err) {
return next(err);
} catch (e) {
return next(e);
}
}
)(req, res, next);
@@ -254,8 +254,8 @@ export function findOrCreateOIDCUserWithToken(
now
);
return resolve(user);
} catch (err) {
return reject(err);
} catch (e) {
return reject(e);
}
}
);
+1 -1
View File
@@ -1,4 +1,4 @@
// tslint:disable:max-classes-per-file
/* eslint-disable max-classes-per-file */
import { Redis } from "ioredis";
import ms from "ms";
+1 -1
View File
@@ -22,7 +22,7 @@ function wrapPath(
{ passport }: Pick<RouterOptions, "passport">,
router: express.Router,
strategy: string,
path: string = `/${strategy}`
path = `/${strategy}`
) {
const handler = wrapOAuth2Authn(passport, app.signingConfig, strategy);
+3 -6
View File
@@ -3,7 +3,7 @@ import { Tenant } from "coral-server/models/tenant";
import { Request } from "coral-server/types/express";
import { URL } from "url";
export function reconstructURL(req: Request, path: string = "/"): string {
export function reconstructURL(req: Request, path = "/"): string {
const scheme = req.secure ? "https" : "http";
const host = req.get("host");
const base = `${scheme}://${host}`;
@@ -19,7 +19,7 @@ export function reconstructURL(req: Request, path: string = "/"): string {
export function constructTenantURL(
config: Config,
tenant: Pick<Tenant, "domain">,
path: string = "/"
path = "/"
): string {
let url: URL = new URL(path, `https://${tenant.domain}`);
if (config.get("env") === "development") {
@@ -60,10 +60,7 @@ export function getOrigin(url: string) {
export function prefixSchemeIfRequired(secure: boolean, url: string) {
if (doesRequireSchemePrefixing(url)) {
return (
"http" +
(secure ? "s" : "") +
(url.indexOf("//") === -1 ? "://" : ":") +
url
"http" + (secure ? "s" : "") + (!url.includes("//") ? "://" : ":") + url
);
}
+1
View File
@@ -41,6 +41,7 @@ const deleteScheduledAccounts: ScheduledJobCommand<Options> = async ({
for await (const tenant of tenantCache) {
log = log.child({ tenantID: tenant.id }, true);
// eslint-disable-next-line no-constant-condition
while (true) {
const now = new Date();
const user = await retrieveUserScheduledForDeletion(
+1 -1
View File
@@ -1,4 +1,4 @@
// tslint:disable:max-classes-per-file
/* eslint-disable max-classes-per-file */
import { FluentBundle } from "fluent/compat";
import { MongoError } from "mongodb";
+2 -1
View File
@@ -17,7 +17,7 @@ export default new GraphQLScalarType({
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.STRING: {
// This handles an empty string.
if (ast.value && ast.value.length === 0) {
return null;
@@ -29,6 +29,7 @@ export default new GraphQLScalarType({
}
return date.toJSDate();
}
default:
return null;
}
@@ -12,6 +12,7 @@ export type Publisher = (input: SUBSCRIPTION_INPUT) => Promise<void>;
* over the pubsub broker to facilitate live updates and notifications.
*
* @param pubsub the pubsub broker to be used to facilitate the publish action
* @param notifier
* @param tenantID the ID of the Tenant where the event will be published with
* @param clientID the ID of the client to de-duplicate mutation responses
*/
@@ -154,13 +154,14 @@ export function onConnect(options: OnConnectOptions): OnConnectFn {
}
if (!(err instanceof CoralError)) {
// eslint-disable-next-line no-ex-assign
err = new InternalError(err, "could not setup websocket connection");
}
const { message } = err.serializeExtensions(
options.i18n.getDefaultBundle()
);
throw { message };
throw new Error(message);
}
};
}
+2 -2
View File
@@ -89,10 +89,10 @@ class Server {
private tenantCache: TenantCache;
// connected when true, indicates that `connect()` was already called.
private connected: boolean = false;
private connected = false;
// processing when true, indicates that `process()` was already called.
private processing: boolean = false;
private processing = false;
// i18n is the server reference to the i18n framework.
private i18n: I18n;
@@ -25,7 +25,7 @@ export class SecretStream extends Transform {
this.push(SecretStream.replace(JSON.stringify(chunk)));
}
} catch (err) {
// tslint:disable-next-line:no-console
// eslint-disable-next-line no-console
console.error(err);
}
+19 -19
View File
@@ -275,6 +275,20 @@ export async function createActions(
export type CommentActionConnectionInput = ConnectionInput<CommentAction>;
async function retrieveConnection(
input: CommentActionConnectionInput,
query: Query<CommentAction>
): Promise<Readonly<Connection<Readonly<CommentAction>>>> {
// Apply the pagination arguments to the query.
query.orderBy({ createdAt: -1 });
if (input.after) {
query.where({ createdAt: { $lt: input.after as Date } });
}
// Return a connection.
return resolveConnection(query, input, action => action.createdAt);
}
export async function retrieveCommentActionConnection(
mongo: Db,
tenantID: string,
@@ -291,20 +305,6 @@ export async function retrieveCommentActionConnection(
return retrieveConnection(input, query);
}
async function retrieveConnection(
input: CommentActionConnectionInput,
query: Query<CommentAction>
): Promise<Readonly<Connection<Readonly<CommentAction>>>> {
// Apply the pagination arguments to the query.
query.orderBy({ createdAt: -1 });
if (input.after) {
query.where({ createdAt: { $lt: input.after as Date } });
}
// Return a connection.
return resolveConnection(query, input, action => action.createdAt);
}
export async function retrieveUserAction(
mongo: Db,
tenantID: string,
@@ -330,7 +330,7 @@ export async function retrieveManyUserActionPresence(
userID: string | null,
commentIDs: string[]
): Promise<GQLActionPresence[]> {
const cursor = await collection(mongo).find(
const cursor = collection(mongo).find(
{
tenantID,
userID,
@@ -505,10 +505,10 @@ interface DecodedActionCountKey {
* actionType and reason.
*/
function decodeActionCountKey(key: string): DecodedActionCountKey | null {
let actionType: string = "";
let reason: string = "";
let actionType = "";
let reason = "";
if (key.indexOf(ACTION_COUNT_JOIN_CHAR) >= 0) {
if (key.includes(ACTION_COUNT_JOIN_CHAR)) {
const keys = key.split(ACTION_COUNT_JOIN_CHAR);
if (keys.length !== 2) {
throw new Error(
@@ -651,7 +651,7 @@ function incrementActionCounts(
actionCounts: ActionCounts,
actionType: ACTION_TYPE,
reason: GQLCOMMENT_FLAG_REASON | undefined,
count: number = 1
count = 1
) {
switch (actionType) {
case ACTION_TYPE.REACTION:
@@ -93,7 +93,7 @@ export async function retrieveCommentModerationActions(
tenantID: string,
filter: CommentModerationActionFilter
) {
const result = await collection(mongo).find({
const result = collection(mongo).find({
tenantID,
...filter,
});
+3 -3
View File
@@ -376,7 +376,7 @@ export async function retrieveManyComments(
tenantID: string,
ids: string[]
) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
id: {
$in: ids,
},
@@ -941,7 +941,7 @@ export async function retrieveStoryCommentTagCounts(
const startTime = performanceNow();
// Load the counts from the database for this particular tag query.
const cursor = await collection<{
const cursor = collection<{
_id: { tag: GQLTAG; storyID: string };
total: number;
}>(mongo).aggregate([
@@ -994,7 +994,7 @@ export async function retrieveManyRecentStatusCounts(
authorIDs: string[]
) {
// Get all the statuses for the given date stamp.
const cursor = await collection<{
const cursor = collection<{
_id: {
status: GQLCOMMENT_STATUS;
authorID: string;
+1 -1
View File
@@ -84,7 +84,7 @@ export default class Query<T> {
"executing query"
);
let cursor = await this.collection.find(this.filter);
let cursor = this.collection.find(this.filter);
if (this.limit) {
// Apply a limit if it exists.
@@ -89,7 +89,7 @@ export async function failMigration(mongo: Db, id: number, now = new Date()) {
}
export async function retrieveAllMigrationRecords(mongo: Db) {
const cursor = await collection(mongo)
const cursor = collection(mongo)
.find({})
.sort({ id: 1 });
return cursor.toArray();
+1 -1
View File
@@ -61,7 +61,7 @@ export async function primeQueries(
}
export async function getQueries(mongo: Db, ids: string[]) {
const cursor = await collection(mongo).find({ id: { $in: ids } });
const cursor = collection(mongo).find({ id: { $in: ids } });
const queries = await cursor.toArray();
return ids.map(id => queries.find(query => query.id === id) || null);
}
@@ -66,7 +66,7 @@ export async function recalculateSharedModerationQueueQueueCounts(
await redis.del(key, freshKey);
// Fetch all the moderation queue counts.
const queueResults = await collection<{
const queueResults = collection<{
_id: string;
total: number;
}>(mongo).aggregate([
@@ -142,7 +142,7 @@ export async function recalculateSharedModerationQueueTotalCounts(
await redis.del(key, freshKey);
// Fetch all the totals for the moderation queues.
const totalResults = await collection<{
const totalResults = collection<{
total: number;
}>(mongo).aggregate([
{
@@ -197,7 +197,7 @@ export async function recalculateSharedStatusCommentCounts(
await redis.del(key, freshKey);
// Fetch all the comments of each status.
const statusResults = await collection<{
const statusResults = collection<{
_id: string;
total: number;
}>(mongo).aggregate([
@@ -271,7 +271,7 @@ export async function recalculateSharedActionCommentCounts(
await redis.del(key, freshKey);
// Fetch all the comments of each status.
const actionResults = await collection<{
const actionResults = collection<{
_id: string;
total: number;
}>(mongo).aggregate([
+2 -2
View File
@@ -258,7 +258,7 @@ export async function retrieveManyStories(
tenantID: string,
ids: string[]
) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
id: { $in: ids },
tenantID,
});
@@ -273,7 +273,7 @@ export async function retrieveManyStoriesByURL(
tenantID: string,
urls: string[]
) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
url: { $in: urls },
tenantID,
});
+2 -2
View File
@@ -212,7 +212,7 @@ export async function retrieveTenant(mongo: Db, id: string) {
}
export async function retrieveManyTenants(mongo: Db, ids: string[]) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
id: {
$in: ids,
},
@@ -227,7 +227,7 @@ export async function retrieveManyTenantsByDomain(
mongo: Db,
domains: string[]
) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
domain: {
$in: domains,
},
+7 -3
View File
@@ -523,11 +523,12 @@ async function findOrCreateUserInput(
// Mutate the profiles to ensure we mask handle any secrets.
switch (profile.type) {
case "local":
case "local": {
// Hash the user's password with bcrypt.
const password = await hashPassword(profile.password);
defaults.profiles.push({ ...profile, password });
break;
}
default:
// Push the profile onto the User.
defaults.profiles.push(profile);
@@ -626,7 +627,7 @@ export async function retrieveManyUsers(
tenantID: string,
ids: string[]
) {
const cursor = await collection(mongo).find({
const cursor = collection(mongo).find({
tenantID,
id: {
$in: ids,
@@ -1098,7 +1099,7 @@ export async function updateUserEmail(
return result.value;
} catch (err) {
if (err instanceof MongoError && err.code === 11000) {
throw new DuplicateEmailError(email!);
throw new DuplicateEmailError(email);
}
throw err;
}
@@ -1420,6 +1421,7 @@ export async function premodUser(
/**
* removeUserPremod will lift a user premod requirement
*
* @param mongo the mongo database handle
* @param tenantID the Tenant's ID where the User exists
* @param id the ID of the user having their ban lifted
@@ -2354,6 +2356,7 @@ export async function retrieveUserScheduledForDeletion(
/**
* createModeratorNote will add a note to a users account
*
* @param mongo the database to put the notification digests into
* @param tenantID the ID of the Tenant that this User exists on
* @param id the ID of the User who is the subject of the note
@@ -2397,6 +2400,7 @@ export async function createModeratorNote(
/**
* deleteModeratorNote will remove a note from a user profile
*
* @param mongo the database to put the notification digests into
* @param tenantID the ID of the Tenant that this User exists on
* @param userID the ID of the user
@@ -256,8 +256,8 @@ export const createJobProcessor = (options: MailProcessorOptions) => {
fromAddress,
data
);
} catch (err) {
throw new InternalError(err, "could not translate the message");
} catch (e) {
throw new InternalError(e, "could not translate the message");
}
// Compute the end time.
@@ -285,8 +285,8 @@ export const createJobProcessor = (options: MailProcessorOptions) => {
// Create the transport based on the smtp uri.
transport = createTransport(opts);
} catch (err) {
throw new InternalError(err, "could not create email transport");
} catch (e) {
throw new InternalError(e, "could not create email transport");
}
// Set the transport back into the cache.
@@ -304,8 +304,8 @@ export const createJobProcessor = (options: MailProcessorOptions) => {
try {
// Send the mail message.
await transport.sendMail(message);
} catch (err) {
throw new InternalError(err, "could not send email");
} catch (e) {
throw new InternalError(e, "could not send email");
}
// Compute the end time.
@@ -16,11 +16,6 @@ import TenantCache from "coral-server/services/tenant/cache";
import { createJobProcessor, JOB_NAME, NotifierData } from "./processor";
export const createNotifierTask = (
queue: Queue.QueueOptions,
options: Options
) => new NotifierQueue(queue, options);
interface Options {
mongo: Db;
mailerQueue: MailerQueue;
@@ -69,3 +64,8 @@ export class NotifierQueue {
return this.task.process();
}
}
export const createNotifierTask = (
queue: Queue.QueueOptions,
options: Options
) => new NotifierQueue(queue, options);
+1 -1
View File
@@ -161,7 +161,7 @@ export async function removeCommentAction(
if (wasRemoved) {
// Compute the action counts, and invert them (because we're deleting an
// action).
const actionCounts = invertEncodedActionCounts(encodeActionCounts(action!));
const actionCounts = invertEncodedActionCounts(encodeActionCounts(action));
// Update the comment action counts here.
const updatedComment = await updateCommentActionCounts(
+18 -18
View File
@@ -258,23 +258,6 @@ export async function signString<T extends {}>(
return jwt.sign(payload, secret, { ...options, algorithm });
}
/**
* extractJWTFromRequest will extract the token from the request if it can find
* it. It first tries to get the token from the headers, then from the cookie.
*
* @param req the request to extract the JWT from
* @param excludeQuery when true, does not pull from the query params
*/
export function extractTokenFromRequest(
req: Request | IncomingMessage,
excludeQuery: boolean = false
): string | null {
return (
extractJWTFromRequestHeaders(req, excludeQuery) ||
extractJWTFromRequestCookie(req)
);
}
/**
* COOKIE_NAME is the name of the authorization cookie used by Coral.
*/
@@ -330,7 +313,7 @@ function extractJWTFromRequestCookie(
*/
function extractJWTFromRequestHeaders(
req: Request | IncomingMessage,
excludeQuery: boolean = false
excludeQuery = false
) {
const options: BearerOptions = {
basic: "password",
@@ -345,6 +328,23 @@ function extractJWTFromRequestHeaders(
return permit.check(req) || null;
}
/**
* extractJWTFromRequest will extract the token from the request if it can find
* it. It first tries to get the token from the headers, then from the cookie.
*
* @param req the request to extract the JWT from
* @param excludeQuery when true, does not pull from the query params
*/
export function extractTokenFromRequest(
req: Request | IncomingMessage,
excludeQuery = false
): string | null {
return (
extractJWTFromRequestHeaders(req, excludeQuery) ||
extractJWTFromRequestCookie(req)
);
}
function generateJTIRevokedKey(jti: string) {
// jtir: JTI Revoked namespace.
return `jtir:${jti}`;
+1 -1
View File
@@ -1,4 +1,4 @@
// tslint:disable: max-classes-per-file
/* eslint-disable max-classes-per-file */
import VError from "verror";
+1 -1
View File
@@ -8,7 +8,7 @@ import logger from "coral-server/logger";
type IndexType = 1 | -1 | "text";
export type IndexSpecification<T> = {
[P in keyof Writable<Partial<T>>]: IndexType
[P in keyof Writable<Partial<T>>]: IndexType;
} &
Record<string, IndexType>;
+3 -2
View File
@@ -33,7 +33,7 @@ export default class Manager {
private clientID: string;
private migrations: Migration[];
private tenantCache: TenantCache;
private ran: boolean = false;
private ran = false;
constructor({ tenantCache, i18n }: ManagerOptions) {
this.clientID = uuid.v4();
@@ -58,10 +58,11 @@ export default class Manager {
// Load the migration.
const filePath = path.join(__dirname, "migrations", fileName);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const m = require(filePath);
// Parse the timestamp out of the migration filename.
const matches = fileName.match(fileNamePattern);
const matches = fileNamePattern.exec(fileName);
if (!matches || matches.length !== 3) {
throw new Error("fileName format is invalid");
}
@@ -28,7 +28,7 @@ export default class extends Migration {
public async test(mongo: Db, tenantID: string) {
// Find all the users that still have premod status unset.
const cursor = await collections
const cursor = collections
.users(mongo)
.find({
"status.premod": null,
+1 -4
View File
@@ -37,10 +37,7 @@ function attachHandlers(redis: Redis) {
});
}
export function createRedisClient(
config: Config,
lazyConnect: boolean = false
): Redis {
export function createRedisClient(config: Config, lazyConnect = false): Redis {
try {
const options = config.get("redis_options") || {};
+1 -1
View File
@@ -98,7 +98,7 @@ export async function remove(
mongo: Db,
tenant: Tenant,
storyID: string,
includeComments: boolean = false
includeComments = false
) {
// Create a logger for this function.
const log = logger.child(
+1 -1
View File
@@ -48,7 +48,7 @@ export default class TenantCache {
/**
* primed is true when the cache has already been fully primed.
*/
private primed: boolean = false;
private primed = false;
/**
* Create a new client application ID. This prevents duplicated messages
+8 -8
View File
@@ -73,6 +73,14 @@ export async function update(
return updatedTenant;
}
/**
* isInstalled will return a promise that if true, indicates that a Tenant has
* been installed.
*/
export async function isInstalled(cache: TenantCache) {
return (await cache.count()) > 0;
}
export type InstallTenant = CreateTenantInput;
export async function install(
@@ -108,14 +116,6 @@ export async function canInstall(cache: TenantCache) {
return (await cache.count()) === 0;
}
/**
* isInstalled will return a promise that if true, indicates that a Tenant has
* been installed.
*/
export async function isInstalled(cache: TenantCache) {
return (await cache.count()) > 0;
}
/**
* regenerateSSOKey will regenerate the Single Sign-On key for the specified
* Tenant and notify all other Tenant's connected that the Tenant was updated.
+1 -1
View File
@@ -25,7 +25,7 @@ export function isURLPermitted(
export function isURLPermitted(
tenant: Pick<Tenant, "allowedDomains" | "domain">,
targetURL: string,
includeTenantDomain: boolean = false
includeTenantDomain = false
) {
// If there aren't any domains, then we reject it, because no url we have can
// satisfy those requirements.
+4 -1
View File
@@ -371,7 +371,7 @@ export async function requestAccountDeletion(
const updatedUser = await scheduleDeletionDate(
mongo,
tenant.id!,
tenant.id,
user.id,
deletionDate.toJSDate()
);
@@ -619,6 +619,7 @@ export async function updateRole(
/**
* enabledAuthenticationIntegrations returns enabled auth integrations for a tenant
*
* @param tenant Tenant where the User will be interacted with
* @param target whether to filter by stream or admin enabled. defaults to requiring both.
*/
@@ -639,6 +640,7 @@ function enabledAuthenticationIntegrations(
/**
* canUpdateLocalProfile will determine if a user is permitted to update their email address.
*
* @param tenant Tenant where the User will be interacted with
* @param user the User that we are updating
*/
@@ -662,6 +664,7 @@ function canUpdateLocalProfile(tenant: Tenant, user: User): boolean {
/**
* updateEmail will update the current User's email address.
*
* @param mongo mongo database to interact with
* @param tenant Tenant where the User will be interacted with
* @param mailer The mailer queue