diff --git a/server/emails/templates/index.ts b/server/emails/templates/index.ts index 3d5a55bb7d..e5c86cfb72 100644 --- a/server/emails/templates/index.ts +++ b/server/emails/templates/index.ts @@ -2,6 +2,7 @@ import { Hook, PluginManager } from "@server/utils/PluginManager"; import { requireDirectory } from "@server/utils/fs"; import type BaseEmail from "./BaseEmail"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- registry of heterogeneous template Props subtypes; BaseEmail isn't assignable from BaseEmail. const emails: Record> = {}; requireDirectory<{ default: typeof BaseEmail }>(__dirname).forEach( diff --git a/server/models/AuthenticationProvider.test.ts b/server/models/AuthenticationProvider.test.ts index 57c0e809c3..343be6d8d9 100644 --- a/server/models/AuthenticationProvider.test.ts +++ b/server/models/AuthenticationProvider.test.ts @@ -1,5 +1,5 @@ import { buildTeam } from "@server/test/factories"; -import { AuthenticationProvider } from "@server/models"; +import { AuthenticationProvider, type User } from "@server/models"; import { createContext } from "@server/context"; describe("AuthenticationProvider", () => { @@ -15,7 +15,7 @@ describe("AuthenticationProvider", () => { enabled: true, }); - const ctx = createContext({ user: { team } as any }); + const ctx = createContext({ user: { team } as unknown as User }); await expect(provider.disable(ctx)).resolves.not.toThrow(); expect(provider.enabled).toBe(false); }); @@ -37,7 +37,7 @@ describe("AuthenticationProvider", () => { enabled: true, }); - const ctx = createContext({ user: { team } as any }); + const ctx = createContext({ user: { team } as unknown as User }); await expect(provider1.disable(ctx)).resolves.not.toThrow(); expect(provider1.enabled).toBe(false); }); @@ -59,7 +59,7 @@ describe("AuthenticationProvider", () => { enabled: true, }); - const ctx = createContext({ user: { team } as any }); + const ctx = createContext({ user: { team } as unknown as User }); await expect(provider.disable(ctx)).rejects.toThrow( "At least one authentication provider is required" ); diff --git a/server/models/Collection.ts b/server/models/Collection.ts index e91c2812a5..f4e3271a12 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -49,6 +49,7 @@ import { import isUUID from "validator/lib/isUUID"; import type { CollectionSort, + ImportableIntegrationService, ProsemirrorData, SourceMetadata, NavigationNode, @@ -510,7 +511,7 @@ class Collection extends ParanoidModel< importId: string | null; @BelongsTo(() => Import, "apiImportId") - apiImport: Import | null; + apiImport: Import | null; @ForeignKey(() => Import) @Column(DataType.UUID) diff --git a/server/models/Document.ts b/server/models/Document.ts index eacaa28f2b..8cc46d8611 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -40,6 +40,7 @@ import { import { MaxLength } from "class-validator"; import isUUID from "validator/lib/isUUID"; import type { + ImportableIntegrationService, NavigationNode, ProsemirrorData, SourceMetadata, @@ -609,7 +610,7 @@ class Document extends ArchivableModel< importId: string | null; @BelongsTo(() => Import, "apiImportId") - apiImport: Import | null; + apiImport: Import | null; @ForeignKey(() => Import) @Column(DataType.UUID) diff --git a/server/models/base/ArchivableModel.ts b/server/models/base/ArchivableModel.ts index 6018a95e00..1f8a0e47e7 100644 --- a/server/models/base/ArchivableModel.ts +++ b/server/models/base/ArchivableModel.ts @@ -2,6 +2,7 @@ import { AllowNull, Column, IsDate } from "sequelize-typescript"; import ParanoidModel from "./ParanoidModel"; class ArchivableModel< + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes to never inside Sequelize helpers. TModelAttributes extends object = any, TCreationAttributes extends object = TModelAttributes, > extends ParanoidModel { diff --git a/server/models/base/IdModel.ts b/server/models/base/IdModel.ts index 2d8e5375bf..d2f4541742 100644 --- a/server/models/base/IdModel.ts +++ b/server/models/base/IdModel.ts @@ -10,6 +10,7 @@ import { import Model from "./Model"; class IdModel< + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes to never inside Sequelize helpers. TModelAttributes extends object = any, TCreationAttributes extends object = TModelAttributes, > extends Model { diff --git a/server/models/base/Model.ts b/server/models/base/Model.ts index 608ab944ac..7948dd4448 100644 --- a/server/models/base/Model.ts +++ b/server/models/base/Model.ts @@ -24,7 +24,7 @@ import { } from "sequelize-typescript"; import Logger from "@server/logging/Logger"; import type { Replace, APIContext } from "@server/types"; -import { getChangsetSkipped } from "../decorators/Changeset"; +import { getChangesetSkipped } from "../decorators/Changeset"; import { InternalError } from "@server/errors"; type EventOverrideOptions = { @@ -48,6 +48,7 @@ type EventOptions = EventOverrideOptions & { export type HookContext = APIContext["context"] & { event?: EventOptions }; class Model< + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Sequelize's Attributes resolves to never under object default; load-bearing for static helpers like saveWithCtx. TModelAttributes extends object = any, TCreationAttributes extends object = TModelAttributes, > extends SequelizeModel { @@ -392,7 +393,7 @@ class Model< const virtualFields = (this.constructor as typeof Model).virtualFields; const blobFields = (this.constructor as typeof Model).blobFields; - const skippedFields = getChangsetSkipped(this); + const skippedFields = getChangesetSkipped(this); for (const change of changes) { const previous = this.previous(change); diff --git a/server/models/base/ParanoidModel.ts b/server/models/base/ParanoidModel.ts index 0f84e23779..2bdb4746e0 100644 --- a/server/models/base/ParanoidModel.ts +++ b/server/models/base/ParanoidModel.ts @@ -2,6 +2,7 @@ import { DeletedAt } from "sequelize-typescript"; import IdModel from "./IdModel"; class ParanoidModel< + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors Model base; tightening to object resolves Attributes to never inside Sequelize helpers. TModelAttributes extends object = any, TCreationAttributes extends object = TModelAttributes, > extends IdModel { diff --git a/server/models/decorators/Changeset.ts b/server/models/decorators/Changeset.ts index 0764b4a488..b10b841583 100644 --- a/server/models/decorators/Changeset.ts +++ b/server/models/decorators/Changeset.ts @@ -5,7 +5,7 @@ const key = Symbol("skipChangeset"); /** * This decorator is used to annotate a property as being skipped from being included in a changeset. */ -export function SkipChangeset(target: any, propertyKey: string) { +export function SkipChangeset(target: object, propertyKey: string) { const properties: string[] = Reflect.getMetadata(key, target); if (!properties) { @@ -18,6 +18,6 @@ export function SkipChangeset(target: any, propertyKey: string) { /** * This function is used to get the properties that should be skipped from a changeset. */ -export function getChangsetSkipped(target: any): string[] { +export function getChangesetSkipped(target: object): string[] { return Reflect.getMetadata(key, target) || []; } diff --git a/server/models/decorators/CounterCache.ts b/server/models/decorators/CounterCache.ts index dda4129e15..825d0bf9a0 100644 --- a/server/models/decorators/CounterCache.ts +++ b/server/models/decorators/CounterCache.ts @@ -78,6 +78,7 @@ export function CounterCache< }); }); }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS rejects PropertyDescriptor as legacy decorator return type; descriptor is consumed by Sequelize at runtime. } as any; }; } diff --git a/server/models/decorators/Encrypted.ts b/server/models/decorators/Encrypted.ts index b89c3cfec1..8bbffcf5a3 100644 --- a/server/models/decorators/Encrypted.ts +++ b/server/models/decorators/Encrypted.ts @@ -11,7 +11,7 @@ const key = "sequelize:vault"; * so that it can be used by getters and setters. Must be accompanied by a * @Column(DataType.BLOB) annotation. */ -export default function Encrypted(target: any, propertyKey: string) { +export default function Encrypted(target: object, propertyKey: string) { // Ensure that the Encrypted decorator is the first decorator applied to the property, we can check // this by looking at the attributes of the target and checking if the propertyKey is already defined. if (getAttributes(target)[propertyKey]) { @@ -71,5 +71,6 @@ export default function Encrypted(target: any, propertyKey: string) { throw err; } }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS rejects PropertyDescriptor return for legacy decorator; descriptor is consumed by Sequelize at runtime. } as any; } diff --git a/server/models/decorators/Fix.ts b/server/models/decorators/Fix.ts index 8a96529ce3..d2bf73d749 100644 --- a/server/models/decorators/Fix.ts +++ b/server/models/decorators/Fix.ts @@ -6,8 +6,10 @@ * @param target model class */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- decorator extends a Sequelize Model class with dynamic statics (rawAttributes, associations) that defy precise typing. export default function Fix(target: any): void { return class extends target { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor must accept arbitrary Sequelize Model args. constructor(...args: any[]) { // suppresses warning from here which is not applicable in our typescript // environment: https://github.com/sequelize/sequelize/blob/00ced18c2cb2a8b99ae0ebf5669c124abb4c673d/src/model.js#L99 @@ -63,5 +65,6 @@ export default function Fix(target: any): void { }); }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mixin class returned in place of original target; consumer expects original class shape. } as any; } diff --git a/server/models/validators/IsFQDN.ts b/server/models/validators/IsFQDN.ts index 4bbe7a2e3d..ea4c59eb47 100644 --- a/server/models/validators/IsFQDN.ts +++ b/server/models/validators/IsFQDN.ts @@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript"; /** * A decorator that validates that a string is a fully qualified domain name. */ -export default function IsFQDN(target: any, propertyName: string) { +export default function IsFQDN(target: object, propertyName: string) { return addAttributeOptions(target, propertyName, { validate: { validDomain(value: string) { diff --git a/server/models/validators/IsHexColor.ts b/server/models/validators/IsHexColor.ts index 0a2b2eb58e..320ef0e617 100644 --- a/server/models/validators/IsHexColor.ts +++ b/server/models/validators/IsHexColor.ts @@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript"; /** * A decorator that validates that a string is a valid hex color code. */ -export default function IsHexColor(target: any, propertyName: string) { +export default function IsHexColor(target: object, propertyName: string) { return addAttributeOptions(target, propertyName, { validate: { validDomain(value: string) { diff --git a/server/models/validators/IsUrlOrRelativePath.ts b/server/models/validators/IsUrlOrRelativePath.ts index 8cec74aef2..19703b1fd6 100644 --- a/server/models/validators/IsUrlOrRelativePath.ts +++ b/server/models/validators/IsUrlOrRelativePath.ts @@ -4,7 +4,10 @@ import { addAttributeOptions } from "sequelize-typescript"; /** * A decorator that validates that a string is a url or relative path. */ -export default function IsUrlOrRelativePath(target: any, propertyName: string) { +export default function IsUrlOrRelativePath( + target: object, + propertyName: string +) { return addAttributeOptions(target, propertyName, { validate: { validUrlOrPath(value: string) { diff --git a/server/models/validators/Length.ts b/server/models/validators/Length.ts index 572fcd7a0d..19d9e7911d 100644 --- a/server/models/validators/Length.ts +++ b/server/models/validators/Length.ts @@ -13,8 +13,8 @@ export default function Length({ msg?: string; min?: number; max: number; -}): (target: any, propertyName: string) => void { - return (target: any, propertyName: string) => +}): (target: object, propertyName: string) => void { + return (target: object, propertyName: string) => addAttributeOptions(target, propertyName, { validate: { validLength(value: string) { diff --git a/server/models/validators/NotContainsUrl.ts b/server/models/validators/NotContainsUrl.ts index fffbd0a8df..7b485b7a03 100644 --- a/server/models/validators/NotContainsUrl.ts +++ b/server/models/validators/NotContainsUrl.ts @@ -4,7 +4,7 @@ import { addAttributeOptions } from "sequelize-typescript"; * A decorator that validates that a string does not include something that * looks like a URL. */ -export default function NotContainsUrl(target: any, propertyName: string) { +export default function NotContainsUrl(target: object, propertyName: string) { return addAttributeOptions(target, propertyName, { validate: { not: { diff --git a/server/models/validators/TextLength.ts b/server/models/validators/TextLength.ts index fe39c45d89..83374ee0e3 100644 --- a/server/models/validators/TextLength.ts +++ b/server/models/validators/TextLength.ts @@ -17,8 +17,8 @@ export default function TextLength({ msg?: string; min?: number; max: number; -}): (target: any, propertyName: string) => void { - return (target: any, propertyName: string) => +}): (target: object, propertyName: string) => void { + return (target: object, propertyName: string) => addAttributeOptions(target, propertyName, { validate: { validLength(value: ProsemirrorData) { diff --git a/server/onerror.test.ts b/server/onerror.test.ts index 7c9bd43dcf..e1833c3467 100644 --- a/server/onerror.test.ts +++ b/server/onerror.test.ts @@ -8,15 +8,32 @@ jest.mock("@server/logging/sentry", () => ({ requestErrorHandler: jest.fn(), })); +type MockCtx = { + headers: Record; + headerSent: boolean; + writable: boolean; + accepts: jest.Mock; + set: jest.Mock; + res: { end: jest.Mock }; + status: number | undefined; + type: string | undefined; + body: unknown; +}; + +type ReportableError = Error & { + status?: number; + isReportable?: boolean; +}; + describe("onerror", () => { let app: Koa; - let ctx: any; + let ctx: MockCtx; beforeEach(() => { // Create a mock Koa app app = { context: {}, - } as any; + } as unknown as Koa; // Apply the onerror middleware onerror(app); @@ -68,7 +85,7 @@ describe("onerror", () => { }); it("should report unknown errors without isReportable property to Sentry", () => { - const error = new Error("Unknown error") as any; + const error = new Error("Unknown error") as ReportableError; error.status = 500; app.context.onerror.call(ctx, error); @@ -77,7 +94,7 @@ describe("onerror", () => { }); it("should report errors with invalid status codes to Sentry", () => { - const error = new Error("Invalid status error") as any; + const error = new Error("Invalid status error") as ReportableError; error.status = 999; app.context.onerror.call(ctx, error); @@ -86,7 +103,7 @@ describe("onerror", () => { }); it("should not report errors explicitly marked with isReportable: false", () => { - const error = new Error("Custom error") as any; + const error = new Error("Custom error") as ReportableError; error.status = 500; error.isReportable = false; @@ -96,7 +113,7 @@ describe("onerror", () => { }); it("should report errors explicitly marked with isReportable: true", () => { - const error = new Error("Custom error") as any; + const error = new Error("Custom error") as ReportableError; error.status = 400; error.isReportable = true; diff --git a/server/presenters/collection.ts b/server/presenters/collection.ts index d49838c882..397ab52c97 100644 --- a/server/presenters/collection.ts +++ b/server/presenters/collection.ts @@ -20,7 +20,7 @@ export default async function presentCollection( ) { const asData = !ctx || Number(ctx?.headers["x-api-version"] ?? 0) >= 3; - const res: Record = { + const res: Record = { id: collection.id, url: collection.path, urlId: collection.urlId, diff --git a/server/presenters/unfurl.ts b/server/presenters/unfurl.ts index 1e8b81a002..caa88daebd 100644 --- a/server/presenters/unfurl.ts +++ b/server/presenters/unfurl.ts @@ -7,8 +7,11 @@ import type { Document, User, Group } from "@server/models"; import { View } from "@server/models"; import { opts } from "@server/utils/i18n"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- heterogeneous payload from internal callers and third-party unfurl plugins. +type UnfurlData = Record; + async function presentUnfurl( - data: Record, + data: UnfurlData, options?: { includeEmail: boolean } ) { switch (data.type) { @@ -30,7 +33,7 @@ async function presentUnfurl( } const presentURL = ( - data: Record + data: UnfurlData ): UnfurlResponse[UnfurlResourceType.URL] => { // TODO: For backwards compatibility, remove once cache has expired in next release. if (data.transformedUnfurl) { @@ -49,7 +52,7 @@ const presentURL = ( }; const presentMention = async ( - data: Record, + data: UnfurlData, options?: { includeEmail: boolean } ): Promise => { const user: User = data.user; @@ -69,7 +72,7 @@ const presentMention = async ( }; const presentGroup = async ( - data: Record + data: UnfurlData ): Promise => { const group: Group = data.group; const memberCount = await group.memberCount; @@ -89,7 +92,7 @@ const presentGroup = async ( }; const presentDocument = ( - data: Record + data: UnfurlData ): UnfurlResponse[UnfurlResourceType.Document] => { const document: Document = data.document; const viewer: User | undefined = data.viewer; @@ -106,18 +109,16 @@ const presentDocument = ( }; }; -const presentPR = ( - data: Record -): UnfurlResponse[UnfurlResourceType.PR] => +const presentPR = (data: UnfurlData): UnfurlResponse[UnfurlResourceType.PR] => data as UnfurlResponse[UnfurlResourceType.PR]; // this would have been transformed by the unfurl plugin. const presentIssue = ( - data: Record + data: UnfurlData ): UnfurlResponse[UnfurlResourceType.Issue] => data as UnfurlResponse[UnfurlResourceType.Issue]; // this would have been transformed by the unfurl plugin. const presentProject = ( - data: Record + data: UnfurlData ): UnfurlResponse[UnfurlResourceType.Project] => data as UnfurlResponse[UnfurlResourceType.Project]; // this would have been transformed by the unfurl plugin. diff --git a/server/queues/processors/SearchIndexProcessor.test.ts b/server/queues/processors/SearchIndexProcessor.test.ts index 5128a0851b..0bd114adc5 100644 --- a/server/queues/processors/SearchIndexProcessor.test.ts +++ b/server/queues/processors/SearchIndexProcessor.test.ts @@ -7,6 +7,8 @@ import { import SearchProviderManager from "@server/utils/SearchProviderManager"; import SearchIndexProcessor from "./SearchIndexProcessor"; +type PerformArg = Parameters[0]; + const processor = new SearchIndexProcessor(); describe("SearchIndexProcessor", () => { @@ -48,7 +50,7 @@ describe("SearchIndexProcessor", () => { collectionId: collection.id, teamId: user.teamId, actorId: user.id, - } as any); + } as PerformArg); expect(indexSpy).toHaveBeenCalledWith( SearchableModel.Document, @@ -69,7 +71,7 @@ describe("SearchIndexProcessor", () => { collectionId: "some-collection-id", teamId: user.teamId, actorId: user.id, - } as any); + } as PerformArg); expect(removeSpy).toHaveBeenCalledWith( SearchableModel.Document, diff --git a/server/queues/tasks/base/BaseTask.ts b/server/queues/tasks/base/BaseTask.ts index 270ac575c1..cd90488271 100644 --- a/server/queues/tasks/base/BaseTask.ts +++ b/server/queues/tasks/base/BaseTask.ts @@ -8,7 +8,7 @@ export enum TaskPriority { High = 10, } -export abstract class BaseTask> { +export abstract class BaseTask { /** * Schedule this task type to be processed asynchronously by a worker. * @@ -32,7 +32,7 @@ export abstract class BaseTask> { * @param props Properties to be used by the task * @returns A promise that resolves once the task has completed. */ - public abstract perform(props: T): Promise; + public abstract perform(props: T): Promise; /** * Handle failure when all attempts are exhausted for the task. diff --git a/server/routes/api/auth/auth.test.ts b/server/routes/api/auth/auth.test.ts index 0b5c488a17..f0cf2a1fb4 100644 --- a/server/routes/api/auth/auth.test.ts +++ b/server/routes/api/auth/auth.test.ts @@ -35,7 +35,9 @@ describe("#auth.info", () => { const body = await res.json(); expect(res.status).toEqual(200); - const availableTeamIds = body.data.availableTeams.map((t: any) => t.id); + const availableTeamIds = body.data.availableTeams.map( + (t: { id: string }) => t.id + ); expect(availableTeamIds.length).toEqual(3); expect(availableTeamIds).toContain(team.id); diff --git a/server/routes/api/documents/documents.test.ts b/server/routes/api/documents/documents.test.ts index 1876d1057b..9f2b5a6b93 100644 --- a/server/routes/api/documents/documents.test.ts +++ b/server/routes/api/documents/documents.test.ts @@ -834,7 +834,7 @@ describe("#documents.list", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.data).toHaveLength(2); - const docIds = body.data.map((doc: any) => doc.id); + const docIds = body.data.map((doc: { id: string }) => doc.id); expect(docIds).toContain(docs[0].id); expect(docIds).toContain(docs[1].id); expect(docIds).not.toContain(docs[2].id); @@ -2033,7 +2033,9 @@ describe("#documents.search", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.data).toHaveLength(2); - const returnedIds = body.data.map((d: any) => d.document.id).sort(); + const returnedIds = body.data + .map((d: { document: { id: string } }) => d.document.id) + .sort(); const expectedIds = docsInCollection1.map((d) => d.id).sort(); expect(returnedIds).toEqual(expectedIds); }); @@ -3218,7 +3220,7 @@ describe("#documents.restore", () => { revisionId, }, headers: { - "x-api-version": 3, + "x-api-version": "3", }, }); const body = await res.json(); @@ -3333,10 +3335,12 @@ describe("#documents.import", () => { collectionId: collection.id, }); - jest.spyOn(FileStorage, "store").mockResolvedValue(undefined as any); + jest + .spyOn(FileStorage, "store") + .mockResolvedValue(undefined as unknown as string); jest.spyOn(DocumentImportTask.prototype, "schedule").mockResolvedValue({ finished: jest.fn().mockResolvedValue({ documentId: document.id }), - } as any); + } as unknown as Awaited>); const content = await readFile( path.resolve( @@ -5765,7 +5769,7 @@ describe("#documents.documents", () => { expect(res.status).toBe(200); expect(body.data.id).toBe(parent.id); - const childIds = body.data.children.map((node: any) => node.id); + const childIds = body.data.children.map((node: { id: string }) => node.id); expect(childIds).toContain(child1.id); expect(childIds).toContain(child2.id); }); diff --git a/server/routes/api/events/events.test.ts b/server/routes/api/events/events.test.ts index 6a6071c05e..82435d6890 100644 --- a/server/routes/api/events/events.test.ts +++ b/server/routes/api/events/events.test.ts @@ -472,7 +472,7 @@ describe("#events.list", () => { expect(res.status).toEqual(200); // admin SHOULD see events for documents without a collection - const eventIds = body.data.map((e: any) => e.id); + const eventIds = body.data.map((e: { id: string }) => e.id); expect(eventIds).toContain(draftEvent.id); }); diff --git a/server/routes/api/groups/groups.test.ts b/server/routes/api/groups/groups.test.ts index 0b36d763a8..f462a9db43 100644 --- a/server/routes/api/groups/groups.test.ts +++ b/server/routes/api/groups/groups.test.ts @@ -293,11 +293,13 @@ describe("#groups.list", () => { expect(body.data.groupMemberships[0].groupId).toEqual(group.id); expect(body.data.groupMemberships[1].groupId).toEqual(group.id); expect( - body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id) + body.data.groupMemberships + .map((u: { user: { id: string } }) => u.user.id) + .includes(user.id) ).toBe(true); expect( body.data.groupMemberships - .map((u: any) => u.user.id) + .map((u: { user: { id: string } }) => u.user.id) .includes(anotherUser.id) ).toBe(true); expect(body.policies.length).toEqual(2); @@ -317,11 +319,13 @@ describe("#groups.list", () => { expect(anotherBody.data.groupMemberships[0].groupId).toEqual(group.id); expect(anotherBody.data.groupMemberships[1].groupId).toEqual(group.id); expect( - body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id) + anotherBody.data.groupMemberships + .map((u: { user: { id: string } }) => u.user.id) + .includes(user.id) ).toBe(true); expect( - body.data.groupMemberships - .map((u: any) => u.user.id) + anotherBody.data.groupMemberships + .map((u: { user: { id: string } }) => u.user.id) .includes(anotherUser.id) ).toBe(true); }); diff --git a/server/routes/api/notifications/notifications.test.ts b/server/routes/api/notifications/notifications.test.ts index e9e6ee4c46..345199cdf3 100644 --- a/server/routes/api/notifications/notifications.test.ts +++ b/server/routes/api/notifications/notifications.test.ts @@ -12,6 +12,12 @@ import { } from "@server/test/factories"; import { getTestServer } from "@server/test/support"; +type NotificationItem = { + actor: { id: string }; + userId: string; + event: NotificationEventType; +}; + const server = getTestServer(); describe("#notifications.list", () => { @@ -69,13 +75,15 @@ describe("#notifications.list", () => { expect(body.data.notifications.length).toBe(3); expect(body.pagination.total).toBe(3); expect(body.data.unseen).toBe(2); - expect((randomElement(body.data.notifications) as any).actor.id).toBe( - actor.id + expect( + randomElement(body.data.notifications).actor.id + ).toBe(actor.id); + expect( + randomElement(body.data.notifications).userId + ).toBe(user.id); + const events = body.data.notifications.map( + (n: NotificationItem) => n.event ); - expect((randomElement(body.data.notifications) as any).userId).toBe( - user.id - ); - const events = body.data.notifications.map((n: any) => n.event); expect(events).toContain(NotificationEventType.UpdateDocument); expect(events).toContain(NotificationEventType.CreateComment); expect(events).toContain(NotificationEventType.MentionedInComment); @@ -134,13 +142,15 @@ describe("#notifications.list", () => { expect(body.data.notifications.length).toBe(1); expect(body.pagination.total).toBe(1); expect(body.data.unseen).toBe(1); - expect((randomElement(body.data.notifications) as any).actor.id).toBe( - actor.id + expect( + randomElement(body.data.notifications).actor.id + ).toBe(actor.id); + expect( + randomElement(body.data.notifications).userId + ).toBe(user.id); + const events = body.data.notifications.map( + (n: NotificationItem) => n.event ); - expect((randomElement(body.data.notifications) as any).userId).toBe( - user.id - ); - const events = body.data.notifications.map((n: any) => n.event); expect(events).toContain(NotificationEventType.MentionedInComment); }); @@ -199,13 +209,15 @@ describe("#notifications.list", () => { expect(body.data.notifications.length).toBe(2); expect(body.pagination.total).toBe(2); expect(body.data.unseen).toBe(2); - expect((randomElement(body.data.notifications) as any).actor.id).toBe( - actor.id + expect( + randomElement(body.data.notifications).actor.id + ).toBe(actor.id); + expect( + randomElement(body.data.notifications).userId + ).toBe(user.id); + const events = body.data.notifications.map( + (n: NotificationItem) => n.event ); - expect((randomElement(body.data.notifications) as any).userId).toBe( - user.id - ); - const events = body.data.notifications.map((n: any) => n.event); expect(events).toContain(NotificationEventType.CreateComment); expect(events).toContain(NotificationEventType.UpdateDocument); }); @@ -265,13 +277,15 @@ describe("#notifications.list", () => { expect(body.data.notifications.length).toBe(1); expect(body.pagination.total).toBe(1); expect(body.data.unseen).toBe(1); - expect((randomElement(body.data.notifications) as any).actor.id).toBe( - actor.id + expect( + randomElement(body.data.notifications).actor.id + ).toBe(actor.id); + expect( + randomElement(body.data.notifications).userId + ).toBe(user.id); + const events = body.data.notifications.map( + (n: NotificationItem) => n.event ); - expect((randomElement(body.data.notifications) as any).userId).toBe( - user.id - ); - const events = body.data.notifications.map((n: any) => n.event); expect(events).toContain(NotificationEventType.MentionedInComment); }); }); diff --git a/server/routes/api/pins/pins.test.ts b/server/routes/api/pins/pins.test.ts index cb48f4fd47..94ef783af0 100644 --- a/server/routes/api/pins/pins.test.ts +++ b/server/routes/api/pins/pins.test.ts @@ -302,10 +302,10 @@ describe("#pins.list", () => { expect(body.data).toBeTruthy(); expect(body.data.pins).toBeTruthy(); expect(body.data.pins).toHaveLength(2); - const pinIds = body.data.pins.map((p: any) => p.id); + const pinIds = body.data.pins.map((p: { id: string }) => p.id); expect(pinIds).toContain(pins[0].id); expect(pinIds).toContain(pins[1].id); - const docIds = body.data.documents.map((d: any) => d.id); + const docIds = body.data.documents.map((d: { id: string }) => d.id); expect(docIds).toContain(docs[0].id); expect(docIds).toContain(docs[1].id); }); diff --git a/server/routes/api/relationships/relationships.test.ts b/server/routes/api/relationships/relationships.test.ts index 79018917b0..a1207e703f 100644 --- a/server/routes/api/relationships/relationships.test.ts +++ b/server/routes/api/relationships/relationships.test.ts @@ -155,7 +155,9 @@ describe("#relationships.info", () => { expect(body.data.relationship).toBeTruthy(); expect(body.data.documents).toHaveLength(2); // User can read their own document but admin document should also be included - const documentIds = body.data.documents.map((doc: any) => doc.id); + const documentIds = body.data.documents.map( + (doc: { id: string }) => doc.id + ); expect(documentIds).toContain(userDocument.id); }); @@ -172,7 +174,9 @@ describe("#relationships.info", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.data.documents).toHaveLength(2); - const documentIds = body.data.documents.map((doc: any) => doc.id); + const documentIds = body.data.documents.map( + (doc: { id: string }) => doc.id + ); expect(documentIds).toContain(document.id); expect(documentIds).toContain(reverseDocument.id); }); @@ -267,7 +271,7 @@ describe("#relationships.list", () => { expect(body.data.relationships).toBeTruthy(); // All returned relationships should be backlinks - body.data.relationships.forEach((rel: any) => { + body.data.relationships.forEach((rel: { type: RelationshipType }) => { expect(rel.type).toEqual(RelationshipType.Backlink); }); }); @@ -285,7 +289,7 @@ describe("#relationships.list", () => { expect(body.data.relationships).toBeTruthy(); // All returned relationships should have the specified documentId - body.data.relationships.forEach((rel: any) => { + body.data.relationships.forEach((rel: { documentId: string }) => { expect(rel.documentId).toEqual(documents[0].id); }); }); @@ -303,7 +307,7 @@ describe("#relationships.list", () => { expect(body.data.relationships).toBeTruthy(); // All returned relationships should have the specified reverseDocumentId - body.data.relationships.forEach((rel: any) => { + body.data.relationships.forEach((rel: { reverseDocumentId: string }) => { expect(rel.reverseDocumentId).toEqual(documents[1].id); }); }); @@ -322,10 +326,12 @@ describe("#relationships.list", () => { expect(body.data.relationships).toBeTruthy(); // All returned relationships should match both filters - body.data.relationships.forEach((rel: any) => { - expect(rel.type).toEqual(RelationshipType.Backlink); - expect(rel.documentId).toEqual(documents[0].id); - }); + body.data.relationships.forEach( + (rel: { type: RelationshipType; documentId: string }) => { + expect(rel.type).toEqual(RelationshipType.Backlink); + expect(rel.documentId).toEqual(documents[0].id); + } + ); }); it("should fail with status 400 bad request when documentId is invalid", async () => { diff --git a/server/routes/api/searches/searches.test.ts b/server/routes/api/searches/searches.test.ts index 4532160a29..38a24484d1 100644 --- a/server/routes/api/searches/searches.test.ts +++ b/server/routes/api/searches/searches.test.ts @@ -39,7 +39,7 @@ describe("#searches.list", () => { const body = await res.json(); expect(res.status).toEqual(200); expect(body.data).toHaveLength(3); - const queries = body.data.map((d: any) => d.query); + const queries = body.data.map((d: { query: string }) => d.query); expect(queries).toContain("query"); expect(queries).toContain("foo"); expect(queries).toContain("bar"); diff --git a/server/test/TestServer.ts b/server/test/TestServer.ts index 55445ffb87..68072b77d7 100644 --- a/server/test/TestServer.ts +++ b/server/test/TestServer.ts @@ -3,13 +3,20 @@ import type { AddressInfo } from "node:net"; import type Koa from "koa"; // oxlint-disable-next-line no-restricted-imports import nodeFetch from "node-fetch"; +// oxlint-disable-next-line no-restricted-imports +import type { RequestInit } from "node-fetch"; + +type TestRequestOptions = Omit & { + body?: unknown; + headers?: Record; +}; class TestServer { private server: http.Server; private listener?: Promise | null; constructor(app: Koa) { - this.server = http.createServer(app.callback() as any); + this.server = http.createServer(app.callback() as http.RequestListener); } get address(): string { @@ -29,19 +36,23 @@ class TestServer { return this.listener; } - fetch(path: string, opts: any) { + fetch(path: string, opts: TestRequestOptions) { return this.listen().then(() => { const url = `${this.address}${path}`; - const options = Object.assign({ headers: {} }, opts); - const contentType = - options.headers["Content-Type"] ?? options.headers["content-type"]; + const headers: Record = { ...opts.headers }; + let body = opts.body; + const contentType = headers["Content-Type"] ?? headers["content-type"]; // automatic JSON encoding - if (!contentType && typeof options.body === "object") { - options.headers["Content-Type"] = "application/json"; - options.body = JSON.stringify(options.body); + if (!contentType && typeof body === "object" && body !== null) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(body); } - return nodeFetch(url, options); + return nodeFetch(url, { + ...opts, + headers, + body: body as string | undefined, + }); }); } @@ -51,31 +62,31 @@ class TestServer { this.server.close(); } - delete(path: string, options?: any) { + delete(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "DELETE" }); } - get(path: string, options?: any) { + get(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "GET" }); } - head(path: string, options?: any) { + head(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "HEAD" }); } - options(path: string, options?: any) { + options(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "OPTIONS" }); } - patch(path: string, options?: any) { + patch(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "PATCH" }); } - post(path: string, options?: any) { + post(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "POST" }); } - put(path: string, options?: any) { + put(path: string, options?: TestRequestOptions) { return this.fetch(path, { ...options, method: "PUT" }); } } diff --git a/server/utils/PluginManager.ts b/server/utils/PluginManager.ts index b39de18478..d9091d328f 100644 --- a/server/utils/PluginManager.ts +++ b/server/utils/PluginManager.ts @@ -44,11 +44,12 @@ export enum Hook { type PluginValueMap = { [Hook.API]: Router; [Hook.AuthProvider]: { router: Router | Promise; id: string }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- typeof BaseEmail isn't assignable from BaseEmail; plugins register heterogeneous template Props. [Hook.EmailTemplate]: typeof BaseEmail; [Hook.IssueProvider]: BaseIssueProvider; [Hook.Processor]: typeof BaseProcessor; [Hook.SearchProvider]: BaseSearchProvider; - [Hook.Task]: typeof BaseTask; + [Hook.Task]: typeof BaseTask; [Hook.Uninstall]: UninstallSignature; [Hook.UnfurlProvider]: { unfurl: UnfurlSignature; cacheExpiry: number }; [Hook.GroupSyncProvider]: { id: string; provider: GroupSyncProvider }; diff --git a/server/utils/decorators/Public.ts b/server/utils/decorators/Public.ts index 10db2b3844..3f61f59719 100644 --- a/server/utils/decorators/Public.ts +++ b/server/utils/decorators/Public.ts @@ -7,7 +7,7 @@ const key = Symbol("env:public"); /** * This decorator on an environment variable makes that variable available client-side */ -export function Public(target: any, propertyKey: string) { +export function Public(target: object, propertyKey: string) { const publicVars: string[] = Reflect.getMetadata(key, target); if (!publicVars) { @@ -18,6 +18,7 @@ export function Public(target: any, propertyKey: string) { } export class PublicEnvironmentRegister { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- consumed at runtime as a flat map of typed Environment values; tightening to unknown breaks call sites. private static publicEnv: Record = {}; static registerEnv(env: Environment) { diff --git a/server/utils/validators.test.ts b/server/utils/validators.test.ts index 77112ca758..841cb0f5b7 100644 --- a/server/utils/validators.test.ts +++ b/server/utils/validators.test.ts @@ -135,11 +135,15 @@ describe("isDatabaseUrl", () => { }); it("should reject null", () => { - expect(isDatabaseUrl(null as any, defaultOptions)).toBe(false); + expect(isDatabaseUrl(null as unknown as string, defaultOptions)).toBe( + false + ); }); it("should reject undefined", () => { - expect(isDatabaseUrl(undefined as any, defaultOptions)).toBe(false); + expect( + isDatabaseUrl(undefined as unknown as string, defaultOptions) + ).toBe(false); }); it("should reject URL with invalid protocol", () => { @@ -279,9 +283,7 @@ describe("isMailboxAddress", () => { }); it("should accept a mailbox format with quoted display name containing a comma", () => { - expect( - isMailboxAddress('"Company, Inc." ') - ).toBe(true); + expect(isMailboxAddress('"Company, Inc." ')).toBe(true); }); it("should accept a mailbox format with a quoted display name", () => {