{"version":3,"file":"bidirectional-relations.mjs","sources":["../../../../src/services/document-service/utils/bidirectional-relations.ts"],"sourcesContent":["/* eslint-disable no-continue */\nimport { keyBy } from 'lodash/fp';\nimport { async } from '@strapi/utils';\nimport type { UID, Schema } from '@strapi/types';\n\ninterface LoadContext {\n oldVersions: { id: string; locale: string }[];\n newVersions: { id: string; locale: string }[];\n}\n\n/**\n * Loads all bidirectional relations that need to be synchronized when content entries change state\n * (e.g., during publish/unpublish operations).\n *\n * In Strapi, bidirectional relations allow maintaining order from both sides of the relation.\n * When an entry is published, the following occurs:\n *\n * 1. The old published entry is deleted\n * 2. A new entry is created with all its relations\n *\n * This process affects relation ordering in the following way:\n *\n * Initial state (Entry A related to X, Y, Z):\n * ```\n * Entry A (draft) Entry A (published)\n * │ │\n * ├──(1)→ X ├──(1)→ X\n * ├──(2)→ Y ├──(2)→ Y\n * └──(3)→ Z └──(3)→ Z\n *\n * X's perspective: Y's perspective: Z's perspective:\n * └──(2)→ Entry A └──(1)→ Entry A └──(3)→ Entry A\n * ```\n *\n * After publishing Entry A (without relation order sync):\n * ```\n * Entry A (draft) Entry A (new published)\n * │ │\n * ├──(1)→ X ├──(1)→ X\n * ├──(2)→ Y ├──(2)→ Y\n * └──(3)→ Z └──(3)→ Z\n *\n * X's perspective: Y's perspective: Z's perspective:\n * └──(3)→ Entry A └──(3)→ Entry A └──(3)→ Entry A\n * (all relations appear last in order)\n * ```\n *\n * This module preserves the original ordering from both perspectives by:\n * 1. Capturing the relation order before the entry state changes\n * 2. Restoring this order after the new relations are created\n *\n * @param uid - The unique identifier of the content type being processed\n * @param context - Object containing arrays of old and new entry versions\n * @returns Array of objects containing join table metadata and relations to be updated\n */\nconst load = async (uid: UID.ContentType, { oldVersions }: LoadContext) => {\n const relationsToUpdate = [] as any;\n\n await strapi.db.transaction(async ({ trx }) => {\n const contentTypes = Object.values(strapi.contentTypes) as Schema.ContentType[];\n const components = Object.values(strapi.components) as Schema.Component[];\n\n for (const model of [...contentTypes, ...components]) {\n const dbModel = strapi.db.metadata.get(model.uid);\n\n for (const attribute of Object.values(dbModel.attributes) as any) {\n // Skip if not a bidirectional relation targeting our content type\n if (\n attribute.type !== 'relation' ||\n attribute.target !== uid ||\n !(attribute.inversedBy || attribute.mappedBy)\n ) {\n continue;\n }\n\n // If it's a self referencing relation, there is no need to sync any relation\n // The order will already be handled as both sides are inside the same content type\n if (model.uid === uid) {\n continue;\n }\n\n const joinTable = attribute.joinTable;\n if (!joinTable) {\n continue;\n }\n\n const { name: targetColumnName } = joinTable.inverseJoinColumn;\n\n // Load all relations that need their order preserved\n const oldEntryIds = oldVersions.map((entry) => entry.id);\n\n const existingRelations = await strapi.db\n .getConnection()\n .select('*')\n .from(joinTable.name)\n .whereIn(targetColumnName, oldEntryIds)\n .transacting(trx);\n\n if (existingRelations.length > 0) {\n relationsToUpdate.push({ joinTable, relations: existingRelations });\n }\n }\n }\n });\n\n return relationsToUpdate;\n};\n\n/**\n * Synchronizes the order of bidirectional relations after content entries have changed state.\n *\n * When entries change state (e.g., draft → published), their IDs change and all relations are recreated.\n * While the order of relations from the entry's perspective is maintained (as they're created in order),\n * the inverse relations (from related entries' perspective) would all appear last in order since they're new.\n *\n * Example:\n * ```\n * Before publish:\n * Article(id:1) →(order:1)→ Category(id:5)\n * Category(id:5) →(order:3)→ Article(id:1)\n *\n * After publish (without sync):\n * Article(id:2) →(order:1)→ Category(id:5) [order preserved]\n * Category(id:5) →(order:99)→ Article(id:2) [order lost - appears last]\n *\n * After sync:\n * Article(id:2) →(order:1)→ Category(id:5) [order preserved]\n * Category(id:5) →(order:3)→ Article(id:2) [order restored]\n * ```\n *\n * @param oldEntries - Array of previous entry versions with their IDs and locales\n * @param newEntries - Array of new entry versions with their IDs and locales\n * @param existingRelations - Array of join table data containing the relations to be updated\n */\nconst sync = async (\n oldEntries: { id: string; locale: string }[],\n newEntries: { id: string; locale: string }[],\n existingRelations: { joinTable: any; relations: any[] }[]\n) => {\n // Group new entries by locale for easier lookup\n const newEntriesByLocale = keyBy('locale', newEntries);\n\n // Create a mapping of old entry IDs to new entry IDs based on locale\n const entryIdMapping = oldEntries.reduce(\n (acc, oldEntry) => {\n const newEntry = newEntriesByLocale[oldEntry.locale];\n if (!newEntry) return acc;\n acc[oldEntry.id] = newEntry.id;\n return acc;\n },\n {} as Record\n );\n\n await strapi.db.transaction(async ({ trx }) => {\n for (const { joinTable, relations } of existingRelations) {\n const sourceColumn = joinTable.inverseJoinColumn.name;\n const targetColumn = joinTable.joinColumn.name;\n const orderColumn = joinTable.orderColumnName;\n\n // Failsafe in case those don't exist\n if (!sourceColumn || !targetColumn || !orderColumn) {\n continue;\n }\n\n // Update order values for each relation\n // TODO: Find a way to batch it more efficiently\n await async.map(relations, (relation: any) => {\n const {\n [sourceColumn]: oldSourceId,\n [targetColumn]: targetId,\n [orderColumn]: originalOrder,\n } = relation;\n\n // Update the order column for the new relation entry\n return trx\n .from(joinTable.name)\n .where(sourceColumn, entryIdMapping[oldSourceId])\n .where(targetColumn, targetId)\n .update({ [orderColumn]: originalOrder });\n });\n }\n });\n};\n\nexport { load, sync };\n"],"names":["load","uid","oldVersions","relationsToUpdate","strapi","db","transaction","trx","contentTypes","Object","values","components","model","dbModel","metadata","get","attribute","attributes","type","target","inversedBy","mappedBy","joinTable","name","targetColumnName","inverseJoinColumn","oldEntryIds","map","entry","id","existingRelations","getConnection","select","from","whereIn","transacting","length","push","relations","sync","oldEntries","newEntries","newEntriesByLocale","keyBy","entryIdMapping","reduce","acc","oldEntry","newEntry","locale","sourceColumn","targetColumn","joinColumn","orderColumn","orderColumnName","async","relation","oldSourceId","targetId","originalOrder","where","update"],"mappings":"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"}