From fa945f253f89f0130efde59f703fddf703450d52 Mon Sep 17 00:00:00 2001 From: Adrian Smijulj Date: Thu, 19 Sep 2024 15:45:00 +0200 Subject: [PATCH] fix: handle publishing of a non-latest rev and unpublishing of non-latest one (#4279) --- .../src/operations/entry/index.ts | 337 ++++++++++-------- .../src/operations/entry/index.ts | 162 ++++++--- ...Entry.publishOldPublishedRevisions.test.ts | 271 ++++++++++++++ 3 files changed, 568 insertions(+), 202 deletions(-) create mode 100644 packages/api-headless-cms/__tests__/contentAPI/contentEntry.publishOldPublishedRevisions.test.ts diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts index 99c9a1642aa..f7ab27e3527 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts @@ -1208,14 +1208,6 @@ export const createEntriesStorageOperations = ( const { entry, storageEntry } = transformer.transformEntryKeys(); - /** - * We need currently published entry to check if need to remove it. - */ - const [publishedStorageEntry] = await dataLoaders.getPublishedRevisionByEntryId({ - model, - ids: [entry.id] - }); - const revisionKeys = { PK: createPartitionKey({ id: entry.id, @@ -1259,175 +1251,158 @@ export const createEntriesStorageOperations = ( ); } - const items = [ - entity.putBatch({ - ...storageEntry, - ...revisionKeys, - TYPE: createRecordType() - }) - ]; - const esItems: BatchWriteItem[] = []; + if (!latestEsEntry) { + throw new WebinyError( + `Could not publish entry. Could not load latest ("L") record (ES table).`, + "PUBLISH_ERROR", + { entry } + ); + } - const { index: esIndex } = configurations.es({ - model + /** + * We need the latest entry to check if it needs to be updated as well in the Elasticsearch. + */ + const [latestStorageEntry] = await dataLoaders.getLatestRevisionByEntryId({ + model, + ids: [entry.id] }); - if (publishedStorageEntry && publishedStorageEntry.id !== entry.id) { - /** - * If there is a `published` entry already, we need to set it to `unpublished`. We need to - * execute two updates: update the previously published entry's status and the published entry record. - * DynamoDB does not support `batchUpdate` - so here we load the previously published - * entry's data to update its status within a batch operation. If, hopefully, - * they introduce a true update batch operation, remove this `read` call. - */ - const [previouslyPublishedEntry] = await dataLoaders.getRevisionById({ - model, - ids: [publishedStorageEntry.id] - }); - items.push( - /** - * Update currently published entry (unpublish it) - */ - entity.putBatch({ - ...previouslyPublishedEntry, - status: CONTENT_ENTRY_STATUS.UNPUBLISHED, - TYPE: createRecordType(), - PK: createPartitionKey(publishedStorageEntry), - SK: createRevisionSortKey(publishedStorageEntry) - }) + if (!latestStorageEntry) { + throw new WebinyError( + `Could not publish entry. Could not load latest ("L") record.`, + "PUBLISH_ERROR", + { entry } ); } + /** - * Update the helper item in DB with the new published entry + * We need currently published entry to check if need to remove it. */ - items.push( + const [publishedStorageEntry] = await dataLoaders.getPublishedRevisionByEntryId({ + model, + ids: [entry.id] + }); + + // 1. Update REV# and P records with new data. + const items = [ + entity.putBatch({ + ...storageEntry, + ...revisionKeys, + TYPE: createRecordType() + }), entity.putBatch({ ...storageEntry, ...publishedKeys, TYPE: createPublishedRecordType() }) - ); + ]; + const esItems: BatchWriteItem[] = []; - /** - * We need the latest entry to check if it needs to be updated as well in the Elasticsearch. - */ - const [latestStorageEntry] = await dataLoaders.getLatestRevisionByEntryId({ - model, - ids: [entry.id] + const { index: esIndex } = configurations.es({ + model }); - if (latestStorageEntry?.id === entry.id) { + // 2. When it comes to the latest record, we need to perform a couple of different + // updates, based on whether the entry being published is the latest revision or not. + const publishedRevisionId = publishedStorageEntry?.id; + const publishingLatestRevision = latestStorageEntry?.id === entry.id; + + if (publishingLatestRevision) { + // 2.1 If we're publishing the latest revision, we first need to update the L record. items.push( entity.putBatch({ ...storageEntry, ...latestKeys }) ); - } - - if (latestEsEntry) { - const publishingLatestRevision = latestStorageEntry?.id === entry.id; - - /** - * Need to decompress the data from Elasticsearch DynamoDB table. - * - * No need to transform it for the storage because it was fetched - * directly from the Elasticsearch table, where it sits transformed. - */ - const latestEsEntryDataDecompressed = (await decompress( - plugins, - latestEsEntry.data - )) as CmsIndexEntry; - if (publishingLatestRevision) { - const updatedMetaFields = pickEntryMetaFields(entry); - - const latestTransformer = createTransformer({ - plugins, - model, - transformedToIndex: { - ...latestEsEntryDataDecompressed, - status: CONTENT_ENTRY_STATUS.PUBLISHED, - locked: true, - ...updatedMetaFields - } - }); - - esItems.push( - esEntity.putBatch({ - index: esIndex, - PK: createPartitionKey(latestEsEntryDataDecompressed), - SK: createLatestSortKey(), - data: await latestTransformer.getElasticsearchLatestEntryData() - }) - ); - } else { - const updatedEntryLevelMetaFields = pickEntryMetaFields( - entry, - isEntryLevelEntryMetaField - ); - - const updatedLatestStorageEntry = { - ...latestStorageEntry, - ...latestKeys, - ...updatedEntryLevelMetaFields - }; - - /** - * First we update the regular DynamoDB table. Two updates are needed: - * - one for the actual revision record - * - one for the latest record - */ - items.push( - entity.putBatch({ - ...updatedLatestStorageEntry, - PK: createPartitionKey({ - id: latestStorageEntry.id, - locale: model.locale, - tenant: model.tenant - }), - SK: createRevisionSortKey(latestStorageEntry), - TYPE: createRecordType() - }) - ); + // 2.2 Additionally, if we have a previously published entry, we need to mark it as unpublished. + // Note that we need to take re-publishing into account (same published revision being + // published again), in which case the below code does not apply. This is because the + // required updates were already applied above. + if (publishedStorageEntry) { + const isRepublishing = publishedStorageEntry.id === entry.id; + if (!isRepublishing) { + items.push( + /** + * Update currently published entry (unpublish it) + */ + entity.putBatch({ + ...publishedStorageEntry, + status: CONTENT_ENTRY_STATUS.UNPUBLISHED, + TYPE: createRecordType(), + PK: createPartitionKey(publishedStorageEntry), + SK: createRevisionSortKey(publishedStorageEntry) + }) + ); + } + } + } else { + // 2.3 If the published revision is not the latest one, the situation is a bit + // more complex. We first need to update the L and REV# records with the new + // values of *only entry-level* meta fields. + const updatedEntryLevelMetaFields = pickEntryMetaFields( + entry, + isEntryLevelEntryMetaField + ); - items.push( - entity.putBatch({ - ...updatedLatestStorageEntry, - TYPE: createLatestRecordType() - }) - ); + // 2.4 Update L record. Apart from updating the entry-level meta fields, we also need + // to change the status from "published" to "unpublished" (if the status is set to "published"). + let latestRevisionStatus = latestStorageEntry.status; + if (latestRevisionStatus === CONTENT_ENTRY_STATUS.PUBLISHED) { + latestRevisionStatus = CONTENT_ENTRY_STATUS.UNPUBLISHED; + } - /** - * Update the Elasticsearch table to propagate changes to the Elasticsearch. - */ - const latestEsEntry = await getClean({ - entity: esEntity, - keys: latestKeys - }); + const latestStorageEntryFields = { + ...latestStorageEntry, + ...updatedEntryLevelMetaFields, + status: latestRevisionStatus + }; - if (latestEsEntry) { - const latestEsEntryDataDecompressed = (await decompress( - plugins, - latestEsEntry.data - )) as CmsIndexEntry; + items.push( + entity.putBatch({ + ...latestStorageEntryFields, + PK: createPartitionKey(latestStorageEntry), + SK: createLatestSortKey(), + TYPE: createLatestRecordType() + }) + ); - const updatedLatestEntry = await compress(plugins, { - ...latestEsEntryDataDecompressed, - ...updatedEntryLevelMetaFields - }); + // 2.5 Update REV# record. + items.push( + entity.putBatch({ + ...latestStorageEntryFields, + PK: createPartitionKey(latestStorageEntry), + SK: createRevisionSortKey(latestStorageEntry), + TYPE: createRecordType() + }) + ); - esItems.push( - esEntity.putBatch({ - ...latestKeys, - index: esIndex, - data: updatedLatestEntry + // 2.6 Additionally, if we have a previously published entry, we need to mark it as unpublished. + // Note that we need to take re-publishing into account (same published revision being + // published again), in which case the below code does not apply. This is because the + // required updates were already applied above. + if (publishedStorageEntry) { + const isRepublishing = publishedStorageEntry.id === entry.id; + const publishedRevisionDifferentFromLatest = + publishedRevisionId !== latestStorageEntry.id; + + if (!isRepublishing && publishedRevisionDifferentFromLatest) { + items.push( + entity.putBatch({ + ...publishedStorageEntry, + PK: createPartitionKey(publishedStorageEntry), + SK: createRevisionSortKey(publishedStorageEntry), + TYPE: createRecordType(), + status: CONTENT_ENTRY_STATUS.UNPUBLISHED }) ); } } } + // 3. Update records in ES -> DDB table. + /** * Update the published revision entry in ES. */ @@ -1440,6 +1415,80 @@ export const createEntriesStorageOperations = ( }) ); + /** + * Need to decompress the data from Elasticsearch DynamoDB table. + * + * No need to transform it for the storage because it was fetched + * directly from the Elasticsearch table, where it sits transformed. + */ + const latestEsEntryDataDecompressed = (await decompress( + plugins, + latestEsEntry.data + )) as CmsIndexEntry; + + if (publishingLatestRevision) { + const updatedMetaFields = pickEntryMetaFields(entry); + + const latestTransformer = createTransformer({ + plugins, + model, + transformedToIndex: { + ...latestEsEntryDataDecompressed, + status: CONTENT_ENTRY_STATUS.PUBLISHED, + locked: true, + ...updatedMetaFields + } + }); + + esItems.push( + esEntity.putBatch({ + index: esIndex, + PK: createPartitionKey(latestEsEntryDataDecompressed), + SK: createLatestSortKey(), + data: await latestTransformer.getElasticsearchLatestEntryData() + }) + ); + } else { + const updatedEntryLevelMetaFields = pickEntryMetaFields( + entry, + isEntryLevelEntryMetaField + ); + + /** + * Update the Elasticsearch table to propagate changes to the Elasticsearch. + */ + const latestEsEntry = await getClean({ + entity: esEntity, + keys: latestKeys + }); + + if (latestEsEntry) { + const latestEsEntryDataDecompressed = (await decompress( + plugins, + latestEsEntry.data + )) as CmsIndexEntry; + + let latestRevisionStatus = latestEsEntryDataDecompressed.status; + if (latestRevisionStatus === CONTENT_ENTRY_STATUS.PUBLISHED) { + latestRevisionStatus = CONTENT_ENTRY_STATUS.UNPUBLISHED; + } + + const updatedLatestEntry = await compress(plugins, { + ...latestEsEntryDataDecompressed, + ...updatedEntryLevelMetaFields, + status: latestRevisionStatus + }); + + esItems.push( + esEntity.putBatch({ + ...latestKeys, + index: esIndex, + data: updatedLatestEntry + }) + ); + } + } + /** * Finally, execute regular table batch. */ diff --git a/packages/api-headless-cms-ddb/src/operations/entry/index.ts b/packages/api-headless-cms-ddb/src/operations/entry/index.ts index 9d1c76fc93c..b46670efe94 100644 --- a/packages/api-headless-cms-ddb/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb/src/operations/entry/index.ts @@ -283,7 +283,13 @@ export const createEntriesStorageOperations = ( ); // Unpublish previously published revision (if any). - const publishedRevisionStorageEntry = await getPublishedRevisionByEntryId(model, entry); + const [publishedRevisionStorageEntry] = await dataLoaders.getPublishedRevisionByEntryId( + { + model, + ids: [entry.id] + } + ); + if (publishedRevisionStorageEntry) { items.push( entity.putBatch({ @@ -1053,19 +1059,22 @@ export const createEntriesStorageOperations = ( * We need the latest and published entries to see if something needs to be updated alongside the publishing one. */ const initialLatestStorageEntry = await getLatestRevisionByEntryId(model, entry); + if (!initialLatestStorageEntry) { + throw new WebinyError( + `Could not publish entry. Could not load latest ("L") record.`, + "PUBLISH_ERROR", + { entry } + ); + } + const initialPublishedStorageEntry = await getPublishedRevisionByEntryId(model, entry); const storageEntry = convertToStorageEntry({ model, storageEntry: initialStorageEntry }); - /** - * We need to update: - * - current entry revision sort key - * - published sort key - * - the latest sort key - if entry updated is actually latest - * - previous published entry to unpublished status - if any previously published entry - */ + + // 1. Update REV# and P records with new data. const items = [ entity.putBatch({ ...storageEntry, @@ -1085,78 +1094,115 @@ export const createEntriesStorageOperations = ( }) ]; - if (initialLatestStorageEntry) { - const publishingLatestRevision = entry.id === initialLatestStorageEntry.id; + // 2. When it comes to the latest record, we need to perform a couple of different + // updates, based on whether the entry being published is the latest revision or not. + const publishedRevisionId = initialPublishedStorageEntry?.id; + const publishingLatestRevision = entry.id === initialLatestStorageEntry.id; - if (publishingLatestRevision) { - // We want to update current latest record because of the status (`status: 'published'`) update. - items.push( - entity.putBatch({ - ...storageEntry, - PK: partitionKey, - SK: createLatestSortKey(), - TYPE: createLatestType(), - GSI1_PK: createGSIPartitionKey(model, "L"), - GSI1_SK: createGSISortKey(entry) - }) - ); - } else { - const latestStorageEntry = convertToStorageEntry({ - storageEntry: initialLatestStorageEntry, + if (publishingLatestRevision) { + // 2.1 If we're publishing the latest revision, we first need to update the L record. + items.push( + entity.putBatch({ + ...storageEntry, + PK: partitionKey, + SK: createLatestSortKey(), + TYPE: createLatestType(), + GSI1_PK: createGSIPartitionKey(model, "L"), + GSI1_SK: createGSISortKey(entry) + }) + ); + + // 2.2 Additionally, if we have a previously published entry, we need to mark it as unpublished. + if (publishedRevisionId && publishedRevisionId !== entry.id) { + const publishedStorageEntry = convertToStorageEntry({ + storageEntry: initialPublishedStorageEntry, model }); - // If the published revision is not the latest one, we still need to - // update the latest record with the new values of entry-level meta fields. - const updatedEntryLevelMetaFields = pickEntryMetaFields( - entry, - isEntryLevelEntryMetaField - ); - - // 1. Update actual revision record. items.push( entity.putBatch({ - ...latestStorageEntry, - ...updatedEntryLevelMetaFields, + ...publishedStorageEntry, PK: partitionKey, - SK: createRevisionSortKey(latestStorageEntry), + SK: createRevisionSortKey(publishedStorageEntry), TYPE: createType(), + status: CONTENT_ENTRY_STATUS.UNPUBLISHED, GSI1_PK: createGSIPartitionKey(model, "A"), - GSI1_SK: createGSISortKey(latestStorageEntry) - }) - ); - - // 2. Update latest record. - items.push( - entity.putBatch({ - ...latestStorageEntry, - ...updatedEntryLevelMetaFields, - PK: partitionKey, - SK: createLatestSortKey(), - TYPE: createLatestType(), - GSI1_PK: createGSIPartitionKey(model, "L"), - GSI1_SK: createGSISortKey(latestStorageEntry) + GSI1_SK: createGSISortKey(publishedStorageEntry) }) ); } - } + } else { + // 2.3 If the published revision is not the latest one, the situation is a bit + // more complex. We first need to update the L and REV# records with the new + // values of *only entry-level* meta fields. + const updatedEntryLevelMetaFields = pickEntryMetaFields( + entry, + isEntryLevelEntryMetaField + ); - if (initialPublishedStorageEntry && initialPublishedStorageEntry.id !== entry.id) { - const publishedStorageEntry = convertToStorageEntry({ - storageEntry: initialPublishedStorageEntry, + const latestStorageEntry = convertToStorageEntry({ + storageEntry: initialLatestStorageEntry, model }); + + // 2.3.1 Update L record. Apart from updating the entry-level meta fields, we also need + // to change the status from "published" to "unpublished" (if the status is set to "published"). + let latestRevisionStatus = latestStorageEntry.status; + if (latestRevisionStatus === CONTENT_ENTRY_STATUS.PUBLISHED) { + latestRevisionStatus = CONTENT_ENTRY_STATUS.UNPUBLISHED; + } + + const latestStorageEntryFields = { + ...latestStorageEntry, + ...updatedEntryLevelMetaFields, + status: latestRevisionStatus + }; + items.push( entity.putBatch({ - ...publishedStorageEntry, + ...latestStorageEntryFields, PK: partitionKey, - SK: createRevisionSortKey(publishedStorageEntry), + SK: createLatestSortKey(), + TYPE: createLatestType(), + GSI1_PK: createGSIPartitionKey(model, "L"), + GSI1_SK: createGSISortKey(latestStorageEntry) + }) + ); + + // 2.3.2 Update REV# record. + items.push( + entity.putBatch({ + ...latestStorageEntryFields, + PK: partitionKey, + SK: createRevisionSortKey(latestStorageEntry), TYPE: createType(), - status: CONTENT_ENTRY_STATUS.UNPUBLISHED, GSI1_PK: createGSIPartitionKey(model, "A"), - GSI1_SK: createGSISortKey(publishedStorageEntry) + GSI1_SK: createGSISortKey(latestStorageEntry) }) ); + + // 2.3.3 Finally, if we got a published entry, but it wasn't the latest one, we need to take + // an extra step and mark it as unpublished. + const publishedRevisionDifferentFromLatest = + publishedRevisionId && publishedRevisionId !== latestStorageEntry.id; + if (publishedRevisionDifferentFromLatest) { + const publishedStorageEntry = convertToStorageEntry({ + storageEntry: initialPublishedStorageEntry, + model + }); + + items.push( + entity.putBatch({ + ...publishedStorageEntry, + PK: partitionKey, + SK: createRevisionSortKey(publishedStorageEntry), + TYPE: createType(), + status: CONTENT_ENTRY_STATUS.UNPUBLISHED, + GSI1_PK: createGSIPartitionKey(model, "A"), + GSI1_SK: createGSISortKey(publishedStorageEntry) + }) + ); + } } try { diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.publishOldPublishedRevisions.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.publishOldPublishedRevisions.test.ts new file mode 100644 index 00000000000..e58d668ccd0 --- /dev/null +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.publishOldPublishedRevisions.test.ts @@ -0,0 +1,271 @@ +import { useTestModelHandler } from "~tests/testHelpers/useTestModelHandler"; +import { identityA } from "./security/utils"; + +describe("Content entries - Entry Publishing", () => { + const { manage, read } = useTestModelHandler({ + identity: identityA + }); + + beforeEach(async () => { + await manage.setup(); + }); + + test("should be able to publish a previously published revision (entry already has the latest revision published)", async () => { + const { data: revision1 } = await manage.createTestEntry({ data: { title: "Revision 1" } }); + + await manage.publishTestEntry({ + revision: revision1.id + }); + + const { data: revision2 } = await manage.createTestEntryFrom({ + revision: revision1.id, + data: { title: "Revision 2" } + }); + + await manage.publishTestEntry({ + revision: revision2.id + }); + + // Let's publish revision 1 again. + await manage.publishTestEntry({ + revision: revision1.id + }); + + const { data: manageEntriesList } = await manage.listTestEntries(); + const { data: readEntriesList } = await read.listTestEntries(); + + expect(manageEntriesList).toHaveLength(1); + expect(manageEntriesList).toMatchObject([ + { id: revision2.id, title: "Revision 2", meta: { status: "unpublished" } } + ]); + + expect(readEntriesList).toHaveLength(1); + expect(readEntriesList).toMatchObject([{ id: revision1.id, title: "Revision 1" }]); + }); + + test("should be able to publish a previously published revision (entry already has a non-latest revision published)", async () => { + const { data: revision1 } = await manage.createTestEntry({ + data: { title: "Revision 1" } + }); + + const { data: revision2 } = await manage.createTestEntryFrom({ + revision: revision1.id, + data: { title: "Revision 2" } + }); + + // Let's publish revision 2. + await manage.publishTestEntry({ + revision: revision2.id + }); + + const { data: revision3 } = await manage.createTestEntryFrom({ + revision: revision2.id, + data: { title: "Revision 3" } + }); + + // Let's publish revision 3. + await manage.publishTestEntry({ + revision: revision3.id + }); + + const { data: revision4 } = await manage.createTestEntryFrom({ + revision: revision3.id, + data: { title: "Revision 4" } + }); + + { + const { data: manageEntriesList } = await manage.listTestEntries(); + const { data: readEntriesList } = await read.listTestEntries(); + + expect(manageEntriesList).toHaveLength(1); + expect(manageEntriesList).toMatchObject([ + { + id: revision4.id, + title: "Revision 4", + meta: { + status: "draft", + revisions: [ + { + title: "Revision 4", + slug: revision1.slug, + meta: { status: "draft", version: 4 } + }, + { + title: "Revision 3", + slug: revision1.slug, + meta: { status: "published", version: 3 } + }, + { + title: "Revision 2", + slug: revision1.slug, + meta: { status: "unpublished", version: 2 } + }, + { + title: "Revision 1", + slug: revision1.slug, + meta: { status: "draft", version: 1 } + } + ] + } + } + ]); + + expect(readEntriesList).toHaveLength(1); + expect(readEntriesList).toMatchObject([{ id: revision3.id, title: "Revision 3" }]); + } + + // Let's publish older revision 2 . + await manage.publishTestEntry({ + revision: revision2.id + }); + + { + const { data: manageEntriesList } = await manage.listTestEntries(); + const { data: readEntriesList } = await read.listTestEntries(); + + expect(manageEntriesList).toHaveLength(1); + expect(manageEntriesList).toMatchObject([ + { + id: revision4.id, + title: "Revision 4", + meta: { + status: "draft", + revisions: [ + { + title: "Revision 4", + slug: revision1.slug, + meta: { status: "draft", version: 4 } + }, + { + title: "Revision 3", + slug: revision1.slug, + meta: { status: "unpublished", version: 3 } + }, + { + title: "Revision 2", + slug: revision1.slug, + meta: { status: "published", version: 2 } + }, + { + title: "Revision 1", + slug: revision1.slug, + meta: { status: "draft", version: 1 } + } + ] + } + } + ]); + + expect(readEntriesList).toHaveLength(1); + expect(readEntriesList).toMatchObject([{ id: revision2.id, title: "Revision 2" }]); + } + }); + + test("should be able to publish a previously published revision (entry already has a non-latest revision published, using `createFrom` mutations to publish in this test)", async () => { + const { data: revision1 } = await manage.createTestEntry({ + data: { title: "Revision 1" } + }); + + const { data: revision2 } = await manage.createTestEntryFrom({ + revision: revision1.id, + data: { title: "Revision 2", status: "published" } + }); + + const { data: revision3 } = await manage.createTestEntryFrom({ + revision: revision2.id, + data: { title: "Revision 3", status: "published" } + }); + + const { data: revision4 } = await manage.createTestEntryFrom({ + revision: revision3.id, + data: { title: "Revision 4" } + }); + + { + const { data: manageEntriesList } = await manage.listTestEntries(); + const { data: readEntriesList } = await read.listTestEntries(); + + expect(manageEntriesList).toHaveLength(1); + expect(manageEntriesList).toMatchObject([ + { + id: revision4.id, + title: "Revision 4", + meta: { + status: "draft", + revisions: [ + { + title: "Revision 4", + slug: revision1.slug, + meta: { status: "draft", version: 4 } + }, + { + title: "Revision 3", + slug: revision1.slug, + meta: { status: "published", version: 3 } + }, + { + title: "Revision 2", + slug: revision1.slug, + meta: { status: "unpublished", version: 2 } + }, + { + title: "Revision 1", + slug: revision1.slug, + meta: { status: "draft", version: 1 } + } + ] + } + } + ]); + + expect(readEntriesList).toHaveLength(1); + expect(readEntriesList).toMatchObject([{ id: revision3.id, title: "Revision 3" }]); + } + + // Let's publish older revision 2. + await manage.publishTestEntry({ + revision: revision2.id + }); + + { + const { data: manageEntriesList } = await manage.listTestEntries(); + const { data: readEntriesList } = await read.listTestEntries(); + + expect(manageEntriesList).toHaveLength(1); + expect(manageEntriesList).toMatchObject([ + { + id: revision4.id, + title: "Revision 4", + meta: { + status: "draft", + revisions: [ + { + title: "Revision 4", + slug: revision1.slug, + meta: { status: "draft", version: 4 } + }, + { + title: "Revision 3", + slug: revision1.slug, + meta: { status: "unpublished", version: 3 } + }, + { + title: "Revision 2", + slug: revision1.slug, + meta: { status: "published", version: 2 } + }, + { + title: "Revision 1", + slug: revision1.slug, + meta: { status: "draft", version: 1 } + } + ] + } + } + ]); + + expect(readEntriesList).toHaveLength(1); + expect(readEntriesList).toMatchObject([{ id: revision2.id, title: "Revision 2" }]); + } + }); +});