chore: Remove suppressImplicitAnyIndexErrors TS rule (#7760)

This commit is contained in:
Tom Moor
2024-10-11 15:46:46 -04:00
committed by GitHub
parent 0f8ac54bcb
commit 9680e57849
70 changed files with 255 additions and 156 deletions

View File

@ -21,11 +21,13 @@ function ConfirmMoveDialog({ collection, item, ...rest }: Props) {
const { documents, dialogs, collections } = useStores();
const { t } = useTranslation();
const prevCollection = collections.get(item.collectionId!);
const accessMapping = {
[CollectionPermission.ReadWrite]: t("view and edit access"),
[CollectionPermission.Read]: t("view only access"),
null: t("no access"),
};
const accessMapping: Record<Partial<CollectionPermission> | "null", string> =
{
[CollectionPermission.Admin]: t("manage access"),
[CollectionPermission.ReadWrite]: t("view and edit access"),
[CollectionPermission.Read]: t("view only access"),
null: t("no access"),
};
const handleSubmit = async () => {
await documents.move({

View File

@ -35,7 +35,7 @@ function ConnectionStatus() {
};
const message = ui.multiplayerErrorCode
? codeToMessage[ui.multiplayerErrorCode]
? codeToMessage[ui.multiplayerErrorCode as keyof typeof codeToMessage]
: undefined;
return ui.multiplayerStatus === "connecting" ||

View File

@ -39,12 +39,15 @@ const LocaleTime: React.FC<Props> = ({
relative,
tooltipDelay,
}: Props) => {
const userLocale: string = useUserLocale() || "";
const dateFormatLong = {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
en_US: "MMMM do, yyyy h:mm a",
fr_FR: "'Le 'd MMMM yyyy 'à' H:mm",
};
const formatLocaleLong = dateFormatLong[userLocale] ?? "MMMM do, yyyy h:mm a";
const formatLocaleLong =
(userLocale ? dateFormatLong[userLocale] : undefined) ??
"MMMM do, yyyy h:mm a";
// @ts-expect-error fallback to formatLocaleLong
const formatLocale = format?.[userLocale] ?? formatLocaleLong;
const [_, setMinutesMounted] = React.useState(0); // eslint-disable-line @typescript-eslint/no-unused-vars
const callback = React.useRef<() => void>();

View File

@ -19,7 +19,8 @@ export interface PaginatedItem {
}
type Props<T> = WithTranslation &
RootStore & {
RootStore &
React.HTMLAttributes<HTMLDivElement> & {
fetch?: (
options: Record<string, any> | undefined
) => Promise<T[] | undefined> | undefined;

View File

@ -29,6 +29,7 @@ const EmojiMenu = (props: Props) => {
.map((item) => {
// We snake_case the shortcode for backwards compatability with gemoji to
// avoid multiple formats being written into documents.
// @ts-expect-error emojiMartToGemoji key
const shortcode = snakeCase(emojiMartToGemoji[item.id] || item.id);
const emoji = item.value;

View File

@ -14,7 +14,10 @@ export default function codeMenuItems(
): MenuItem[] {
const node = state.selection.$from.node();
const allLanguages = Object.entries(LANGUAGES);
const allLanguages = Object.entries(LANGUAGES) as [
keyof typeof LANGUAGES,
string
][];
const frequentLanguages = getFrequentCodeLanguages();
const frequentLangMenuItems = frequentLanguages.map((value) => {
@ -49,6 +52,7 @@ export default function codeMenuItems(
visible: !readOnly,
name: "code_block",
icon: <ExpandedIcon />,
// @ts-expect-error We have a fallback for incorrect mapping
label: LANGUAGES[node.attrs.language ?? "none"],
children: languageMenuItems,
},

View File

@ -3,16 +3,9 @@ import useCurrentUser from "./useCurrentUser";
/**
* Returns the user's locale, or undefined if the user is not logged in.
*
* @param languageCode Whether to only return the language code
* @returns The user's locale, or undefined if the user is not logged in
*/
export default function useUserLocale(languageCode?: boolean) {
export default function useUserLocale() {
const user = useCurrentUser({ rejectOnEmpty: false });
if (!user?.language) {
return undefined;
}
const { language } = user;
return languageCode ? language.split("_")[0] : language;
return user?.language;
}

View File

@ -1,5 +1,9 @@
import { computed, observable } from "mobx";
import { FileOperationFormat, FileOperationType } from "@shared/types";
import {
FileOperationFormat,
FileOperationState,
FileOperationType,
} from "@shared/types";
import { bytesToHumanReadable } from "@shared/utils/files";
import User from "./User";
import Model from "./base/Model";
@ -10,7 +14,7 @@ class FileOperation extends Model {
id: string;
@observable
state: string;
state: FileOperationState;
name: string;

View File

@ -11,6 +11,7 @@ import {
UserRole,
} from "@shared/types";
import type { NotificationSettings } from "@shared/types";
import { locales } from "@shared/utils/date";
import { client } from "~/utils/ApiClient";
import Document from "./Document";
import Group from "./Group";
@ -39,7 +40,7 @@ class User extends ParanoidModel {
@Field
@observable
language: string;
language: keyof typeof locales;
@Field
@observable

View File

@ -40,6 +40,7 @@ export default abstract class Model {
* @returns A promise that resolves when loading is complete.
*/
async loadRelations(
this: Model,
options: { withoutPolicies?: boolean } = {}
): Promise<any> {
const relations = getRelationsForModelClass(
@ -62,7 +63,7 @@ export default abstract class Model {
if ("fetch" in store) {
const id = this[properties.idKey];
if (id) {
promises.push(store.fetch(id));
promises.push(store.fetch(id as string));
}
}
}
@ -145,6 +146,7 @@ export default abstract class Model {
if (key === "initialized") {
continue;
}
// @ts-expect-error TODO
this[key] = data[key];
} catch (error) {
Logger.warn(`Error setting ${key} on model`, error);

View File

@ -17,7 +17,7 @@ type RelationOptions<T = Model> = {
type RelationProperties<T = Model> = {
/** The name of the property on the model that stores the ID of the relation. */
idKey: string;
idKey: keyof T;
/** A function that returns the class of the relation. */
relationClassResolver: () => typeof Model;
/** Options for the relation. */

View File

@ -43,8 +43,12 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
memberships.fetchPage(options),
groupMemberships.fetchPage(options),
]);
setUsersCount(users[PAGINATION_SYMBOL].total);
setGroupsCount(groups[PAGINATION_SYMBOL].total);
if (users[PAGINATION_SYMBOL]) {
setUsersCount(users[PAGINATION_SYMBOL].total);
}
if (groups[PAGINATION_SYMBOL]) {
setGroupsCount(groups[PAGINATION_SYMBOL].total);
}
} finally {
setIsLoading(false);
}

View File

@ -71,10 +71,11 @@ function Invite({ onSubmit }: Props) {
[onSubmit, invites, role, t, users]
);
const handleChange = React.useCallback((ev, index) => {
const handleChange = React.useCallback((ev, index: number) => {
setInvites((prevInvites) => {
const newInvites = [...prevInvites];
newInvites[index][ev.target.name] = ev.target.value;
newInvites[index][ev.target.name as keyof InviteRequest] =
ev.target.value;
return newInvites;
});
}, []);

View File

@ -62,7 +62,9 @@ function Members() {
filter,
role,
});
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
if (response[PAGINATION_SYMBOL]) {
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
}
setUserIds(response.map((u: User) => u.id));
} finally {
setIsLoading(false);

View File

@ -47,7 +47,9 @@ function Shares() {
sort,
direction,
});
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
if (response[PAGINATION_SYMBOL]) {
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
}
setShareIds(response.map((u: Share) => u.id));
} finally {
setIsLoading(false);

View File

@ -38,7 +38,7 @@ const FileOperationListItem = ({ fileOperation }: Props) => {
[FileOperationState.Error]: t("Failed"),
};
const iconMapping = {
const iconMapping: Record<FileOperationState, React.JSX.Element> = {
[FileOperationState.Creating]: <Spinner />,
[FileOperationState.Uploading]: <Spinner />,
[FileOperationState.Expired]: <ArchiveIcon color={theme.textTertiary} />,
@ -46,8 +46,9 @@ const FileOperationListItem = ({ fileOperation }: Props) => {
[FileOperationState.Error]: <WarningIcon color={theme.danger} />,
};
const formatMapping = {
const formatMapping: Record<FileOperationFormat, string> = {
[FileOperationFormat.JSON]: "JSON",
[FileOperationFormat.Notion]: "Notion",
[FileOperationFormat.MarkdownZip]: "Markdown",
[FileOperationFormat.HTMLZip]: "HTML",
[FileOperationFormat.PDF]: "PDF",

View File

@ -5,7 +5,11 @@ import GroupMembership from "~/models/GroupMembership";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import RootStore from "./RootStore";
import Store, { PAGINATION_SYMBOL, RPCAction } from "./base/Store";
import Store, {
PAGINATION_SYMBOL,
PaginatedResponse,
RPCAction,
} from "./base/Store";
export default class GroupMembershipsStore extends Store<GroupMembership> {
actions = [RPCAction.Create, RPCAction.Delete];
@ -24,7 +28,7 @@ export default class GroupMembershipsStore extends Store<GroupMembership> {
documentId?: string;
collectionId?: string;
groupId?: string;
}): Promise<GroupMembership[]> => {
}): Promise<PaginatedResponse<GroupMembership>> => {
this.isFetching = true;
try {
@ -41,7 +45,7 @@ export default class GroupMembershipsStore extends Store<GroupMembership> {
: await client.post(`/groupMemberships.list`, params);
invariant(res?.data, "Data not available");
let response: GroupMembership[] = [];
let response: PaginatedResponse<GroupMembership> = [];
runInAction(`GroupMembershipsStore#fetchPage`, () => {
res.data.groups?.forEach(this.rootStore.groups.add);
res.data.documents?.forEach(this.rootStore.documents.add);

View File

@ -5,7 +5,11 @@ import Membership from "~/models/Membership";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import RootStore from "./RootStore";
import Store, { PAGINATION_SYMBOL, RPCAction } from "./base/Store";
import Store, {
PAGINATION_SYMBOL,
PaginatedResponse,
RPCAction,
} from "./base/Store";
export default class MembershipsStore extends Store<Membership> {
actions = [RPCAction.Create, RPCAction.Delete];
@ -17,14 +21,14 @@ export default class MembershipsStore extends Store<Membership> {
@action
fetchPage = async (
params: (PaginationParams & { id?: string }) | undefined
): Promise<Membership[]> => {
): Promise<PaginatedResponse<Membership>> => {
this.isFetching = true;
try {
const res = await client.post(`/collections.memberships`, params);
invariant(res?.data, "Data not available");
let response: Membership[] = [];
let response: PaginatedResponse<Membership> = [];
runInAction(`MembershipsStore#fetchPage`, () => {
res.data.users.forEach(this.rootStore.users.add);
response = res.data.memberships.map(this.add);

View File

@ -102,14 +102,11 @@ export default class RootStore {
*
* @param modelName
*/
public getStoreForModelName<K extends keyof RootStore>(
modelName: string
): RootStore[K] {
public getStoreForModelName<K extends keyof RootStore>(modelName: string) {
const storeName = this.getStoreNameForModelName(modelName);
const store = this[storeName];
invariant(store, `No store found for model name "${modelName}"`);
return store;
return store as RootStore[K];
}
/**
@ -118,8 +115,9 @@ export default class RootStore {
public clear() {
Object.getOwnPropertyNames(this)
.filter((key) => ["auth", "ui"].includes(key) === false)
.forEach((key) => {
this[key]?.clear?.();
.forEach((key: keyof RootStore) => {
// @ts-expect-error clear exists on all stores
"clear" in this[key] && this[key].clear();
});
}
@ -128,7 +126,10 @@ export default class RootStore {
*
* @param StoreClass
*/
private registerStore<T = typeof Store>(StoreClass: T, name?: string) {
private registerStore<T = typeof Store>(
StoreClass: T,
name?: keyof RootStore
) {
// @ts-expect-error TS thinks we are instantiating an abstract class.
const store = new StoreClass(this);
const storeName = name ?? this.getStoreNameForModelName(store.modelName);
@ -136,6 +137,6 @@ export default class RootStore {
}
private getStoreNameForModelName(modelName: string) {
return pluralize(lowerFirst(modelName));
return pluralize(lowerFirst(modelName)) as keyof RootStore;
}
}

View File

@ -17,7 +17,6 @@ import { LifecycleManager } from "~/models/decorators/Lifecycle";
import { getInverseRelationsForModelClass } from "~/models/decorators/Relation";
import type { PaginationParams, PartialExcept, Properties } from "~/types";
import { client } from "~/utils/ApiClient";
import Logger from "~/utils/Logger";
import { AuthorizationError, NotFoundError } from "~/utils/errors";
export enum RPCAction {
@ -29,10 +28,19 @@ export enum RPCAction {
Count = "count",
}
export type FetchPageParams = PaginationParams & Record<string, any>;
export const PAGINATION_SYMBOL = Symbol.for("pagination");
export type PaginatedResponse<T> = T[] & {
[PAGINATION_SYMBOL]?: {
total: number;
limit: number;
offset: number;
nextPath: string;
};
};
export type FetchPageParams = PaginationParams & Record<string, any>;
export default abstract class Store<T extends Model> {
@observable
data: Map<string, T> = new Map();
@ -129,6 +137,7 @@ export default abstract class Store<T extends Model> {
if (deleteBehavior === "cascade") {
store.remove(item.id);
} else if (deleteBehavior === "null") {
// @ts-expect-error TODO
item[relation.idKey] = null;
}
});
@ -166,6 +175,7 @@ export default abstract class Store<T extends Model> {
if (archiveBehavior === "cascade") {
store.addToArchive(item);
} else if (archiveBehavior === "null") {
// @ts-expect-error TODO
item[relation.idKey] = null;
}
});
@ -316,7 +326,9 @@ export default abstract class Store<T extends Model> {
}
@action
fetchPage = async (params?: FetchPageParams | undefined): Promise<T[]> => {
fetchPage = async (
params?: FetchPageParams | undefined
): Promise<PaginatedResponse<T>> => {
if (!this.actions.includes(RPCAction.List)) {
throw new Error(`Cannot list ${this.modelName}`);
}
@ -327,7 +339,7 @@ export default abstract class Store<T extends Model> {
const res = await client.post(`/${this.apiEndpoint}.list`, params);
invariant(res?.data, "Data not available");
let response: T[] = [];
let response: PaginatedResponse<T> = [];
runInAction(`list#${this.modelName}`, () => {
this.addPolicies(res.policies);
@ -343,15 +355,16 @@ export default abstract class Store<T extends Model> {
};
@action
fetchAll = async (params?: Record<string, any>): Promise<T[]> => {
fetchAll = async (
params?: Record<string, any>
): Promise<PaginatedResponse<T>> => {
const limit = params?.limit ?? Pagination.defaultLimit;
const response = await this.fetchPage({ ...params, limit });
if (!response[PAGINATION_SYMBOL]) {
Logger.warn("Pagination information not available in response", {
params,
});
}
invariant(
response[PAGINATION_SYMBOL],
"Pagination information not available in response"
);
const pages = Math.ceil(response[PAGINATION_SYMBOL].total / limit);
const fetchPages = [];

View File

@ -17,13 +17,14 @@ import {
getCurrentTimeAsString,
unicodeCLDRtoBCP47,
dateLocale,
locales,
} from "@shared/utils/date";
import User from "~/models/User";
export function dateToHeading(
dateTime: string,
t: TFunction,
userLocale: string | null | undefined
userLocale: keyof typeof locales | undefined
) {
const date = Date.parse(dateTime);
const now = new Date();
@ -84,7 +85,7 @@ export function dateToHeading(
export function dateToExpiry(
dateTime: string,
t: TFunction,
userLocale: string | null | undefined
userLocale: keyof typeof locales | null | undefined
) {
const date = Date.parse(dateTime);
const now = new Date();

View File

@ -33,7 +33,9 @@ export default function download(
// reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
// @ts-expect-error this is weird code
x = [x, m];
// @ts-expect-error this is weird code
m = x[0];
// @ts-expect-error this is weird code
x = x[1];
}

View File

@ -1,5 +1,5 @@
import { i18n } from "i18next";
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
import { locales, unicodeCLDRtoBCP47 } from "@shared/utils/date";
import Desktop from "./Desktop";
/**
@ -25,7 +25,7 @@ export function formatNumber(number: number, locale: string) {
export function detectLanguage() {
const [ln, r] = navigator.language.split("-");
const region = (r || ln).toUpperCase();
return `${ln}_${region}`;
return `${ln}_${region}` as keyof typeof locales;
}
/**

View File

@ -19,5 +19,5 @@ export function getVisibilityListener(): string {
}
export function getPageVisible(): boolean {
return !document[hidden];
return !document[hidden as keyof Document];
}

View File

@ -40,7 +40,7 @@ function SlackListItem({ integration, collection }: Props) {
toast.success(t("Settings saved"));
};
const mapping = {
const mapping: Record<string, string> = {
"documents.publish": t("document published"),
"documents.update": t("document updated"),
};

View File

@ -79,7 +79,10 @@ router.post(
return;
}
// get content for unfurled links
const unfurls = {};
const unfurls: Record<
string,
{ title: string; text: string; color?: string | undefined }
> = {};
for (const link of event.links) {
const documentId = parseDocumentSlug(link.url);
@ -109,7 +112,7 @@ router.post(
unfurls[link.url] = {
title: doc.title,
text: doc.getSummary(),
color: doc.collection?.color,
color: doc.collection?.color ?? undefined,
};
}
}

View File

@ -74,7 +74,7 @@ export class SlackUtils {
redirectUri = SlackUtils.callbackUrl()
): string {
const baseUrl = SlackUtils.authBaseUrl;
const params = {
const params: Record<string, string> = {
client_id: env.SLACK_CLIENT_ID,
scope: scopes ? scopes.join(" ") : "",
redirect_uri: redirectUri,

View File

@ -85,7 +85,10 @@ describe("DeliverWebhookTask", () => {
event,
});
const headers = fetchMock.mock.calls[0]![1]!.headers!;
const headers = fetchMock.mock.calls[0]![1]!.headers! as Record<
string,
string
>;
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(headers["Outline-Signature"]).toMatch(/^t=[0-9]+,s=[a-z0-9]+$/);

View File

@ -692,7 +692,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
"user-agent": `Outline-Webhooks${
env.VERSION ? `/${env.VERSION.slice(0, 7)}` : ""
}`,
};
} as Record<string, string>;
const signature = subscription.signature(JSON.stringify(requestBody));
if (signature) {

View File

@ -2,9 +2,9 @@ import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs";
import BaseEmail from "./BaseEmail";
const emails = {};
const emails: Record<string, typeof BaseEmail> = {};
requireDirectory<{ default: BaseEmail<any> }>(__dirname).forEach(
requireDirectory<{ default: typeof BaseEmail }>(__dirname).forEach(
([module, id]) => {
if (id === "index") {
return;

View File

@ -127,8 +127,8 @@ async function start(_id: number, disconnect: () => void) {
}
Logger.info("lifecycle", `Starting ${name} service`);
const init = services[name];
await init(app, server, env.SERVICES);
const init = services[name as keyof typeof services];
init(app, server as https.Server, env.SERVICES);
}
server.on("error", (err) => {

View File

@ -237,7 +237,7 @@ class Logger {
}
if (isObject(input)) {
const output = { ...input };
const output: Record<string, any> = { ...input };
for (const key of Object.keys(output)) {
if (isObject(output[key])) {
@ -252,7 +252,7 @@ class Logger {
output[key] = this.sanitize(output[key], level + 1);
}
}
return output;
return output as T;
}
return input;

View File

@ -38,7 +38,7 @@ describe("getDocumentParents", () => {
});
const result = collection.getDocumentParents(document.id);
expect(result?.length).toBe(1);
expect(result[0]).toBe(parent.id);
expect(result ? result[0] : undefined).toBe(parent.id);
});
test("should return array of parent document ids", async () => {

View File

@ -45,6 +45,7 @@ import {
} from "@shared/types";
import { UserRoleHelper } from "@shared/utils/UserRoleHelper";
import { stringToColor } from "@shared/utils/color";
import { locales } from "@shared/utils/date";
import env from "@server/env";
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
@ -179,8 +180,8 @@ class User extends ParanoidModel<
@Default(env.DEFAULT_LANGUAGE)
@IsIn([languages])
@Column
language: string;
@Column(DataType.STRING)
language: keyof typeof locales | null;
@AllowNull
@IsUrlOrRelativePath

View File

@ -69,6 +69,7 @@ class Model<
) {
const difference = Object.keys(previous)
.concat(Object.keys(current))
// @ts-expect-error TODO
.filter((key) => !isEqual(previous[key], current[key]));
previousAttributes[change] = pick(

View File

@ -36,11 +36,14 @@ export function CounterCache<
setImmediate(() => {
const recalculateCache =
(offset: number) => async (model: InstanceType<T>) => {
const cacheKey = `${cacheKeyPrefix}:${model[options.foreignKey]}`;
const cacheKey = `${cacheKeyPrefix}:${
model[options.foreignKey as keyof typeof model]
}`;
const count = await modelClass.count({
where: {
[options.foreignKey]: model[options.foreignKey],
[options.foreignKey]:
model[options.foreignKey as keyof typeof model],
},
});
await CacheHelper.setData(cacheKey, count + offset);

View File

@ -355,7 +355,10 @@ export default class SearchHelper {
options: SearchOptions
) {
const teamId = model instanceof Team ? model.id : model.teamId;
const where: WhereOptions<Document> = {
const where: WhereOptions<Document> & {
[Op.or]: WhereOptions<Document>[];
[Op.and]: WhereOptions<Document>[];
} = {
teamId,
[Op.or]: [],
[Op.and]: [

View File

@ -112,7 +112,7 @@ export class CanCan {
* and sent in API responses to allow clients to adjust which UI is displayed.
*/
public serialize = (performer: Model, target: Model | null): Policy => {
const output = {};
const output: Record<string, boolean | string[]> = {};
abilities.forEach((ability) => {
if (
performer instanceof ability.model &&
@ -184,11 +184,14 @@ export class CanCan {
(ability.action === "manage" || action === ability.action)
);
private get = (obj: object, key: string) =>
private get = <T extends object>(obj: T, key: keyof T) =>
"get" in obj && typeof obj.get === "function" ? obj.get(key) : obj[key];
private isPartiallyEqual = (target: object, obj: object) =>
Object.keys(obj).every((key) => this.get(target, key) === obj[key]);
private isPartiallyEqual = <T extends object>(target: T, obj: T) =>
Object.keys(obj).every(
// @ts-expect-error TODO
(key: keyof T) => this.get(target, key) === obj[key]
);
private getConditionFn =
(condition: object) => (performer: Model, target: Model) =>
@ -204,6 +207,7 @@ export class CanCan {
if (typeof value === "string") {
return [value];
}
// @ts-expect-error - TS doesn't know that value is iterable
if (typeof value[Symbol.iterator] === "function") {
// @ts-expect-error - TS doesn't know that value is iterable
return [...value];

View File

@ -1,7 +1,7 @@
import { Event } from "@server/types";
export default abstract class BaseProcessor {
static applicableEvents: Event["name"][] | ["*"] = [];
static applicableEvents: (Event["name"] | "*")[] = [];
public abstract perform(event: Event): Promise<void>;
}

View File

@ -2,9 +2,9 @@ import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs";
import BaseProcessor from "./BaseProcessor";
const processors = {};
const processors: Record<string, typeof BaseProcessor> = {};
requireDirectory<{ default: BaseProcessor }>(__dirname).forEach(
requireDirectory<{ default: typeof BaseProcessor }>(__dirname).forEach(
([module, id]) => {
if (id === "index") {
return;

View File

@ -15,6 +15,7 @@ export default class EmailTask extends BaseTask<Props> {
);
}
// @ts-expect-error We won't instantiate an abstract class
const email = new EmailClass(props, metadata);
return email.send();
}

View File

@ -2,9 +2,9 @@ import { Hook, PluginManager } from "@server/utils/PluginManager";
import { requireDirectory } from "@server/utils/fs";
import BaseTask from "./BaseTask";
const tasks = {};
const tasks: Record<string, typeof BaseTask> = {};
requireDirectory<{ default: BaseTask<any> }>(__dirname).forEach(
requireDirectory<{ default: typeof BaseTask }>(__dirname).forEach(
([module, id]) => {
if (id === "index") {
return;

View File

@ -813,7 +813,9 @@ router.post(
const { transaction } = ctx.state;
const collectionIds = await user.collectionIds({ transaction });
const where: WhereOptions<Collection> = {
const where: WhereOptions<Collection> & {
[Op.and]: WhereOptions<Collection>[];
} = {
teamId: user.teamId,
[Op.and]: [
{

View File

@ -97,7 +97,9 @@ router.post(
// always filter by the current team
const { user } = ctx.state.auth;
const where: WhereOptions<Document> = {
const where: WhereOptions<Document> & {
[Op.and]: WhereOptions<Document>[];
} = {
teamId: user.teamId,
[Op.and]: [
{

View File

@ -1,6 +1,8 @@
import { z } from "zod";
import { NotificationEventType, UserPreference, UserRole } from "@shared/types";
import { locales } from "@shared/utils/date";
import User from "@server/models/User";
import { zodEnumFromObjectKeys } from "@server/utils/zod";
import { BaseSchema } from "../schema";
const BaseIdSchema = z.object({
@ -80,7 +82,7 @@ export const UsersUpdateSchema = BaseSchema.extend({
id: z.string().uuid().optional(),
name: z.string().optional(),
avatarUrl: z.string().nullish(),
language: z.string().optional(),
language: zodEnumFromObjectKeys(locales).optional(),
preferences: z.record(z.nativeEnum(UserPreference), z.boolean()).optional(),
}),
});

View File

@ -97,7 +97,7 @@ router.use(compress());
router.get("/locales/:lng.json", async (ctx) => {
const { lng } = ctx.params;
if (!languages.includes(lng)) {
if (!languages.includes(lng as (typeof languages)[number])) {
ctx.status = 404;
return;
}

View File

@ -33,6 +33,7 @@ export default async function main(exit = false) {
user.notificationSettings = {};
for (const eventType of eventTypes) {
// @ts-expect-error old migration
user.notificationSettings[eventType] = true;
user.changed("notificationSettings", true);
}

View File

@ -12,4 +12,4 @@ export default {
web,
worker,
cron,
};
} as const;

View File

@ -3,6 +3,7 @@ import Logger from "@server/logging/Logger";
import { setResource, addTags } from "@server/logging/tracer";
import { traceFunction } from "@server/logging/tracing";
import HealthMonitor from "@server/queues/HealthMonitor";
import { Event } from "@server/types";
import { initI18n } from "@server/utils/i18n";
import {
globalEventQueue,
@ -25,7 +26,7 @@ export default function init() {
spanName: "process",
isRoot: true,
})(async function (job) {
const event = job.data;
const event = job.data as Event;
let err;
setResource(`Event.${event.name}`);
@ -99,6 +100,7 @@ export default function init() {
);
}
// @ts-expect-error We will not instantiate an abstract class
const processor = new ProcessorClass();
if (processor.perform) {
@ -146,6 +148,7 @@ export default function init() {
Logger.info("worker", `${name} running`, props);
// @ts-expect-error We will not instantiate an abstract class
const task = new TaskClass();
try {

View File

@ -23,9 +23,9 @@ export class PublicEnvironmentRegister {
static registerEnv(env: Environment) {
process.nextTick(() => {
const vars: string[] = Reflect.getMetadata(key, env) ?? [];
vars.forEach((key: string) => {
if (isUndefined(this.publicEnv[key])) {
this.publicEnv[key] = env[key];
vars.forEach((k: keyof Environment) => {
if (isUndefined(this.publicEnv[k])) {
this.publicEnv[k] = env[k];
}
});
});

View File

@ -59,13 +59,13 @@ type Token = {
};
type Segment = {
beforeTokens: Array<Token>;
afterTokens: Array<Token>;
beforeTokens: Token[];
afterTokens: Token[];
beforeIndex: number;
afterIndex: number;
beforeMap: object;
afterMap: object;
beforeMap: Record<string, number[]>;
afterMap: Record<string, number[]>;
};
type MatchT = {
@ -232,7 +232,7 @@ function Match(
*
* @return {Array} The list of tokens.
*/
function htmlToTokens(html: string): Array<Token> {
function htmlToTokens(html: string): Token[] {
let mode = "char";
let currentWord = "";
let currentAtomicTag = "";
@ -364,7 +364,7 @@ function getKeyForToken(token: string): string {
*
* @return {Object} A mapping that can be used to search for tokens.
*/
function createMap(tokens: Array<Token>): object {
function createMap(tokens: Token[]) {
return tokens.reduce(function (map, token, index) {
if (map[token.key]) {
map[token.key].push(index);
@ -651,8 +651,8 @@ function getFullMatch(
* @return {Segment} The segment object.
*/
function createSegment(
beforeTokens: Array<Token>,
afterTokens: Array<Token>,
beforeTokens: Token[],
afterTokens: Token[],
beforeIndex: number,
afterIndex: number
): Segment {
@ -759,8 +759,8 @@ function findMatchingBlocks(segment: Segment): Array<MatchT> {
* - {number} endInAfter The end of the range in the list of after tokens.
*/
function calculateOperations(
beforeTokens: Array<Token>,
afterTokens: Array<Token>
beforeTokens: Token[],
afterTokens: Token[]
): Array<Operation> {
if (!beforeTokens) {
throw new Error("Missing beforeTokens");
@ -902,8 +902,8 @@ function TokenWrapper(tokens: any) {
* and whether those tokens are wrappable or not. The result should be a string.
*/
TokenWrapper.prototype.combine = function (
mapFn: (wrappable: boolean, tokens: Array<Token>) => void,
tagFn: (tokens: Array<Token>) => void
mapFn: (wrappable: boolean, tokens: Token[]) => void,
tagFn: (tokens: Token[]) => void
) {
const notes = this.notes;
const tokens = this.tokens.slice();
@ -1069,7 +1069,8 @@ function renderOperations(
return operations.reduce(function (rendering, op, index) {
return (
rendering +
OPS[op.action](
// @ts-expect-error TODO
OPS[op.action]?.(
op,
beforeTokens,
afterTokens,

View File

@ -6,7 +6,7 @@ import { Collection, Document, Star } from "@server/models";
export async function collectionIndexing(
teamId: string,
{ transaction }: FindOptions<Collection>
): Promise<{ [id: string]: string }> {
) {
const collections = await Collection.findAll({
where: {
teamId,
@ -34,16 +34,14 @@ export async function collectionIndexing(
await Promise.all(promises);
const indexedCollections = {};
const indexedCollections: Record<string, string | null> = {};
sortable.forEach((collection) => {
indexedCollections[collection.id] = collection.index;
});
return indexedCollections;
}
export async function starIndexing(
userId: string
): Promise<{ [id: string]: string }> {
export async function starIndexing(userId: string) {
const stars = await Star.findAll({
where: { userId },
});
@ -77,7 +75,7 @@ export async function starIndexing(
await Promise.all(promises);
const indexedStars = {};
const indexedStars: Record<string, string | null> = {};
sortable.forEach((star) => {
indexedStars[star.id] = star.index;
});

View File

@ -4,6 +4,7 @@ import Logger from "@server/logging/Logger";
export type Chunk = {
file: string;
imports: string[];
src: string;
isEntry?: boolean;
};

View File

@ -10,7 +10,11 @@ const tableShouldBeSkippedCache = new WeakMap<HTMLTableElement, boolean>();
function getAlignment(node: HTMLElement) {
return node
? (node.getAttribute("align") || node.style.textAlign || "").toLowerCase()
? ((
node.getAttribute("align") ||
node.style.textAlign ||
""
).toLowerCase() as "left" | "right" | "center")
: "";
}

View File

@ -19,7 +19,7 @@ export function CannotUseWithout(
validator: {
validate<T>(value: T, args: ValidationArguments) {
const obj = args.object as unknown as T;
const required = args.constraints[0] as string;
const required = args.constraints[0] as keyof T;
return obj[required] !== undefined;
},
defaultMessage(args: ValidationArguments) {

View File

@ -5,7 +5,9 @@ import { Command, Plugin } from "prosemirror-state";
import { Primitive } from "utility-types";
import type { Editor } from "../../../app/editor";
export type CommandFactory = (attrs?: Record<string, Primitive>) => Command;
export type CommandFactory = (
attrs?: Record<string, Primitive>
) => Command | void;
export type WidgetProps = { rtl: boolean; readOnly: boolean | undefined };

View File

@ -186,6 +186,7 @@ export default class ExtensionManager {
.map((extension) =>
["node", "mark"].includes(extension.type)
? extension.keys({
// @ts-expect-error TODO
type: schema[`${extension.type}s`][extension.name],
schema,
})
@ -206,6 +207,7 @@ export default class ExtensionManager {
.filter((extension) => extension.inputRules)
.map((extension) =>
extension.inputRules({
// @ts-expect-error TODO
type: schema[`${extension.type}s`][extension.name],
schema,
})
@ -222,13 +224,14 @@ export default class ExtensionManager {
.filter((extension) => extension.commands)
.reduce((allCommands, extension) => {
const { name, type } = extension;
const commands = {};
const commands: Record<string, CommandFactory> = {};
// @ts-expect-error FIXME
const value = extension.commands({
schema,
...(["node", "mark"].includes(type)
? {
// @ts-expect-error TODO
type: schema[`${type}s`][name],
}
: {}),
@ -239,12 +242,12 @@ export default class ExtensionManager {
attrs: Record<string, Primitive>
) => {
if (!view.editable && !extension.allowInReadOnly) {
return false;
return;
}
if (extension.focusAfterExecution) {
view.focus();
}
return callback(attrs)(view.state, view.dispatch, view);
return callback(attrs)?.(view.state, view.dispatch, view);
};
const handle = (_name: string, _value: CommandFactory) => {
@ -252,8 +255,8 @@ export default class ExtensionManager {
commands[_name] = (attrs: Record<string, Primitive>) =>
_value.forEach((callback) => apply(callback, attrs));
} else if (typeof _value === "function") {
commands[_name] = (attrs: Record<string, Primitive>) =>
apply(_value, attrs);
commands[_name] = ((attrs: Record<string, Primitive>) =>
apply(_value, attrs)) as CommandFactory;
}
};

View File

@ -1,4 +1,5 @@
import Storage from "../../utils/Storage";
import { LANGUAGES } from "../extensions/Prism";
const RecentStorageKey = "rme-code-language";
const StorageKey = "frequent-code-languages";
@ -44,9 +45,10 @@ export const getRecentCodeLanguage = () => Storage.get(RecentStorageKey);
export const getFrequentCodeLanguages = () => {
const recentLang = Storage.get(RecentStorageKey);
const frequentLangEntries = Object.entries(
(Storage.get(StorageKey) ?? {}) as Record<string, number>
);
const frequentLangEntries = Object.entries(Storage.get(StorageKey) ?? {}) as [
keyof typeof LANGUAGES,
number
][];
const frequentLangs = sortFrequencies(frequentLangEntries)
.slice(0, FrequentlyUsedCount.Get)
@ -61,5 +63,5 @@ export const getFrequentCodeLanguages = () => {
return frequentLangs;
};
const sortFrequencies = (freqs: [string, number][]) =>
const sortFrequencies = <T>(freqs: [T, number][]) =>
freqs.sort((a, b) => (a[1] >= b[1] ? -1 : 1));

View File

@ -21,6 +21,7 @@ export const nameToEmoji: Record<string, string> = Object.values(
(data as EmojiMartData).emojis
).reduce((acc, emoji) => {
const convertedId = snakeCase(emoji.id);
// @ts-expect-error emojiMartToGemoji is a valid map
acc[emojiMartToGemoji[convertedId] ?? convertedId] = emoji.skins[0].native;
return acc;
}, {});

View File

@ -13,7 +13,7 @@ export function getFromPath(obj: JSONValue, path: string): JSONValue {
if (typeof obj !== "object") {
throw new Error();
}
const property = pathParts.shift() as string;
const property = pathParts.shift() as keyof JSONValue;
obj = obj[property];
}
return obj;

View File

@ -220,7 +220,7 @@ export default class Heading extends Node {
get plugins() {
const getAnchors = (doc: ProsemirrorNode) => {
const decorations: Decoration[] = [];
const previouslySeen = {};
const previouslySeen: Record<string, number> = {};
doc.descendants((node, pos) => {
if (node.type.name !== this.name) {

View File

@ -1,6 +1,13 @@
import { locales } from "../utils/date";
type LanguageOption = {
label: string;
value: keyof typeof locales;
};
// Note: Updating the available languages? Make sure to also update the
// locales array in shared/utils/date.ts to enable translation for timestamps.
export const languageOptions = [
export const languageOptions: LanguageOption[] = [
{
label: "English (US)",
value: "en_US",

View File

@ -168,6 +168,7 @@
"Are you sure you want to permanently delete this entire comment thread?": "Are you sure you want to permanently delete this entire comment thread?",
"Are you sure you want to permanently delete this comment?": "Are you sure you want to permanently delete this comment?",
"Confirm": "Confirm",
"manage access": "manage access",
"view and edit access": "view and edit access",
"view only access": "view only access",
"no access": "no access",

View File

@ -292,20 +292,22 @@ export type NotificationSettings = {
| boolean;
};
export const NotificationEventDefaults = {
[NotificationEventType.PublishDocument]: false,
[NotificationEventType.UpdateDocument]: true,
[NotificationEventType.CreateCollection]: false,
[NotificationEventType.CreateComment]: true,
[NotificationEventType.MentionedInDocument]: true,
[NotificationEventType.MentionedInComment]: true,
[NotificationEventType.InviteAccepted]: true,
[NotificationEventType.Onboarding]: true,
[NotificationEventType.Features]: true,
[NotificationEventType.ExportCompleted]: true,
[NotificationEventType.AddUserToDocument]: true,
[NotificationEventType.AddUserToCollection]: true,
};
export const NotificationEventDefaults: Record<NotificationEventType, boolean> =
{
[NotificationEventType.PublishDocument]: false,
[NotificationEventType.UpdateDocument]: true,
[NotificationEventType.CreateCollection]: false,
[NotificationEventType.CreateComment]: true,
[NotificationEventType.CreateRevision]: false,
[NotificationEventType.MentionedInDocument]: true,
[NotificationEventType.MentionedInComment]: true,
[NotificationEventType.InviteAccepted]: true,
[NotificationEventType.Onboarding]: true,
[NotificationEventType.Features]: true,
[NotificationEventType.ExportCompleted]: true,
[NotificationEventType.AddUserToDocument]: true,
[NotificationEventType.AddUserToCollection]: true,
};
export enum UnfurlResourceType {
OEmbed = "oembed",

View File

@ -279,7 +279,7 @@ export class ProsemirrorHelper {
*/
static getHeadings(doc: Node, schema: Schema) {
const headings: Heading[] = [];
const previouslySeen = {};
const previouslySeen: Record<string, number> = {};
doc.forEach((node) => {
if (node.type.name === "heading") {

View File

@ -75,7 +75,7 @@ class Storage {
* when localStorage is not available.
*/
class MemoryStorage {
private data = {};
private data: Record<string, string> = {};
getItem(key: string) {
return this.data[key] || null;

View File

@ -189,7 +189,7 @@ const locales = {
* @param language The user language
* @returns The date-fns locale.
*/
export function dateLocale(language: string | null | undefined) {
export function dateLocale(language: keyof typeof locales | undefined | null) {
return language ? locales[language] : undefined;
}

View File

@ -51,7 +51,9 @@ type GetVariantsProps = {
const getVariants = ({ id, name, skins }: GetVariantsProps): EmojiVariants =>
skins.reduce((obj, skin) => {
const skinToneCode = skin.unified.split("-")[1];
const skinToneCode = skin.unified.split(
"-"
)[1] as keyof typeof SKINTONE_CODE_TO_ENUM;
const skinToneType =
SKINTONE_CODE_TO_ENUM[skinToneCode] ?? EmojiSkinTone.Default;
obj[skinToneType] = { id, name, value: skin.native } satisfies Emoji;
@ -72,7 +74,8 @@ const EMOJI_ID_TO_VARIANTS = Object.entries(Emojis).reduce(
const CATEGORY_TO_EMOJI_IDS: Record<EmojiCategory, string[]> =
Categories.reduce((obj, { id, emojis }) => {
const category = EmojiCategory[capitalize(id)];
const key = capitalize(id) as EmojiCategory;
const category = EmojiCategory[key];
if (!category) {
return obj;
}

View File

@ -14,7 +14,7 @@ const stripEmojis = (value: string) => value.replace(regex, "");
const cleanValue = (value: string) => stripEmojis(deburr(value));
function getSortByField<T>(
function getSortByField<T extends Record<string, any>>(
item: T,
keyOrCallback: string | ((item: T) => string)
) {
@ -25,7 +25,7 @@ function getSortByField<T>(
return cleanValue(field);
}
function naturalSortBy<T>(
function naturalSortBy<T extends Record<string, any>>(
items: T[],
key: string | ((item: T) => string),
sortOptions?: NaturalSortOptions

View File

@ -23,7 +23,6 @@
"noEmit": true,
"skipLibCheck": true,
"ignoreDeprecations": "5.0",
"suppressImplicitAnyIndexErrors": true,
"target": "es2020",
"paths": {
"@server/*": ["./server/*"],