Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sort entries by references before add #913

Merged
merged 3 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { ApplyChangesetContext } from '../types'
import { pluralizeEntry } from '../../utils'
import { isString } from 'lodash'
import { publishEntity } from '../actions/publish-entity'
import sortEntries from '../../utils/sort-entries-by-reference'

export const createAddEntitiesTask = (): ListrTask => {
return {
title: 'Adding entries',
task: async (context: ApplyChangesetContext, task) => {
const { client, changeset, environmentId, logger, responseCollector } = context
logger.info('Start createAddEntitiesTask')
const entries = changeset.items.filter((item) => item.changeType === 'add') as AddedChangesetItem[]
const entries = sortEntries(changeset.items.filter((item) => item.changeType === 'add') as AddedChangesetItem[])
const entityCount = entries.length

task.title = `Adding ${entityCount} ${pluralizeEntry(entityCount)}`
Expand Down
113 changes: 113 additions & 0 deletions src/engine/utils/sort-entries-by-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { some, filter, map, flatten, values, get, has } from 'lodash'
import { AddedChangesetItem } from '../types'
import { EntryField } from 'contentful'
import { EntrySkeletonType } from 'contentful/dist/types/types/query'

type LinksPerEntry = { index: number; linkIndexes: number[] }

/**
* Given a list of entries, this function reorders them so that entries which
* are linked from other entries always come first in the order. This ensures
* that when we publish the newly added entries, we are not publishing entries
* which contain links to other entries that haven't been published yet.
*/
export default function sortEntries(entries: AddedChangesetItem[]): AddedChangesetItem[] {
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved
const linkedEntries = getLinkedEntries(entries)

const mergedLinkedEntries = mergeSort(linkedEntries, (a: LinksPerEntry) => {
return hasLinkedIndexesInFront(a)
})

return map(mergedLinkedEntries, (linkInfo: LinksPerEntry) => entries[linkInfo.index])

function hasLinkedIndexesInFront(item: LinksPerEntry): number {
if (hasLinkedIndexes(item)) {
return some(item.linkIndexes, (index) => index > item.index) ? 1 : -1
}
return 0
}

function hasLinkedIndexes(item: LinksPerEntry): boolean {
return item.linkIndexes.length > 0
}
}

function getLinkedEntries(entries: AddedChangesetItem[]): LinksPerEntry[] {
return map(entries, function (entry: AddedChangesetItem) {
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved
const entryIndex = entries.indexOf(entry)
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved

const rawLinks = map(entry.data.fields, (field) => {
field = values(field)[0]
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved
if (isEntryLink(field)) {
return getFieldEntriesIndex(field, entries)
} else if (isEntityArray(field) && isEntryLink(field[0])) {
return map(field, (item: EntryField<EntrySkeletonType>) => getFieldEntriesIndex(item, entries))
}
})

return {
index: entryIndex,
linkIndexes: filter(flatten(rawLinks), (index: number) => index >= 0) as number[],
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved
}
})
}

function getFieldEntriesIndex(field: EntryField<EntrySkeletonType>, entries: AddedChangesetItem[]): number {
const id = get(field, 'sys.id')
Cyberxon marked this conversation as resolved.
Show resolved Hide resolved
return entries.findIndex((entry) => entry.data.sys.id === id)
}

function isEntryLink(item: EntryField<EntrySkeletonType>): boolean {
return get(item, 'sys.type') === 'Entry' || get(item, 'sys.linkType') === 'Entry'
}

function isEntityArray(item: EntryField<EntrySkeletonType>): boolean {
return Array.isArray(item) && item.length > 0 && has(item[0], 'sys')
}

/**
* From https://github.com/millermedeiros/amd-utils/blob/master/src/array/sort.js
* MIT Licensed
* Merge sort (http://en.wikipedia.org/wiki/Merge_sort)
* @version 0.1.0 (2012/05/23)
*/
function mergeSort(
arr: LinksPerEntry[],
compareFn: ((a: LinksPerEntry) => number) | ((a: LinksPerEntry, b: LinksPerEntry) => number),
): LinksPerEntry[] {
if (arr.length < 2) return arr

if (compareFn == null) compareFn = defaultCompare

const mid = ~~(arr.length / 2)
const left = mergeSort(arr.slice(0, mid), compareFn)
const right = mergeSort(arr.slice(mid, arr.length), compareFn)

return merge(left, right, compareFn)
}

function defaultCompare(a: LinksPerEntry, b: LinksPerEntry): -1 | 1 | 0 {
return a < b ? -1 : a > b ? 1 : 0
}

function merge(
left: LinksPerEntry[],
right: LinksPerEntry[],
compareFn: ((a: LinksPerEntry) => number) | ((a: LinksPerEntry, b: LinksPerEntry) => number),
): LinksPerEntry[] {
const result: LinksPerEntry[] = []

while (left.length > 0 && right.length > 0) {
if (compareFn(left[0], right[0]) <= 0) {
// if 0 it should preserve same order (stable)
result.push(left.shift() as LinksPerEntry)
} else {
result.push(right.shift() as LinksPerEntry)
}
}

if (left.length) result.push(...left)
if (right.length) result.push(...right)

return result
}
27 changes: 27 additions & 0 deletions src/test/helpers/create-changeset-item-with-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AddedChangesetItem } from '../../engine/types'
import { createLinkObject } from '../../engine/utils/create-link-object'
import { EntryField } from 'contentful'
import { EntrySkeletonType } from 'contentful/dist/types/types/query'

export const createChangesetItemWithData = (
contentTypeId: string,
entryId: string,
fields: EntryField<EntrySkeletonType> = {},
): AddedChangesetItem => {
const referencedItem: AddedChangesetItem = createLinkObject(entryId, 'add', 'Entry')
referencedItem.data = {
sys: {
space: { sys: { type: 'Link', linkType: 'Space', id: '529ziq3ce86u' } },
id: entryId,
type: 'Entry',
createdAt: '2023-05-17T10:36:22.538Z',
updatedAt: '2023-05-17T10:36:40.280Z',
environment: { sys: { id: 'master', type: 'Link', linkType: 'Environment' } },
revision: 1,
version: 1,
contentType: { sys: { type: 'Link', linkType: 'ContentType', id: contentTypeId } },
},
fields,
}
return referencedItem
}
117 changes: 117 additions & 0 deletions test/unit/engine/utils/sort-entries-by-reference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { AddedChangesetItem } from '../../../../src/engine/types'
import sortEntries from '../../../../src/engine/utils/sort-entries-by-reference'
import { expect } from 'chai'
import { createChangesetItemWithData } from '../../../../src/test/helpers/create-changeset-item-with-data'

describe('sortEntriesByReference', () => {
const referencedItem: AddedChangesetItem = createChangesetItemWithData('lesson', 'added-entry')
const referencedItem1: AddedChangesetItem = createChangesetItemWithData('lesson', 'added-entry1')
const itemWithOneLink: AddedChangesetItem = createChangesetItemWithData('lesson1', 'added-entry-with-one-link', {
referenceField: {
'en-US': {
sys: { type: 'Link', linkType: 'Entry', id: 'added-entry' },
},
},
})

const itemWithArrayLinks: AddedChangesetItem = createChangesetItemWithData('lesson2', 'added-entry-with-multi-link', {
referencesField: {
'en-US': [
{
sys: { type: 'Link', linkType: 'Entry', id: 'added-entry' },
},
{
sys: { type: 'Link', linkType: 'Entry', id: 'added-entry1' },
},
],
},
})

const itemWithNonExistentLinks: AddedChangesetItem = createChangesetItemWithData(
'lesson3',
'added-entry-with-non-existent-link',
{
referenceField: {
'en-US': {
sys: { type: 'Link', linkType: 'Entry', id: 'random-entry' },
},
},
},
)

const nonReferencedItem = createChangesetItemWithData('lesson', 'non-referenced-entry')

it('should order based on references if the linked entry is present in the changeset', () => {
// one link
expect(sortEntries([itemWithOneLink, referencedItem, referencedItem1])).to.deep.equal([
referencedItem,
referencedItem1,
itemWithOneLink,
])
// multi link
expect(sortEntries([itemWithArrayLinks, referencedItem, referencedItem1])).to.deep.equal([
referencedItem,
referencedItem1,
itemWithArrayLinks,
])
// both
expect(sortEntries([itemWithArrayLinks, itemWithOneLink, referencedItem, referencedItem1])).to.deep.equal([
referencedItem,
referencedItem1,
itemWithOneLink,
itemWithArrayLinks,
])
})

it('should not reorder if the linked entry is not present in the changeset', () => {
// one Link
expect(sortEntries([itemWithOneLink, nonReferencedItem])).to.deep.equal([itemWithOneLink, nonReferencedItem])
// Muli links
expect(sortEntries([itemWithArrayLinks, nonReferencedItem])).to.deep.equal([itemWithArrayLinks, nonReferencedItem])
//both
expect(sortEntries([itemWithArrayLinks, itemWithOneLink, nonReferencedItem])).to.deep.equal([
itemWithArrayLinks,
itemWithOneLink,
nonReferencedItem,
])
})

it('should not reorder there are no links in the changeset', () => {
expect(sortEntries([nonReferencedItem, referencedItem, referencedItem1])).to.deep.equal([
nonReferencedItem,
referencedItem,
referencedItem1,
])
})

it('should reorder only entries with links present in the changeset', () => {
// with links non present in the changeset
expect(sortEntries([itemWithNonExistentLinks, referencedItem, referencedItem1, nonReferencedItem])).to.deep.equal([
itemWithNonExistentLinks,
referencedItem,
referencedItem1,
nonReferencedItem,
])
// With Both links present and not present
expect(
sortEntries([itemWithArrayLinks, referencedItem, referencedItem1, nonReferencedItem, itemWithNonExistentLinks]),
).to.deep.equal([referencedItem, referencedItem1, nonReferencedItem, itemWithNonExistentLinks, itemWithArrayLinks])
expect(
sortEntries([
itemWithArrayLinks,
itemWithOneLink,
itemWithNonExistentLinks,
referencedItem,
referencedItem1,
nonReferencedItem,
]),
).to.deep.equal([
itemWithNonExistentLinks,
referencedItem,
referencedItem1,
nonReferencedItem,
itemWithOneLink,
itemWithArrayLinks,
])
})
})