-
Notifications
You must be signed in to change notification settings - Fork 150
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(content type): New Action to transform entries to another conten…
…t type (#143) ## Summary This PR extends the available migrations with a new "transformEntriesToType"-transform that allows migrations from one content type to another as discussed in #113 ## Description A new action was added to support the transformation. Various enhancements to the OfflineAPI as well as the Fetcher were needed to implement the features. ## Motivation and Context See #113 ## Todos - [x] Basic functionality working - [x] Option to remove the source item after transformation - [x] Option to publish/not publish or keep the state of the original entry - [x] Option to change all references to the original entry so that they point to the new entry instead - [x] Additional tests
- Loading branch information
1 parent
b4fb608
commit f5b3595
Showing
23 changed files
with
998 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const MurmurHash3 = require('imurmurhash'); | ||
|
||
// Basic example: create new content type. | ||
module.exports = function (migration) { | ||
// please run 01-angry-dog first, add some entries | ||
// create new content type | ||
const copycat = migration.createContentType('copycat').name('copy of dog').description('super friendly copy dog'); | ||
|
||
// add field | ||
copycat.createField('woofs', { | ||
name: 'woof woof woof', | ||
type: 'Symbol', | ||
required: true | ||
}); | ||
|
||
// add entry title | ||
copycat.displayField('woofs'); | ||
|
||
migration.transformEntriesToType({ | ||
sourceContentType: 'dog', | ||
targetContentType: 'copycat', | ||
from: ['woofs'], | ||
shouldPublish: false, | ||
updateReferences: true, | ||
removeOldEntries: true, | ||
identityKey: function (fields) { | ||
const value = fields.woofs['en-US'].toString(); | ||
return MurmurHash3(value).result().toString(); | ||
}, | ||
transformEntryForLocale: function (fromFields, currentLocale) { | ||
return { | ||
woofs: `copy - ${fromFields.woofs[currentLocale]}` | ||
}; | ||
} | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import EntryTransformToType from '../interfaces/entry-transform-to-type' | ||
import { APIAction } from './action' | ||
import { OfflineAPI } from '../offline-api' | ||
import Entry from '../entities/entry' | ||
import * as _ from 'lodash' | ||
|
||
class EntryTransformToTypeAction extends APIAction { | ||
private fromFields?: string[] | ||
private sourceContentTypeId: string | ||
private targetContentTypeId: string | ||
private transformEntryForLocale: (inputFields: any, locale: string) => Promise<any> | ||
private identityKey: (fromFields: any) => Promise<string> | ||
private shouldPublish: boolean|'preserve' | ||
private removeOldEntries: boolean | ||
private updateReferences: boolean | ||
|
||
constructor (entryTransformation: EntryTransformToType) { | ||
super() | ||
this.fromFields = entryTransformation.from | ||
this.sourceContentTypeId = entryTransformation.sourceContentType | ||
this.targetContentTypeId = entryTransformation.targetContentType | ||
this.identityKey = entryTransformation.identityKey | ||
this.shouldPublish = entryTransformation.shouldPublish || false | ||
this.removeOldEntries = entryTransformation.removeOldEntries || false | ||
this.updateReferences = entryTransformation.updateReferences || false | ||
this.transformEntryForLocale = entryTransformation.transformEntryForLocale | ||
} | ||
|
||
async applyTo (api: OfflineAPI) { | ||
const entries: Entry[] = await api.getEntriesForContentType(this.sourceContentTypeId) | ||
const locales: string[] = await api.getLocalesForSpace() | ||
|
||
for (const entry of entries) { | ||
const inputs = this.fromFields ? _.pick(entry.fields, this.fromFields) : entry.fields | ||
const newEntryId = await this.identityKey(inputs) | ||
const hasEntry = await api.hasEntry(newEntryId) | ||
|
||
if (hasEntry) { | ||
await api.recordRuntimeError(new Error(`Entry with id '${newEntryId}' already exists`)) | ||
continue | ||
} | ||
|
||
let skipEntry = true | ||
let fieldsForTargetEntry = {} | ||
|
||
for (const locale of locales) { | ||
let outputsForCurrentLocale | ||
try { | ||
outputsForCurrentLocale = await this.transformEntryForLocale(inputs, locale) | ||
} catch (err) { | ||
await api.recordRuntimeError(err) | ||
continue | ||
} | ||
|
||
if (outputsForCurrentLocale === undefined) { | ||
continue | ||
} | ||
|
||
skipEntry = false | ||
|
||
// we collect all the values for the target entry before writing it to the | ||
// offline API because we don't yet know if the entry might be skipped | ||
// TODO: verify that the derivedFields actually get written to | ||
// and to no other field | ||
for (const [fieldId, localizedValue] of _.entries(outputsForCurrentLocale)) { | ||
if (!fieldsForTargetEntry[fieldId]) { | ||
fieldsForTargetEntry[fieldId] = {} | ||
} | ||
fieldsForTargetEntry[fieldId][locale] = localizedValue | ||
} | ||
} | ||
|
||
// if derive returned undefined for all locales of this entry, there are no changes | ||
// to be made, neither on the source nor the target entry, so we move on to the next | ||
if (skipEntry) { | ||
continue | ||
} | ||
|
||
const targetEntry = await api.createEntry(this.targetContentTypeId, newEntryId) | ||
|
||
// we are not skipping this source entry and the target entry does not yet exist, | ||
// so now is the time to write the collected target entry values to the offline API | ||
for (const [fieldId, localizedField] of _.entries(fieldsForTargetEntry)) { | ||
if (!targetEntry.fields[fieldId]) { | ||
targetEntry.setField(fieldId, {}) | ||
} | ||
|
||
for (const [locale, localizedValue] of _.entries(localizedField)) { | ||
targetEntry.setFieldForLocale(fieldId, locale, localizedValue) | ||
} | ||
|
||
} | ||
await api.saveEntry(targetEntry.id) | ||
if (this.shouldPublish === true || (this.shouldPublish === 'preserve' && entry.isPublished) ) { | ||
await api.publishEntry(targetEntry.id) | ||
} | ||
|
||
// look for entries linking to the old entry and replace them with references to the new entry | ||
if (this.updateReferences) { | ||
const links = await api.getLinks(entry.id, locales) | ||
for (const link of links) { | ||
if (!link.isInArray()) { | ||
link.element.setFieldForLocale(link.field, link.locale,{ sys: { id: newEntryId, type: 'Link', linkType: 'Entry'}}) | ||
} else { | ||
link.element.replaceArrayLinkForLocale(link.field, link.locale, link.index, newEntryId) | ||
} | ||
|
||
await api.saveEntry(link.element.id) | ||
if (this.shouldPublish === true || (this.shouldPublish === 'preserve' && link.element.isPublished) ) { | ||
await api.publishEntry(link.element.id) | ||
} | ||
} | ||
} | ||
|
||
// remove the original item | ||
if (this.removeOldEntries) { | ||
if (entry.isPublished) { | ||
await api.unpublishEntry(entry.id) | ||
} | ||
await api.deleteEntry(entry.id) | ||
} | ||
} | ||
} | ||
} | ||
|
||
export { EntryTransformToTypeAction } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import Entry from './entry' | ||
|
||
class Link { | ||
private _field: string | ||
private _locale: string | ||
private _index: number | ||
private _element: Entry | ||
|
||
constructor (element: Entry, field: string, locale: string, index: number = -1) { | ||
this._field = field | ||
this._locale = locale | ||
this._index = index | ||
this._element = element | ||
} | ||
|
||
get field (): string { | ||
return this._field | ||
} | ||
|
||
get locale (): string { | ||
return this._locale | ||
} | ||
|
||
get index (): number { | ||
return this._index | ||
} | ||
|
||
get element (): Entry { | ||
return this._element | ||
} | ||
|
||
isValid (): boolean { | ||
return this._field.length > 0 | ||
} | ||
|
||
isInArray (): boolean { | ||
return this._index !== -1 | ||
} | ||
} | ||
|
||
export { | ||
Link as default, | ||
Link | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.