From c3f93a3e9dd35f9df8c077667e038f7060e31d47 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:24:01 -0400 Subject: [PATCH] Add relationships API endpoints (#9402) * Migrate Backlink model to generic Relationship model - Create new Relationship model with type field to support different relationship types - Add database migration to create relationships table and migrate existing backlinks - Update Backlink model to delegate to Relationship model for backward compatibility - Update BacklinksProcessor to use Relationship model with backlink type - Update API routes to use new Relationship model - Update test files to use Relationship model - Maintain backward compatibility through database view and model delegation Fixes #9366 * Update migration to rename table instead of creating new one - Rename existing backlinks table to relationships instead of creating new table - Add type column with default value to existing table - Update existing rows to have type='backlink' - Avoid expensive data migration by keeping existing data in place - Maintain backward compatibility with database view - Update rollback to reverse table rename and column addition This approach is much more efficient for large datasets as it avoids copying millions of rows. * Remove unnecessary UPDATE statement from migration The UPDATE statement is not needed since defaultValue automatically applies to existing rows when adding a column with a default value. Thanks @tommoor for catching this! * Wrap up migration in transaction - Wrap all migration operations in a transaction for atomicity - Add transaction parameter to all queryInterface calls - Follow the same pattern as other migrations in the codebase - Ensures all operations succeed or fail together * Remove Backlink class entirely and use Relationship everywhere - Delete server/models/Backlink.ts - Remove Backlink export from server/models/index.ts - Remove Backlink import and association from Document model - All functionality now uses Relationship model with RelationshipType.Backlink - Maintains same API through Relationship model methods - Cleaner architecture with single relationship model * Update documents.test.ts to use RelationshipType enum instead of string - Import RelationshipType from Relationship model - Replace type: "backlink" with type: RelationshipType.Backlink - Improves type safety and consistency with enum usage * Address code review feedback - Add transaction wrapper to migration down method for safer rollback - Remove unused findByTypeForUser method from Relationship model - Method wasn't used and won't work for all relationship types (e.g., user mentions) - Clean up code structure and improve safety * Restore imports * Add relationships API endpoints - Create relationships API following stars pattern - Add CRUD operations: create, list, delete - Include proper validation, authentication, and authorization - Support filtering by relationship type and document IDs - Add relationship presenter and policies - Register routes in main API router * Remove relationships.create and relationships.delete endpoints - Keep only relationships.list endpoint as requested - Remove create and delete schemas from validation - Update policies to only allow read operations - Relationships will be managed internally, not via external API * Add relationships.info endpoint - Use Document.findByPk for authorization as requested - Find relationship by ID and verify user has access to related document - Return relationship details with accessible documents - Include proper validation schema for UUID parameter * Update 20250601223331-migrate-backlink-to-relationship.js * Update Relationship.ts * wip * test * Final tweaks --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com> Co-authored-by: Tom Moor --- .github/workflows/ci.yml | 17 +- ...223331-migrate-backlink-to-relationship.js | 2 +- server/models/Relationship.ts | 2 + server/presenters/index.ts | 2 + server/presenters/relationship.ts | 13 + server/routes/api/index.ts | 2 + server/routes/api/relationships/index.ts | 1 + .../api/relationships/relationships.test.ts | 418 ++++++++++++++++++ .../routes/api/relationships/relationships.ts | 98 ++++ server/routes/api/relationships/schema.ts | 34 ++ server/test/factories.ts | 28 ++ yarn.lock | 2 +- 12 files changed, 608 insertions(+), 11 deletions(-) create mode 100644 server/presenters/relationship.ts create mode 100644 server/routes/api/relationships/index.ts create mode 100644 server/routes/api/relationships/relationships.test.ts create mode 100644 server/routes/api/relationships/relationships.ts create mode 100644 server/routes/api/relationships/schema.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a0b84151a..b5580bb51d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,15 +25,14 @@ jobs: node-version: [20.x, 22.x] steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'yarn' - - - name: Install dependencies - run: yarn install --frozen-lockfile --prefer-offline + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "yarn" + - name: Install dependencies + run: yarn install --frozen-lockfile --prefer-offline lint: needs: build diff --git a/server/migrations/20250601223331-migrate-backlink-to-relationship.js b/server/migrations/20250601223331-migrate-backlink-to-relationship.js index 1216e28814..05f52afc0c 100644 --- a/server/migrations/20250601223331-migrate-backlink-to-relationship.js +++ b/server/migrations/20250601223331-migrate-backlink-to-relationship.js @@ -14,7 +14,7 @@ module.exports = { "relationships", "type", { - type: Sequelize.ENUM("backlink"), + type: Sequelize.ENUM("backlink", "similar"), allowNull: false, defaultValue: "backlink", }, diff --git a/server/models/Relationship.ts b/server/models/Relationship.ts index 17346f18c5..f0b79e2791 100644 --- a/server/models/Relationship.ts +++ b/server/models/Relationship.ts @@ -13,6 +13,7 @@ import Fix from "./decorators/Fix"; export enum RelationshipType { Backlink = "backlink", + Similar = "similar", } @Table({ tableName: "relationships", modelName: "relationship" }) @@ -54,6 +55,7 @@ class Relationship extends IdModel< * * @param documentId The document ID to find backlinks for * @param user The user to check access for + * @deprecated */ public static async findSourceDocumentIdsForUser( documentId: string, diff --git a/server/presenters/index.ts b/server/presenters/index.ts index f3fb962ce9..eae074b9cd 100644 --- a/server/presenters/index.ts +++ b/server/presenters/index.ts @@ -19,6 +19,7 @@ import presentPolicies from "./policy"; import presentProviderConfig from "./providerConfig"; import presentPublicTeam from "./publicTeam"; import presentReaction from "./reaction"; +import presentRelationship from "./relationship"; import presentRevision from "./revision"; import presentSearchQuery from "./searchQuery"; import presentShare from "./share"; @@ -51,6 +52,7 @@ export { presentPolicies, presentProviderConfig, presentReaction, + presentRelationship, presentRevision, presentSearchQuery, presentShare, diff --git a/server/presenters/relationship.ts b/server/presenters/relationship.ts new file mode 100644 index 0000000000..32bae232dc --- /dev/null +++ b/server/presenters/relationship.ts @@ -0,0 +1,13 @@ +import { Relationship } from "@server/models"; + +export default function presentRelationship(relationship: Relationship) { + return { + id: relationship.id, + type: relationship.type, + documentId: relationship.documentId, + reverseDocumentId: relationship.reverseDocumentId, + userId: relationship.userId, + createdAt: relationship.createdAt, + updatedAt: relationship.updatedAt, + }; +} diff --git a/server/routes/api/index.ts b/server/routes/api/index.ts index 583e23267b..51cd199c27 100644 --- a/server/routes/api/index.ts +++ b/server/routes/api/index.ts @@ -32,6 +32,7 @@ import oauthAuthentications from "./oauthAuthentications"; import oauthClients from "./oauthClients"; import pins from "./pins"; import reactions from "./reactions"; +import relationships from "./relationships"; import revisions from "./revisions"; import searches from "./searches"; import shares from "./shares"; @@ -102,6 +103,7 @@ router.use("/", fileOperationsRoute.routes()); router.use("/", urls.routes()); router.use("/", userMemberships.routes()); router.use("/", reactions.routes()); +router.use("/", relationships.routes()); router.use("/", imports.routes()); if (!env.isCloudHosted) { diff --git a/server/routes/api/relationships/index.ts b/server/routes/api/relationships/index.ts new file mode 100644 index 0000000000..2ea2e14587 --- /dev/null +++ b/server/routes/api/relationships/index.ts @@ -0,0 +1 @@ +export { default } from "./relationships"; diff --git a/server/routes/api/relationships/relationships.test.ts b/server/routes/api/relationships/relationships.test.ts new file mode 100644 index 0000000000..ef4c8fe0f0 --- /dev/null +++ b/server/routes/api/relationships/relationships.test.ts @@ -0,0 +1,418 @@ +import { CollectionPermission } from "@shared/types"; +import { Document, Relationship, User } from "@server/models"; +import { RelationshipType } from "@server/models/Relationship"; +import { + buildAdmin, + buildCollection, + buildDocument, + buildRelationship, + buildUser, +} from "@server/test/factories"; +import { getTestServer } from "@server/test/support"; + +const server = getTestServer(); + +describe("#relationships.info", () => { + let admin: User; + let user: User; + let anotherUser: User; + let document: Document; + let reverseDocument: Document; + let relationship: Relationship; + + beforeEach(async () => { + admin = await buildAdmin(); + [user, anotherUser] = await Promise.all([ + buildUser({ teamId: admin.teamId }), + buildUser(), + ]); + + document = await buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }); + + reverseDocument = await buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }); + + relationship = await buildRelationship({ + userId: admin.id, + documentId: document.id, + reverseDocumentId: reverseDocument.id, + type: RelationshipType.Backlink, + }); + }); + + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/relationships.info", { + body: { + id: relationship.id, + }, + }); + expect(res.status).toEqual(401); + }); + + it("should fail with status 400 bad request when id is not supplied", async () => { + const res = await server.post("/api/relationships.info", { + body: { + token: user.getJwtToken(), + }, + }); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toEqual("id: Required"); + }); + + it("should fail with status 400 bad request when id is not a valid UUID", async () => { + const res = await server.post("/api/relationships.info", { + body: { + token: user.getJwtToken(), + id: "invalid-uuid", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toEqual("id: Invalid uuid"); + }); + + it("should fail with status 404 not found when relationship does not exist", async () => { + const res = await server.post("/api/relationships.info", { + body: { + token: admin.getJwtToken(), + id: "550e8400-e29b-41d4-a716-446655440000", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(404); + expect(body.message).toEqual("Resource not found"); + }); + + it("should fail with status 403 forbidden when user cannot read the document", async () => { + const res = await server.post("/api/relationships.info", { + body: { + token: anotherUser.getJwtToken(), + id: relationship.id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(403); + expect(body.message).toEqual("Authorization error"); + }); + + it("should succeed with status 200 ok when user can read the document", async () => { + const res = await server.post("/api/relationships.info", { + body: { + token: admin.getJwtToken(), + id: relationship.id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + expect(body.data.relationship).toBeTruthy(); + expect(body.data.relationship.id).toEqual(relationship.id); + expect(body.data.relationship.documentId).toEqual(document.id); + expect(body.data.relationship.reverseDocumentId).toEqual( + reverseDocument.id + ); + expect(body.data.relationship.type).toEqual(RelationshipType.Backlink); + expect(body.data.documents).toBeTruthy(); + expect(body.data.documents).toHaveLength(2); + expect(body.policies).toBeTruthy(); + }); + + it("should succeed with status 200 ok when user can read document but not reverse document", async () => { + // Create a relationship where user can read main document but not reverse document + const userDocument = await buildDocument({ + createdById: user.id, + teamId: user.teamId, + }); + + const adminDocument = await buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }); + + const userRelationship = await buildRelationship({ + userId: user.id, + documentId: userDocument.id, + reverseDocumentId: adminDocument.id, + }); + + const res = await server.post("/api/relationships.info", { + body: { + token: user.getJwtToken(), + id: userRelationship.id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + 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); + expect(documentIds).toContain(userDocument.id); + }); + + it("should include both documents when user can read both", async () => { + // Make user team member so they can read both documents + const teamUser = await buildUser({ teamId: admin.teamId }); + + const res = await server.post("/api/relationships.info", { + body: { + token: teamUser.getJwtToken(), + id: relationship.id, + }, + }); + 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); + expect(documentIds).toContain(document.id); + expect(documentIds).toContain(reverseDocument.id); + }); +}); + +describe("#relationships.list", () => { + let admin: User; + let user: User; + let anotherUser: User; // eslint-disable-line @typescript-eslint/no-unused-vars + let relationships: Relationship[]; // eslint-disable-line @typescript-eslint/no-unused-vars + let documents: Document[]; + + beforeEach(async () => { + admin = await buildAdmin(); + [user, anotherUser] = await Promise.all([ + buildUser({ teamId: admin.teamId }), + buildUser(), + ]); + + documents = await Promise.all([ + buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }), + buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }), + buildDocument({ + createdById: admin.id, + teamId: admin.teamId, + }), + ]); + + relationships = [ + await buildRelationship({ + userId: admin.id, + documentId: documents[0].id, + reverseDocumentId: documents[1].id, + type: RelationshipType.Backlink, + }), + await buildRelationship({ + userId: admin.id, + documentId: documents[1].id, + reverseDocumentId: documents[2].id, + type: RelationshipType.Similar, + }), + await buildRelationship({ + userId: admin.id, + documentId: documents[2].id, + reverseDocumentId: documents[0].id, + type: RelationshipType.Backlink, + }), + ]; + }); + + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/relationships.list", { + body: {}, + }); + const body = await res.json(); + expect(res.status).toEqual(401); + expect(body.message).toEqual("Authentication required"); + }); + + it("should succeed with status 200 ok returning all relationships", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + expect(body.data.relationships).toBeTruthy(); + expect(body.data.relationships.length).toBeGreaterThanOrEqual(3); + expect(body.data.documents).toBeTruthy(); + expect(body.pagination).toBeTruthy(); + expect(body.policies).toBeTruthy(); + }); + + it("should succeed with status 200 ok returning relationships filtered by type", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + type: RelationshipType.Backlink, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + expect(body.data.relationships).toBeTruthy(); + + // All returned relationships should be backlinks + body.data.relationships.forEach((rel: any) => { + expect(rel.type).toEqual(RelationshipType.Backlink); + }); + }); + + it("should succeed with status 200 ok returning relationships filtered by documentId", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + documentId: documents[0].id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + expect(body.data.relationships).toBeTruthy(); + + // All returned relationships should have the specified documentId + body.data.relationships.forEach((rel: any) => { + expect(rel.documentId).toEqual(documents[0].id); + }); + }); + + it("should succeed with status 200 ok returning relationships filtered by reverseDocumentId", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + reverseDocumentId: documents[1].id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + expect(body.data.relationships).toBeTruthy(); + + // All returned relationships should have the specified reverseDocumentId + body.data.relationships.forEach((rel: any) => { + expect(rel.reverseDocumentId).toEqual(documents[1].id); + }); + }); + + it("should succeed with status 200 ok returning relationships with multiple filters", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + type: RelationshipType.Backlink, + documentId: documents[0].id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toBeTruthy(); + 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); + }); + }); + + it("should fail with status 400 bad request when documentId is invalid", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + documentId: "invalid-id", + }, + }); + expect(res.status).toEqual(400); + }); + + it("should fail with status 400 bad request when reverseDocumentId is invalid", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + reverseDocumentId: "invalid-id", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toContain("uuid or url slug"); + }); + + it("should respect pagination", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + limit: 1, + offset: 0, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.relationships).toHaveLength(1); + expect(body.pagination).toBeTruthy(); + expect(body.pagination.limit).toEqual(1); + expect(body.pagination.offset).toEqual(0); + }); + + it("should return empty results when no relationships match filters", async () => { + const res = await server.post("/api/relationships.list", { + body: { + token: admin.getJwtToken(), + documentId: "550e8400-e29b-41d4-a716-446655440000", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.relationships).toHaveLength(0); + expect(body.data.documents).toHaveLength(0); + }); + + it("should only return documents that the user can read", async () => { + // Create a relationship where user can only read some documents + + const cannotAccessCollection = await buildCollection({ + teamId: user.teamId, + permission: null, + }); + const collection = await buildCollection({ + teamId: user.teamId, + permission: CollectionPermission.Read, + }); + const userDocument = await buildDocument({ + collectionId: collection.id, + teamId: user.teamId, + }); + const cannotAccessDocument = await buildDocument({ + collectionId: cannotAccessCollection.id, + teamId: admin.teamId, + }); + + await buildRelationship({ + userId: user.id, + documentId: userDocument.id, + reverseDocumentId: cannotAccessDocument.id, + }); + + const res = await server.post("/api/relationships.list", { + body: { + token: user.getJwtToken(), + documentId: userDocument.id, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + + expect(body.data.relationships).toHaveLength(1); + expect(body.data.documents).toHaveLength(0); + }); +}); diff --git a/server/routes/api/relationships/relationships.ts b/server/routes/api/relationships/relationships.ts new file mode 100644 index 0000000000..c3b160f195 --- /dev/null +++ b/server/routes/api/relationships/relationships.ts @@ -0,0 +1,98 @@ +import Router from "koa-router"; +import auth from "@server/middlewares/authentication"; +import validate from "@server/middlewares/validate"; +import { Document, Relationship } from "@server/models"; +import { authorize, can } from "@server/policies"; +import { + presentRelationship, + presentDocument, + presentPolicies, +} from "@server/presenters"; +import { APIContext } from "@server/types"; +import pagination from "../middlewares/pagination"; +import * as T from "./schema"; + +const router = new Router(); + +router.post( + "relationships.info", + auth(), + validate(T.RelationshipsInfoSchema), + async (ctx: APIContext) => { + const { id } = ctx.input.body; + const { user } = ctx.state.auth; + + const relationship = await Relationship.findByPk(id, { + rejectOnEmpty: true, + }); + const document = await Document.findByPk(relationship.documentId, { + userId: user.id, + rejectOnEmpty: true, + }); + authorize(user, "read", document); + + const reverseDocument = await Document.findByPk( + relationship.reverseDocumentId, + { + userId: user.id, + rejectOnEmpty: true, + } + ); + authorize(user, "read", reverseDocument); + + const documents = [document, reverseDocument]; + + ctx.body = { + data: { + relationship: presentRelationship(relationship), + documents: await Promise.all( + documents.map((doc: Document) => presentDocument(ctx, doc)) + ), + }, + policies: presentPolicies(user, documents), + }; + } +); + +router.post( + "relationships.list", + auth(), + pagination(), + validate(T.RelationshipsListSchema), + async (ctx: APIContext) => { + const { user } = ctx.state.auth; + const where = ctx.input.body || {}; + + const relationships = await Relationship.findAll({ + where, + order: [["createdAt", "DESC"]], + offset: ctx.state.pagination.offset, + limit: ctx.state.pagination.limit, + }); + + const documents = await Document.findByIds( + relationships.flatMap((relationship) => + where.reverseDocumentId + ? relationship.documentId + : relationship.reverseDocumentId + ), + { userId: user.id } + ); + + const policies = presentPolicies(user, [...documents, ...relationships]); + + ctx.body = { + pagination: ctx.state.pagination, + data: { + relationships: relationships.map(presentRelationship), + documents: await Promise.all( + documents.map((document: Document) => presentDocument(ctx, document)) + ), + policies: presentPolicies(user, documents), + }, + policies, + }; + } +); + +export default router; diff --git a/server/routes/api/relationships/schema.ts b/server/routes/api/relationships/schema.ts new file mode 100644 index 0000000000..63cd31be98 --- /dev/null +++ b/server/routes/api/relationships/schema.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; +import { RelationshipType } from "@server/models/Relationship"; +import { ValidateDocumentId } from "@server/validation"; +import { BaseSchema } from "../schema"; + +export const RelationshipsInfoSchema = BaseSchema.extend({ + body: z.object({ + id: z.string().uuid(), + }), +}); + +export type RelationshipsInfoReq = z.infer; + +export const RelationshipsListSchema = BaseSchema.extend({ + body: z + .object({ + type: z.nativeEnum(RelationshipType).optional(), + documentId: z + .string() + .refine(ValidateDocumentId.isValid, { + message: ValidateDocumentId.message, + }) + .optional(), + reverseDocumentId: z + .string() + .refine(ValidateDocumentId.isValid, { + message: ValidateDocumentId.message, + }) + .optional(), + }) + .optional(), +}); + +export type RelationshipsListReq = z.infer; diff --git a/server/test/factories.ts b/server/test/factories.ts index b328f7d9a2..102dfc011b 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -47,7 +47,9 @@ import { OAuthClient, AuthenticationProvider, OAuthAuthentication, + Relationship, } from "@server/models"; +import { RelationshipType } from "@server/models/Relationship"; import AttachmentHelper from "@server/models/helpers/AttachmentHelper"; import { hash } from "@server/utils/crypto"; import { OAuthInterface } from "@server/utils/oauth/OAuthInterface"; @@ -829,3 +831,29 @@ export function buildCommentMark(overrides: { attrs: overrides, }; } + +export async function buildRelationship(overrides: Partial = {}) { + if (!overrides.userId) { + const user = await buildUser(); + overrides.userId = user.id; + } + + if (!overrides.documentId) { + const document = await buildDocument({ + createdById: overrides.userId, + }); + overrides.documentId = document.id; + } + + if (!overrides.reverseDocumentId) { + const reverseDocument = await buildDocument({ + createdById: overrides.userId, + }); + overrides.reverseDocumentId = reverseDocument.id; + } + + return Relationship.create({ + type: RelationshipType.Backlink, + ...overrides, + }); +} diff --git a/yarn.lock b/yarn.lock index e642eb8f93..17e4d22880 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7704,7 +7704,7 @@ ejs@^3.1.10, ejs@^3.1.6: dependencies: jake "^10.8.5" -electron-to-chromium@^1.5.173: +electron-to-chromium@^1.5.173, electron-to-chromium@^1.5.73: version "1.5.182" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz#4ab73104f893938acb3ab9c28d7bec170c116b3e" integrity sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==