diff --git a/.oxlintrc.json b/.oxlintrc.json index 7f2d7660d6..d5e24afd1b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -87,6 +87,7 @@ "import/no-named-as-default": "off", "import/no-named-as-default-member": "off", "typescript/consistent-type-imports": "error", + "typescript/no-misused-spread": "error", "no-unused-vars": [ "error", { diff --git a/app/editor/components/SuggestionsMenu.tsx b/app/editor/components/SuggestionsMenu.tsx index 2f812cc642..b7081624a9 100644 --- a/app/editor/components/SuggestionsMenu.tsx +++ b/app/editor/components/SuggestionsMenu.tsx @@ -411,10 +411,7 @@ function SuggestionsMenu(props: Props) { for (const embed of embeds) { if (embed.title && embed.visible !== false && !embed.disabled) { embedItems.push( - new EmbedDescriptor({ - ...embed, - name: "embed", - }) + new EmbedDescriptor(Object.assign({}, embed, { name: "embed" })) ); } } diff --git a/app/editor/index.tsx b/app/editor/index.tsx index 563cac22f9..68d0f8d60b 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -369,12 +369,12 @@ export class Editor extends React.PureComponent< } private createNodeViews() { - return this.extensions.extensions - .filter((extension: ReactNode) => extension.component) - .reduce( - (nodeViews, extension: ReactNode) => ({ - ...nodeViews, - [extension.name]: ( + return Object.fromEntries( + this.extensions.extensions + .filter((extension: ReactNode) => extension.component) + .map((extension: ReactNode) => [ + extension.name, + ( node: ProsemirrorNode, view: EditorView, getPos: () => number, @@ -388,9 +388,8 @@ export class Editor extends React.PureComponent< getPos, decorations, }), - }), - {} - ); + ]) + ) as { [name: string]: NodeViewConstructor }; } private createCommands() { diff --git a/app/index.tsx b/app/index.tsx index f8104b3d36..13b4766a52 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -57,6 +57,7 @@ if (element) { const App = () => ( + {/* oxlint-disable-next-line typescript/no-misused-spread */} diff --git a/app/models/AuthenticationProvider.ts b/app/models/AuthenticationProvider.ts index cdc68ec6c5..b3c52c6ac9 100644 --- a/app/models/AuthenticationProvider.ts +++ b/app/models/AuthenticationProvider.ts @@ -37,8 +37,11 @@ class AuthenticationProvider extends Model { @AfterDelete static afterDelete(model: AuthenticationProvider) { // Restore a placeholder record to allow re-connection + const data = model.toJSON() as Partial & { + id: string; + }; return (model.store as AuthenticationProvidersStore).add({ - ...model, + ...data, isEnabled: false, isConnected: false, }); diff --git a/app/models/Document.ts b/app/models/Document.ts index ef361602de..8cd7fde376 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -570,7 +570,7 @@ export default class Document extends ArchivableModel implements Searchable { ); // if saving is successful set the new values on the model itself - set(this, { ...params, ...model }); + set(this, { ...params, ...model.toJSON() }); this.persistedAttributes = this.toAPI(); diff --git a/app/models/base/Model.ts b/app/models/base/Model.ts index 67f80f2037..005af4463b 100644 --- a/app/models/base/Model.ts +++ b/app/models/base/Model.ts @@ -120,7 +120,7 @@ export default abstract class Model { ); // if saving is successful set the new values on the model itself - this.updateData({ ...params, ...model }); + this.updateData({ ...params, ...model.toJSON() }); if (isNew) { LifecycleManager.executeHooks(this.constructor, "afterCreate", this); diff --git a/app/utils/ApiClient.ts b/app/utils/ApiClient.ts index 45e1b4fb35..55522f5d7d 100644 --- a/app/utils/ApiClient.ts +++ b/app/utils/ApiClient.ts @@ -70,10 +70,14 @@ class ApiClient { if (this.shareId) { // add to data - data = { - ...(data || {}), - shareId: this.shareId, - }; + if (data instanceof FormData) { + data.append("shareId", this.shareId); + } else { + data = { + ...(data || {}), + shareId: this.shareId, + }; + } } if (method === "GET") { @@ -285,7 +289,7 @@ class ApiClient { post = ( path: string, - data?: JSONObject | FormData | undefined, + data?: JSONObject | FormData , options?: FetchOptions ): Promise => { if (data instanceof FormData) { diff --git a/server/models/Document.ts b/server/models/Document.ts index eacaa28f2b..86e7f9e1fc 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -923,10 +923,9 @@ class Document extends ArchivableModel< collaborators = async (options?: FindOptions): Promise => await User.findAll({ ...options, - where: { - ...options?.where, - id: this.collaboratorIds, - }, + where: options?.where + ? { [Op.and]: [options.where, { id: this.collaboratorIds }] } + : { id: this.collaboratorIds }, }); /** diff --git a/server/models/FileOperation.ts b/server/models/FileOperation.ts index 1431143092..4a980b275b 100644 --- a/server/models/FileOperation.ts +++ b/server/models/FileOperation.ts @@ -169,11 +169,15 @@ class FileOperation extends ParanoidModel< ): Promise { return this.count({ where: { - teamId, - createdAt: { - [Op.gt]: startDate, - }, - ...where, + [Op.and]: [ + { + teamId, + createdAt: { + [Op.gt]: startDate, + }, + }, + where, + ], }, }); } diff --git a/server/models/Team.ts b/server/models/Team.ts index cb1fb192e1..79e8306d5a 100644 --- a/server/models/Team.ts +++ b/server/models/Team.ts @@ -560,7 +560,9 @@ class Team extends ParanoidModel< return this.findOne({ ...options, - where: { ...options?.where, domain: normalized }, + where: options?.where + ? { [Op.and]: [options.where, { domain: normalized }] } + : { domain: normalized }, }); } diff --git a/server/models/base/Model.ts b/server/models/base/Model.ts index 608ab944ac..b25faa52a1 100644 --- a/server/models/base/Model.ts +++ b/server/models/base/Model.ts @@ -159,7 +159,11 @@ class Model< // Record not found, try to create it try { const created = await this.create( - { ...options.defaults, ...options.where } as CreationAttributes, + Object.assign( + {}, + options.defaults, + options.where + ) as CreationAttributes, { ...hookContext, transaction } ); return [created as M, true]; diff --git a/server/queues/tasks/base/CronTask.ts b/server/queues/tasks/base/CronTask.ts index 635f59c8f9..71d421c498 100644 --- a/server/queues/tasks/base/CronTask.ts +++ b/server/queues/tasks/base/CronTask.ts @@ -1,4 +1,3 @@ -import type { WhereOptions } from "sequelize"; import { Op } from "sequelize"; import { Minute } from "@shared/utils/time"; import { BaseTask } from "./BaseTask"; @@ -178,7 +177,7 @@ export abstract class CronTask extends BaseTask { protected getPartitionWhereClause( idField: string, partitionInfo: PartitionInfo | undefined - ): WhereOptions { + ): Record { if (!partitionInfo) { return {}; } diff --git a/server/storage/files/BaseStorage.ts b/server/storage/files/BaseStorage.ts index ae4fc02c18..7bcba4db82 100644 --- a/server/storage/files/BaseStorage.ts +++ b/server/storage/files/BaseStorage.ts @@ -7,8 +7,8 @@ import { isBase64Url, isInternalUrl } from "@shared/utils/urls"; import { Week } from "@shared/utils/time"; import env from "@server/env"; import Logger from "@server/logging/Logger"; -import type { RequestInit } from "@server/utils/fetch"; -import fetch, { chromeUserAgent } from "@server/utils/fetch"; +import type { HeadersInit, RequestInit } from "@server/utils/fetch"; +import fetch, { Headers, chromeUserAgent } from "@server/utils/fetch"; import type { AppContext } from "@server/types"; export default abstract class BaseStorage { @@ -189,10 +189,16 @@ export default abstract class BaseStorage { } } else { try { - const headers = { + const headers: Record = { "User-Agent": chromeUserAgent, - ...init?.headers, }; + if (init?.headers) { + for (const [name, value] of new Headers( + init.headers as HeadersInit + ).entries()) { + headers[name] = value; + } + } const initWithoutHeaders = omit(init, ["headers"]); const res = await fetch(url, { diff --git a/server/utils/fetch.ts b/server/utils/fetch.ts index 206db26113..661eb2ec33 100644 --- a/server/utils/fetch.ts +++ b/server/utils/fetch.ts @@ -1,7 +1,11 @@ /* oxlint-disable no-restricted-imports, react/rules-of-hooks */ import type http from "node:http"; import type https from "node:https"; -import nodeFetch, { type RequestInit, type Response } from "node-fetch"; +import nodeFetch, { + Headers, + type RequestInit, + type Response, +} from "node-fetch"; import { getProxyForUrl } from "proxy-from-env"; import tunnelAgent, { type TunnelAgent } from "tunnel-agent"; import { useAgent as useFilteringAgent } from "request-filtering-agent"; @@ -23,7 +27,8 @@ const DefaultOptions = { maxCachedSessions: 500, }; -export type { RequestInit } from "node-fetch"; +export type { HeadersInit, RequestInit } from "node-fetch"; +export { Headers } from "node-fetch"; /** * Default user agent string for outgoing requests. @@ -72,13 +77,15 @@ export default async function fetch( const signal = abortController?.signal || rest.signal; + const headers = new Headers(rest?.headers); + if (!headers.has("User-Agent")) { + headers.set("User-Agent", outlineUserAgent); + } + try { const response = await nodeFetch(url, { ...rest, - headers: { - "User-Agent": outlineUserAgent, - ...rest?.headers, - }, + headers, signal, agent: buildAgent(url, { signal, allowPrivateIPAddress }), }); diff --git a/shared/editor/lib/ChangesetHelper.ts b/shared/editor/lib/ChangesetHelper.ts index b3542f3aae..14d2c94940 100644 --- a/shared/editor/lib/ChangesetHelper.ts +++ b/shared/editor/lib/ChangesetHelper.ts @@ -253,7 +253,10 @@ export class ChangesetHelper { } return { - ...change, + fromA: change.fromA, + toA: change.toA, + fromB: change.fromB, + toB: change.toB, deleted: change.deleted.filter( (_, index) => !matchedDeletionIndices.has(index) ), diff --git a/shared/editor/lib/ExtensionManager.ts b/shared/editor/lib/ExtensionManager.ts index caf3e7f475..68deb55c38 100644 --- a/shared/editor/lib/ExtensionManager.ts +++ b/shared/editor/lib/ExtensionManager.ts @@ -1,7 +1,7 @@ import type { Options, PluginSimple } from "markdown-it"; import { observer } from "mobx-react"; import { keymap } from "prosemirror-keymap"; -import { MarkdownParser } from "prosemirror-markdown"; +import { MarkdownParser, type ParseSpec } from "prosemirror-markdown"; import type { MarkSpec, NodeSpec, Schema } from "prosemirror-model"; import type { EditorView } from "prosemirror-view"; import type { Primitive } from "utility-types"; @@ -62,27 +62,21 @@ export default class ExtensionManager { } get widgets() { - return this.extensions - .filter((extension) => extension.widget({ rtl: false, readOnly: false })) - .reduce( - (memo, node: Node) => ({ - ...memo, - [node.name]: observer(node.widget as any), - }), - {} - ); + return Object.fromEntries( + this.extensions + .filter((extension) => + extension.widget({ rtl: false, readOnly: false }) + ) + .map((node: Node) => [node.name, observer(node.widget as any)]) + ); } get nodes() { - const nodes: Record = this.extensions - .filter((extension) => extension.type === "node") - .reduce( - (memo, node: Node) => ({ - ...memo, - [node.name]: node.schema, - }), - {} - ); + const nodes: Record = Object.fromEntries( + this.extensions + .filter((extension) => extension.type === "node") + .map((node: Node) => [node.name, node.schema]) + ); for (const i in nodes) { const { marks } = nodes[i]; @@ -100,15 +94,11 @@ export default class ExtensionManager { } get marks() { - const marks: Record = this.extensions - .filter((extension) => extension.type === "mark") - .reduce( - (memo, mark: Mark) => ({ - ...memo, - [mark.name]: mark.schema, - }), - {} - ); + const marks: Record = Object.fromEntries( + this.extensions + .filter((extension) => extension.type === "mark") + .map((mark: Mark) => [mark.name, mark.schema]) + ); for (const i in marks) { const { excludes } = marks[i]; @@ -126,25 +116,17 @@ export default class ExtensionManager { } serializer() { - const nodes = this.extensions - .filter((extension) => extension.type === "node") - .reduce( - (memo, extension: Node) => ({ - ...memo, - [extension.name]: extension.toMarkdown, - }), - {} - ); + const nodes = Object.fromEntries( + this.extensions + .filter((extension) => extension.type === "node") + .map((extension: Node) => [extension.name, extension.toMarkdown]) + ); - const marks = this.extensions - .filter((extension) => extension.type === "mark") - .reduce( - (memo, extension: Mark) => ({ - ...memo, - [extension.name]: extension.toMarkdown, - }), - {} - ); + const marks = Object.fromEntries( + this.extensions + .filter((extension) => extension.type === "mark") + .map((extension: Mark) => [extension.name, extension.toMarkdown]) + ); return new MarkdownSerializer(nodes, marks); } @@ -158,21 +140,18 @@ export default class ExtensionManager { rules?: Options; plugins?: PluginSimple[]; }): MarkdownParser { - const tokens = this.extensions - .filter( - (extension) => extension.type === "mark" || extension.type === "node" - ) - .reduce((nodes, extension: Node | Mark) => { - const parseSpec = extension.parseMarkdown(); - if (!parseSpec) { - return nodes; - } - - return { - ...nodes, - [extension.markdownToken || extension.name]: parseSpec, - }; - }, {}); + const tokens: Record = {}; + for (const extension of this.extensions) { + if (extension.type !== "mark" && extension.type !== "node") { + continue; + } + const node = extension as Node | Mark; + const parseSpec = node.parseMarkdown(); + if (!parseSpec) { + continue; + } + tokens[node.markdownToken || node.name] = parseSpec; + } return new MarkdownParser( schema,