{"version":3,"file":"5.0.0-discard-drafts.mjs","sources":["../../../src/migrations/database/5.0.0-discard-drafts.ts"],"sourcesContent":["/**\n * This migration is responsible for creating the draft counterpart for all the entries that were in a published state.\n *\n * In v4, entries could either be in a draft or published state, but not both at the same time.\n * In v5, we introduced the concept of document, and an entry can be in a draft or published state.\n *\n * This means the migration needs to create the draft counterpart if an entry was published.\n *\n * This migration performs the following steps:\n * 1. Creates draft entries for all published entries, without it's components, dynamic zones or relations.\n * 2. Using the document service, discard those same drafts to copy its relations.\n */\n\n/* eslint-disable no-continue */\nimport type { UID } from '@strapi/types';\nimport type { Database, Migration } from '@strapi/database';\nimport { async, contentTypes } from '@strapi/utils';\nimport { createDocumentService } from '../../services/document-service';\n\ntype DocumentVersion = { documentId: string; locale: string };\ntype Knex = Parameters<Migration['up']>[0];\n\n/**\n * Check if the model has draft and publish enabled.\n */\nconst hasDraftAndPublish = async (trx: Knex, meta: any) => {\n  const hasTable = await trx.schema.hasTable(meta.tableName);\n\n  if (!hasTable) {\n    return false;\n  }\n\n  const uid = meta.uid as UID.ContentType;\n  const model = strapi.getModel(uid);\n  const hasDP = contentTypes.hasDraftAndPublish(model);\n  if (!hasDP) {\n    return false;\n  }\n\n  return true;\n};\n\n/**\n * Copy all the published entries to draft entries, without it's components, dynamic zones or relations.\n * This ensures all necessary draft's exist before copying it's relations.\n */\nasync function copyPublishedEntriesToDraft({\n  db,\n  trx,\n  uid,\n}: {\n  db: Database;\n  trx: Knex;\n  uid: string;\n}) {\n  // Extract all scalar attributes to use in the insert query\n  const meta = db.metadata.get(uid);\n\n  // Get scalar attributes that will be copied over the new draft\n  const scalarAttributes = Object.values(meta.attributes).reduce((acc, attribute: any) => {\n    if (['id'].includes(attribute.columnName)) {\n      return acc;\n    }\n\n    if (contentTypes.isScalarAttribute(attribute)) {\n      acc.push(attribute.columnName);\n    }\n\n    return acc;\n  }, [] as string[]);\n\n  /**\n   * Query to copy the published entries into draft entries.\n   *\n   * INSERT INTO tableName (columnName1, columnName2, columnName3, ...)\n   * SELECT columnName1, columnName2, columnName3, ...\n   * FROM tableName\n   */\n  await trx\n    // INSERT INTO tableName (columnName1, columnName2, columnName3, ...)\n    .into(\n      trx.raw(`?? (${scalarAttributes.map(() => `??`).join(', ')})`, [\n        meta.tableName,\n        ...scalarAttributes,\n      ])\n    )\n    .insert((subQb: typeof trx) => {\n      // SELECT columnName1, columnName2, columnName3, ...\n      subQb\n        .select(\n          ...scalarAttributes.map((att: string) => {\n            // Override 'publishedAt' and 'updatedAt' attributes\n            if (att === 'published_at') {\n              return trx.raw('NULL as ??', 'published_at');\n            }\n\n            return att;\n          })\n        )\n        .from(meta.tableName)\n        // Only select entries that were published\n        .whereNotNull('published_at');\n    });\n}\n\n/**\n * Load a batch of versions to discard.\n *\n * Versions with only a draft version will be ignored.\n * Only versions with a published version (which always have a draft version) will be discarded.\n */\nexport async function* getBatchToDiscard({\n  db,\n  trx,\n  uid,\n  batchSize = 1000,\n}: {\n  db: Database;\n  trx: Knex;\n  uid: string;\n  batchSize?: number;\n}) {\n  let offset = 0;\n  let hasMore = true;\n\n  while (hasMore) {\n    // Look for the published entries to discard\n    const batch: DocumentVersion[] = await db\n      .queryBuilder(uid)\n      .select(['id', 'documentId', 'locale'])\n      .where({ publishedAt: { $ne: null } })\n      .limit(batchSize)\n      .offset(offset)\n      .orderBy('id')\n      .transacting(trx)\n      .execute();\n\n    if (batch.length < batchSize) {\n      hasMore = false;\n    }\n\n    offset += batchSize;\n    yield batch;\n  }\n}\n\n/**\n * 2 pass migration to create the draft entries for all the published entries.\n * And then discard the drafts to copy the relations.\n */\nconst migrateUp = async (trx: Knex, db: Database) => {\n  const dpModels = [];\n  for (const meta of db.metadata.values()) {\n    const hasDP = await hasDraftAndPublish(trx, meta);\n    if (hasDP) {\n      dpModels.push(meta);\n    }\n  }\n\n  /**\n   * Create plain draft entries for all the entries that were published.\n   */\n  for (const model of dpModels) {\n    await copyPublishedEntriesToDraft({ db, trx, uid: model.uid });\n  }\n\n  /**\n   * Discard the drafts will copy the relations from the published entries to the newly created drafts.\n   *\n   * Load a batch of entries (batched to prevent loading millions of rows at once ),\n   * and discard them using the document service.\n   *\n   * NOTE: This is using a custom document service without any validations,\n   *       to prevent the migration from failing if users already had invalid data in V4.\n   *       E.g. @see https://github.com/strapi/strapi/issues/21583\n   */\n  const documentService = createDocumentService(strapi, {\n    async validateEntityCreation(_, data) {\n      return data;\n    },\n    async validateEntityUpdate(_, data) {\n      // Data can be partially empty on partial updates\n      // This migration doesn't trigger any update (or partial update),\n      // so it's safe to return the data as is.\n      return data as any;\n    },\n  });\n\n  for (const model of dpModels) {\n    const discardDraft = async (entry: DocumentVersion) =>\n      documentService(model.uid as UID.ContentType).discardDraft({\n        documentId: entry.documentId,\n        locale: entry.locale,\n      });\n\n    for await (const batch of getBatchToDiscard({ db, trx, uid: model.uid })) {\n      // NOTE: concurrency had to be disabled to prevent a race condition with self-references\n      // TODO: improve performance in a safe way\n      await async.map(batch, discardDraft, { concurrency: 1 });\n    }\n  }\n};\n\nexport const discardDocumentDrafts: Migration = {\n  name: 'core::5.0.0-discard-drafts',\n  async up(trx, db) {\n    await migrateUp(trx, db);\n  },\n  async down() {\n    throw new Error('not implemented');\n  },\n};\n"],"names":[],"mappings":"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"}