{"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<string, any>[];\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<string, any>,\n    model: Schema.Component | Schema.ContentType,\n    trx: any\n  ) => Promise<boolean>;\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<RelationUpdate[]> => {\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         * Self-referential relations (model.uid === uid) are handled by selfReferentialRelations;\n         * processing them here would re-insert stale source FK values pointing to deleted entries.\n         */\n        if (\n          attribute.type !== 'relation' ||\n          attribute.target !== uid ||\n          attribute.inversedBy ||\n          attribute.mappedBy ||\n          model.uid === uid\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        const oldVersionsRelations = await strapi.db\n          .getConnection()\n          .select('*')\n          .from(joinTable.name)\n          .whereIn(targetColumnName, ids)\n          .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<string, Data.ID>\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","oldVersionsRelations","getConnection","select","from","whereIn","transacting","length","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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"}