mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
chore: enable typescript/no-misused-spread lint rule
Replace spread operations on class instances and iterables with safer patterns (Object.fromEntries, Object.assign, explicit field copies, or toJSON()) to preserve prototypes and avoid silently dropping data. Notable changes: - Sequelize \`where\` clauses now use \`Op.and\` instead of spreading WhereOptions (which may be a Literal/Fn instance). - HeadersInit values are funneled through a Headers instance before use rather than being spread directly. - Model.toJSON() is used to convert MobX model instances into plain objects before re-spreading. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
{
|
||||
|
||||
@@ -411,10 +411,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
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" }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -57,6 +57,7 @@ if (element) {
|
||||
const App = () => (
|
||||
<StrictMode>
|
||||
<HelmetProvider>
|
||||
{/* oxlint-disable-next-line typescript/no-misused-spread */}
|
||||
<Provider {...stores}>
|
||||
<Analytics>
|
||||
<Router history={history}>
|
||||
|
||||
@@ -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<AuthenticationProvider> & {
|
||||
id: string;
|
||||
};
|
||||
return (model.store as AuthenticationProvidersStore).add({
|
||||
...model,
|
||||
...data,
|
||||
isEnabled: false,
|
||||
isConnected: false,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = <T = any>(
|
||||
path: string,
|
||||
data?: JSONObject | FormData | undefined,
|
||||
data?: JSONObject | FormData ,
|
||||
options?: FetchOptions
|
||||
): Promise<T> => {
|
||||
if (data instanceof FormData) {
|
||||
|
||||
@@ -923,10 +923,9 @@ class Document extends ArchivableModel<
|
||||
collaborators = async (options?: FindOptions<User>): Promise<User[]> =>
|
||||
await User.findAll({
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
id: this.collaboratorIds,
|
||||
},
|
||||
where: options?.where
|
||||
? { [Op.and]: [options.where, { id: this.collaboratorIds }] }
|
||||
: { id: this.collaboratorIds },
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,11 +169,15 @@ class FileOperation extends ParanoidModel<
|
||||
): Promise<number> {
|
||||
return this.count({
|
||||
where: {
|
||||
teamId,
|
||||
createdAt: {
|
||||
[Op.gt]: startDate,
|
||||
},
|
||||
...where,
|
||||
[Op.and]: [
|
||||
{
|
||||
teamId,
|
||||
createdAt: {
|
||||
[Op.gt]: startDate,
|
||||
},
|
||||
},
|
||||
where,
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<M>,
|
||||
Object.assign(
|
||||
{},
|
||||
options.defaults,
|
||||
options.where
|
||||
) as CreationAttributes<M>,
|
||||
{ ...hookContext, transaction }
|
||||
);
|
||||
return [created as M, true];
|
||||
|
||||
@@ -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<Props> {
|
||||
protected getPartitionWhereClause(
|
||||
idField: string,
|
||||
partitionInfo: PartitionInfo | undefined
|
||||
): WhereOptions {
|
||||
): Record<string, unknown> {
|
||||
if (!partitionInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -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<string, string | string[]> = {
|
||||
"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, {
|
||||
|
||||
+13
-6
@@ -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 }),
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
|
||||
@@ -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<string, NodeSpec> = this.extensions
|
||||
.filter((extension) => extension.type === "node")
|
||||
.reduce(
|
||||
(memo, node: Node) => ({
|
||||
...memo,
|
||||
[node.name]: node.schema,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const nodes: Record<string, NodeSpec> = 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<string, MarkSpec> = this.extensions
|
||||
.filter((extension) => extension.type === "mark")
|
||||
.reduce(
|
||||
(memo, mark: Mark) => ({
|
||||
...memo,
|
||||
[mark.name]: mark.schema,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const marks: Record<string, MarkSpec> = 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<string, ParseSpec> = {};
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user