{"version":3,"file":"unidirectional-relations.mjs","sources":["../../../../src/services/document-service/utils/unidirectional-relations.ts"],"sourcesContent":["/* eslint-disable no-continue */\nimport { keyBy, omit } from 'lodash/fp';\n\nimport type { Data, UID, Schema } from '@strapi/types';\n\nimport type { JoinTable } from '@strapi/database';\n\ninterface LoadContext {\n oldVersions: { id: Data.ID; locale: string }[];\n newVersions: { id: Data.ID; locale: string }[];\n}\n\ninterface RelationUpdate {\n joinTable: JoinTable;\n relations: Record[];\n}\n\ninterface RelationFilterOptions {\n /**\n * Function to determine if a relation should be propagated to new document versions\n * This replaces the hardcoded component-specific logic\n */\n shouldPropagateRelation?: (\n relation: Record,\n model: Schema.Component | Schema.ContentType,\n trx: any\n ) => Promise;\n}\n\n/**\n * Loads lingering relations that need to be updated when overriding a published or draft entry.\n * This is necessary because the relations are uni-directional and the target entry is not aware of the source entry.\n * This is not the case for bi-directional relations, where the target entry is also linked to the source entry.\n */\nconst load = async (\n uid: UID.ContentType,\n { oldVersions, newVersions }: LoadContext,\n options: RelationFilterOptions = {}\n): Promise => {\n const updates: RelationUpdate[] = [];\n\n // Iterate all components and content types to find relations that need to be updated\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 /**\n * Only consider unidirectional relations.\n * Bidirectional relations (inversedBy/mappedBy) are handled by bidirectionalRelations.\n * Self-referential relations (model.uid === uid) are included here, but rows where\n * the source entry is also being republished in this same operation are excluded —\n * those are handled by selfReferentialRelations, which remaps both sides simultaneously.\n * Without this guard, we would insert rows pointing to a source entry that is about\n * to be deleted, creating stale foreign-key values.\n */\n if (\n attribute.type !== 'relation' ||\n attribute.target !== uid ||\n attribute.inversedBy ||\n attribute.mappedBy\n ) {\n continue;\n }\n\n // TODO: joinColumn relations\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 /**\n * Load all relations that need to be updated\n */\n // NOTE: when the model has draft and publish, we can assume relation are only draft to draft & published to published\n const ids = oldVersions.map((entry) => entry.id);\n\n let oldVersionsQuery = strapi.db\n .getConnection()\n .select('*')\n .from(joinTable.name)\n .whereIn(targetColumnName, ids);\n\n /**\n * For self-referential relations, exclude join rows where the source entry is also\n * being republished. When both sides are in the same publish batch, selfReferentialRelations\n * already handles remapping them. Including them here too would insert a row whose\n * source FK points at an entry that is about to be deleted.\n */\n if (model.uid === uid && ids.length > 0) {\n oldVersionsQuery = oldVersionsQuery.whereNotIn(sourceColumnName, ids);\n }\n\n const oldVersionsRelations = await oldVersionsQuery.transacting(trx);\n\n if (oldVersionsRelations.length > 0) {\n updates.push({ joinTable, relations: oldVersionsRelations });\n }\n\n /**\n * if publishing\n * if published version exists\n * updated published versions links\n * else\n * create link to newly published version\n *\n * if discarding\n * if published version link exists & not draft version link\n * create link to new draft version\n */\n if (!model.options?.draftAndPublish) {\n const ids = newVersions.map((entry) => entry.id);\n\n // This is the step were we query the join table based on the id of the document\n const newVersionsRelations = await strapi.db\n .getConnection()\n .select('*')\n .from(joinTable.name)\n .whereIn(targetColumnName, ids)\n .transacting(trx);\n\n let versionRelations = newVersionsRelations;\n if (options.shouldPropagateRelation) {\n const relationsToPropagate = [];\n for (const relation of newVersionsRelations) {\n if (await options.shouldPropagateRelation(relation, model, trx)) {\n relationsToPropagate.push(relation);\n }\n }\n versionRelations = relationsToPropagate;\n }\n\n if (versionRelations.length > 0) {\n // when publishing a draft that doesn't have a published version yet,\n // copy the links to the draft over to the published version\n // when discarding a published version, if no drafts exists\n const discardToAdd = versionRelations\n .filter((relation) => {\n const matchingOldVersion = oldVersionsRelations.find((oldRelation) => {\n return oldRelation[sourceColumnName] === relation[sourceColumnName];\n });\n\n return !matchingOldVersion;\n })\n .map(omit(strapi.db.metadata.identifiers.ID_COLUMN));\n\n updates.push({ joinTable, relations: discardToAdd });\n }\n }\n }\n }\n });\n\n return updates;\n};\n\n/**\n * Updates uni directional relations to target the right entries when overriding published or draft entries.\n *\n * This function:\n * 1. Creates new relations pointing to the new entry versions\n * 2. Precisely deletes only the old relations being replaced to prevent orphaned links\n *\n * @param oldEntries The old entries that are being overridden\n * @param newEntries The new entries that are overriding the old ones\n * @param oldRelations The relations that were previously loaded with `load` @see load\n */\nconst sync = async (\n oldEntries: { id: Data.ID; locale: string }[],\n newEntries: { id: Data.ID; locale: string }[],\n oldRelations: { joinTable: any; relations: any[] }[]\n) => {\n /**\n * Create a map of old entry ids to new entry ids\n *\n * Will be used to update the relation target ids\n */\n const newEntryByLocale = keyBy('locale', newEntries);\n const oldEntriesMap = oldEntries.reduce(\n (acc, entry) => {\n const newEntry = newEntryByLocale[entry.locale];\n if (!newEntry) return acc;\n acc[String(entry.id)] = newEntry.id;\n return acc;\n },\n {} as Record\n );\n\n await strapi.db.transaction(async ({ trx }) => {\n // Iterate old relations that are deleted and insert the new ones\n for (const { joinTable, relations } of oldRelations) {\n // Update old ids with the new ones\n const column = joinTable.inverseJoinColumn.name;\n\n const newRelations = relations.map((relation) => {\n const newId = oldEntriesMap[relation[column]];\n return { ...relation, [column]: newId };\n });\n\n const batchSize = strapi.db.dialect.getBatchInsertSize();\n await trx.batchInsert(joinTable.name, newRelations, batchSize);\n }\n });\n};\n\nexport { load, sync };\nexport type { RelationFilterOptions };\n"],"names":["load","uid","oldVersions","newVersions","options","updates","strapi","db","transaction","trx","contentTypes","Object","values","components","model","dbModel","metadata","get","attribute","attributes","type","target","inversedBy","mappedBy","joinTable","name","sourceColumnName","joinColumn","targetColumnName","inverseJoinColumn","ids","map","entry","id","oldVersionsQuery","getConnection","select","from","whereIn","length","whereNotIn","oldVersionsRelations","transacting","push","relations","draftAndPublish","newVersionsRelations","versionRelations","shouldPropagateRelation","relationsToPropagate","relation","discardToAdd","filter","matchingOldVersion","find","oldRelation","omit","identifiers","ID_COLUMN","sync","oldEntries","newEntries","oldRelations","newEntryByLocale","keyBy","oldEntriesMap","reduce","acc","newEntry","locale","String","column","newRelations","newId","batchSize","dialect","getBatchInsertSize","batchInsert"],"mappings":"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"}