{"version":3,"file":"self-referential-relations.mjs","sources":["../../../../src/services/document-service/utils/self-referential-relations.ts"],"sourcesContent":["/* eslint-disable no-continue */\nimport { keyBy, omit } from 'lodash/fp';\nimport type { Data, UID } from '@strapi/types';\nimport type { JoinTable } from '@strapi/database';\n\ninterface VersionEntry {\n  id: Data.ID;\n  locale: string;\n}\n\ninterface RelationData {\n  joinTable: JoinTable;\n  relations: Record<string, unknown>[];\n}\n\n/**\n * Preserves self-referential relations during publish/discard operations.\n *\n * When publishing or discarding a draft, self-referential relations (where both sides\n * of the relation belong to the same content type) are lost because:\n *\n * 1. The old entry is deleted\n * 2. A new entry is created with relations resolved via documentId → entity ID mapping\n * 3. At mapping time, the old entity is already deleted and the new one doesn't exist yet\n * 4. The relation is silently dropped\n *\n * This utility:\n * 1. Captures self-referential join table rows before deletion\n * 2. Remaps old entity IDs to new entity IDs after creation\n * 3. Inserts the remapped relations\n */\n\n/**\n * Loads self-referential relations from source entries before they are deleted/recreated.\n */\nconst load = async (\n  uid: UID.ContentType,\n  sourceEntries: VersionEntry[]\n): Promise<RelationData[]> => {\n  const updates: RelationData[] = [];\n  const dbModel = strapi.db.metadata.get(uid);\n\n  await strapi.db.transaction(async ({ trx }) => {\n    for (const attribute of Object.values(dbModel.attributes) as any) {\n      if (attribute.type !== 'relation' || attribute.target !== uid) {\n        continue;\n      }\n\n      // Bidirectional inverse side (e.g. `children` mappedBy `parent`) shares the same physical\n      // join table as the owning attribute; processing both would duplicate rows and inserts.\n      if (attribute.mappedBy) {\n        continue;\n      }\n\n      const joinTable = attribute.joinTable;\n      if (!joinTable) {\n        continue;\n      }\n\n      const { name: sourceColumnName } = joinTable.joinColumn;\n      const { name: targetColumnName } = joinTable.inverseJoinColumn;\n\n      const sourceIds = sourceEntries.map((entry) => String(entry.id));\n\n      // Load relations where both source and target are among the entries being processed.\n      // These are the self-referential relations that would be lost during the\n      // delete-and-recreate cycle.\n      const selfRelations = await strapi.db\n        .getConnection()\n        .select('*')\n        .from(joinTable.name)\n        .whereIn(sourceColumnName, sourceIds)\n        .whereIn(targetColumnName, sourceIds)\n        .transacting(trx);\n\n      if (selfRelations.length > 0) {\n        updates.push({ joinTable, relations: selfRelations });\n      }\n    }\n  });\n\n  return updates;\n};\n\n/**\n * Syncs self-referential relations by remapping old entry IDs to new entry IDs\n * and inserting the remapped relations into the join table.\n */\nconst sync = async (\n  sourceEntries: VersionEntry[],\n  targetEntries: VersionEntry[],\n  relationData: RelationData[]\n) => {\n  if (relationData.length === 0) return;\n\n  const targetEntriesByLocale = keyBy('locale', targetEntries);\n\n  // Keys stringified for object lookup; values keep the original DB type so PostgreSQL integer columns receive integers, not strings\n  const idMapping = sourceEntries.reduce(\n    (acc, sourceEntry) => {\n      const targetEntry = targetEntriesByLocale[sourceEntry.locale];\n      if (!targetEntry) return acc;\n      acc[String(sourceEntry.id)] = targetEntry.id;\n      return acc;\n    },\n    {} as Record<string, Data.ID>\n  );\n\n  const batchSize = strapi.db.dialect.getBatchInsertSize();\n\n  await strapi.db.transaction(async ({ trx }) => {\n    for (const { joinTable, relations } of relationData) {\n      const sourceColumn = joinTable.joinColumn.name;\n      const targetColumn = joinTable.inverseJoinColumn.name;\n\n      const newRelations = relations\n        .map((relation) => {\n          const oldSourceId = String(relation[sourceColumn]);\n          const oldTargetId = String(relation[targetColumn]);\n          const newSourceId = idMapping[oldSourceId];\n          const newTargetId = idMapping[oldTargetId];\n\n          // Both sides must map to new entries\n          if (!newSourceId || !newTargetId) return null;\n\n          return {\n            ...omit(strapi.db.metadata.identifiers.ID_COLUMN, relation),\n            [sourceColumn]: newSourceId,\n            [targetColumn]: newTargetId,\n          };\n        })\n        .filter(Boolean) as Record<string, unknown>[];\n\n      const pairKey = (r: Record<string, unknown>) =>\n        `${String(r[sourceColumn])}:${String(r[targetColumn])}`;\n      const seenPairs = new Set<string>();\n      const deduped = newRelations.filter((r) => {\n        const key = pairKey(r);\n        if (seenPairs.has(key)) return false;\n        seenPairs.add(key);\n        return true;\n      });\n\n      if (deduped.length === 0) continue;\n\n      const newSourceIds = [...new Set(deduped.map((r) => String(r[sourceColumn])))];\n      const existingRows = await trx(joinTable.name)\n        .whereIn(sourceColumn, newSourceIds)\n        .select(sourceColumn, targetColumn);\n\n      const existingSet = new Set(existingRows.map((r: Record<string, unknown>) => pairKey(r)));\n      const toInsert = deduped.filter((r) => !existingSet.has(pairKey(r)));\n\n      if (toInsert.length > 0) {\n        await trx.batchInsert(joinTable.name, toInsert as any[], batchSize);\n      }\n    }\n  });\n};\n\nexport { load, sync };\n"],"names":["load","uid","sourceEntries","updates","dbModel","strapi","db","metadata","get","transaction","trx","attribute","Object","values","attributes","type","target","mappedBy","joinTable","name","sourceColumnName","joinColumn","targetColumnName","inverseJoinColumn","sourceIds","map","entry","String","id","selfRelations","getConnection","select","from","whereIn","transacting","length","push","relations","sync","targetEntries","relationData","targetEntriesByLocale","keyBy","idMapping","reduce","acc","sourceEntry","targetEntry","locale","batchSize","dialect","getBatchInsertSize","sourceColumn","targetColumn","newRelations","relation","oldSourceId","oldTargetId","newSourceId","newTargetId","omit","identifiers","ID_COLUMN","filter","Boolean","pairKey","r","seenPairs","Set","deduped","key","has","add","newSourceIds","existingRows","existingSet","toInsert","batchInsert"],"mappings":"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"}