fix: Filter relationships returned from list endpoint (#11738)

* fix: Filter relationships returned from list endpoint

* fix: BacklinksProcessor does not check teamId

* Port from upstream
This commit is contained in:
Tom Moor
2026-03-12 22:09:31 -04:00
committed by GitHub
parent 88f7ef9d03
commit ea4fbdb7bb
8 changed files with 108 additions and 32 deletions
@@ -61,7 +61,7 @@ function Overview({ collection, readOnly }: Props) {
() => ({
padding: "0 32px",
margin: "0 -32px",
paddingBottom: `calc(50vh - ${childOffsetHeight}px)`,
paddingBottom: `calc(30vh - ${childOffsetHeight}px)`,
}),
[childOffsetHeight]
);
+1 -1
View File
@@ -172,7 +172,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
() => ({
padding: "0 32px",
margin: "0 -32px",
paddingBottom: `calc(50vh - ${childOffsetHeight}px)`,
paddingBottom: `calc(30vh - ${childOffsetHeight}px)`,
}),
[childOffsetHeight]
);
@@ -30,7 +30,7 @@ function References({ document }: Props) {
useEffect(() => {
if (!isShare) {
void documents.fetchBacklinks(document.id);
void documents.fetchRelationships(document.id);
}
}, [isShare, documents, document.id]);
+32 -9
View File
@@ -53,6 +53,9 @@ export default class DocumentsStore extends Store<Document> {
@observable
backlinks: Map<string, string[]> = new Map();
@observable
similar: Map<string, string[]> = new Map();
@observable
movingDocumentId: string | null | undefined;
@@ -254,16 +257,27 @@ export default class DocumentsStore extends Store<Document> {
}
@action
fetchBacklinks = async (documentId: string): Promise<void> => {
const documents = await this.fetchAll({
backlinkDocumentId: documentId,
});
fetchRelationships = async (documentId: string): Promise<void> => {
const res = await client.post("/relationships.list", { documentId });
invariant(res?.data, "Relationships not available");
runInAction("DocumentsStore#fetchBacklinks", () => {
this.backlinks.set(
documentId,
documents.map((doc) => doc.id)
);
runInAction("DocumentsStore#fetchRelationships", () => {
res.data.documents.forEach(this.add);
this.addPolicies(res.policies);
const backlinkIds: string[] = [];
const similarIds: string[] = [];
for (const relationship of res.data.relationships) {
if (relationship.type === "backlink") {
backlinkIds.push(relationship.reverseDocumentId);
} else if (relationship.type === "similar") {
similarIds.push(relationship.reverseDocumentId);
}
}
this.backlinks.set(documentId, backlinkIds);
this.similar.set(documentId, similarIds);
});
};
@@ -276,6 +290,15 @@ export default class DocumentsStore extends Store<Document> {
);
}
getSimilarDocuments(documentId: string): Document[] {
const documentIds = this.similar.get(documentId) || [];
return orderBy(
compact(documentIds.map((id) => this.data.get(id))),
"title",
"asc"
);
}
@action
fetchChildDocuments = async (documentId: string): Promise<void> => {
const res = await client.post(`/documents.list`, {
@@ -1,7 +1,7 @@
import { parser } from "@server/editor";
import { Relationship } from "@server/models";
import { RelationshipType } from "@server/models/Relationship";
import { buildDocument } from "@server/test/factories";
import { buildDocument, buildTeam } from "@server/test/factories";
import BacklinksProcessor from "./BacklinksProcessor";
@@ -9,8 +9,10 @@ const ip = "127.0.0.1";
describe("documents.publish", () => {
it("should create new backlink records", async () => {
const otherDocument = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
text: `[this is a link](${otherDocument.url})`,
});
@@ -33,9 +35,11 @@ describe("documents.publish", () => {
});
it("should not fail when linked document is destroyed", async () => {
const otherDocument = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
await otherDocument.destroy();
const document = await buildDocument({
teamId: team.id,
version: 0,
text: `[ ] checklist item`,
});
@@ -61,12 +65,41 @@ describe("documents.publish", () => {
});
expect(backlinks.length).toBe(0);
});
it("should not create backlink records for cross-team links", async () => {
const teamA = await buildTeam();
const teamB = await buildTeam();
const otherDocument = await buildDocument({ teamId: teamB.id });
const document = await buildDocument({
teamId: teamA.id,
text: `[this is a link](${otherDocument.url})`,
});
const processor = new BacklinksProcessor();
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
ip,
});
const backlinks = await Relationship.findAll({
where: {
reverseDocumentId: document.id,
type: RelationshipType.Backlink,
},
});
expect(backlinks.length).toBe(0);
});
});
describe("documents.update", () => {
it("should not fail on a document with no previous revisions", async () => {
const otherDocument = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
text: `[this is a link](${otherDocument.url})`,
});
@@ -91,8 +124,10 @@ describe("documents.update", () => {
});
it("should not fail when previous revision is different document version", async () => {
const otherDocument = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
version: undefined,
text: `[ ] checklist item`,
});
@@ -122,8 +157,9 @@ describe("documents.update", () => {
});
it("should create new backlink records", async () => {
const otherDocument = await buildDocument();
const document = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({ teamId: team.id });
document.content = parser
.parse(`[this is a link](${otherDocument.url})`)
?.toJSON();
@@ -150,9 +186,11 @@ describe("documents.update", () => {
});
it("should destroy removed backlink records", async () => {
const otherDocument = await buildDocument();
const yetAnotherDocument = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const yetAnotherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
text: `[this is a link](${otherDocument.url})
[this is a another link](${yetAnotherDocument.url})`,
@@ -199,8 +237,9 @@ describe("documents.update", () => {
describe("documents.delete", () => {
it("should destroy related backlinks", async () => {
const otherDocument = await buildDocument();
const document = await buildDocument();
const team = await buildTeam();
const otherDocument = await buildDocument({ teamId: team.id });
const document = await buildDocument({ teamId: team.id });
document.content = parser
.parse(`[this is a link](${otherDocument.url})`)
?.toJSON();
+12 -4
View File
@@ -26,10 +26,14 @@ export default class BacklinksProcessor extends BaseProcessor {
await Promise.all(
linkIds.map(async (linkId) => {
const linkedDocument = await Document.findByPk(linkId, {
attributes: ["id"],
attributes: ["id", "teamId"],
});
if (!linkedDocument || linkedDocument.id === event.documentId) {
if (
!linkedDocument ||
linkedDocument.id === event.documentId ||
linkedDocument.teamId !== document.teamId
) {
return;
}
@@ -72,10 +76,14 @@ export default class BacklinksProcessor extends BaseProcessor {
await Promise.all(
linkIds.map(async (linkId) => {
const linkedDocument = await Document.findByPk(linkId, {
attributes: ["id"],
attributes: ["id", "teamId"],
});
if (!linkedDocument || linkedDocument.id === event.documentId) {
if (
!linkedDocument ||
linkedDocument.id === event.documentId ||
linkedDocument.teamId !== document.teamId
) {
return;
}
@@ -414,7 +414,7 @@ describe("#relationships.list", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.relationships).toHaveLength(1);
expect(body.data.relationships).toHaveLength(0);
expect(body.data.documents).toHaveLength(0);
});
});
@@ -77,16 +77,22 @@ router.post(
{ userId: user.id }
);
const policies = presentPolicies(user, [...documents, ...relationships]);
const documentIds = new Set(documents.map((d) => d.id));
const filteredRelationships = relationships.filter((relationship) =>
documentIds.has(
where.reverseDocumentId
? relationship.documentId
: relationship.reverseDocumentId
)
);
ctx.body = {
pagination: ctx.state.pagination,
data: {
relationships: relationships.map(presentRelationship),
relationships: filteredRelationships.map(presentRelationship),
documents: await presentDocuments(ctx, documents),
policies: presentPolicies(user, documents),
},
policies,
policies: presentPolicies(user, [...documents, ...filteredRelationships]),
};
}
);