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: add force flag to import omitted fields [#62] #919

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 33 additions & 25 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import UpdateRenderer from 'listr-update-renderer'
import VerboseRenderer from 'listr-verbose-renderer'
import { startCase } from 'lodash'
import PQueue from 'p-queue'

import { pipe } from 'lodash/fp'
import { displayErrorLog, setupLogging, writeErrorLogFile } from 'contentful-batch-libs/dist/logging'
import { wrapTask } from 'contentful-batch-libs/dist/listr'

Expand All @@ -17,6 +17,9 @@ import transformSpace from './transform/transform-space'
import { assertDefaultLocale, assertPayload } from './utils/validations'
import parseOptions from './parseOptions'
import { ContentfulMultiError, LogItem } from './utils/errors'
import { transformers } from './transform/transformers'
import { ContentTypeProps } from 'contentful-management'
import { forceDeleteOmittedFieldTransform } from './transform/force-delete-omitted-field-transform'

const ONE_SECOND = 1000

Expand All @@ -39,28 +42,29 @@ function createListrOptions (options) {

// These type definitions follow what is specified in the Readme
type RunContentfulImportParams = {
spaceId: string,
environmentId?: string,
managementToken: string,
contentFile?: string,
content?: object,
contentModelOnly?: boolean,
skipContentModel?: boolean,
skipLocales?: boolean,
skipContentPublishing?: boolean,
uploadAssets?: boolean,
assetsDirectory?: string,
host?: string,
proxy?: string,
rawProxy?: string,
rateLimit?: number,
headers?: object,
errorLogFile?: string,
useVerboseRenderer?: boolean,
// TODO These properties are not documented in the Readme
timeout?: number,
retryLimit?: number,
config?: string,
spaceId: string,
environmentId?: string,
managementToken: string,
contentFile?: string,
content?: object,
contentModelOnly?: boolean,
skipContentModel?: boolean,
skipLocales?: boolean,
skipContentPublishing?: boolean,
uploadAssets?: boolean,
assetsDirectory?: string,
host?: string,
proxy?: string,
rawProxy?: string,
rateLimit?: number,
headers?: object,
errorLogFile?: string,
useVerboseRenderer?: boolean,
// TODO These properties are not documented in the Readme
timeout?: number,
retryLimit?: number,
config?: string,
force?: boolean,
}

async function runContentfulImport (params: RunContentfulImportParams) {
Expand Down Expand Up @@ -133,8 +137,12 @@ async function runContentfulImport (params: RunContentfulImportParams) {
{
title: 'Apply transformations to source data',
task: wrapTask(async (ctx) => {
const transformedSourceData = transformSpace(ctx.sourceDataUntransformed, ctx.destinationData)
ctx.sourceData = transformedSourceData
const customTransformers: Partial<typeof transformers> = {}
if (options.force) {
customTransformers.contentTypes = (contentType: ContentTypeProps) => pipe(transformers.contentTypes, forceDeleteOmittedFieldTransform)(contentType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally not the biggest fan of adding the pipe dependency "just" for this, not sure about the benefit, we also just can pass the params through all all transformers instead, no?

}

ctx.sourceData = transformSpace(ctx.sourceDataUntransformed, ctx.destinationData, customTransformers)
})
},
{
Expand Down
9 changes: 9 additions & 0 deletions lib/transform/force-delete-omitted-field-transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ContentTypeProps } from 'contentful-management'

export function forceDeleteOmittedFieldTransform (contentType: ContentTypeProps) {
const omittedFields = contentType.fields.filter(field => field.omitted)
omittedFields.forEach(field => {
contentType.fields = contentType.fields.filter(f => f.id !== field.id)
})
return contentType
}
8 changes: 5 additions & 3 deletions lib/transform/transform-space.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { omit, defaults } from 'lodash/object'
import { omit, defaults } from 'lodash'

import * as defaultTransformers from './transformers'
import { transformers as defaultTransformers } from './transformers'
import sortEntries from '../utils/sort-entries'
import sortLocales from '../utils/sort-locales'
import { DestinationData, OriginalSourceData, TransformedSourceData } from '../types'
Expand All @@ -14,14 +14,16 @@ const spaceEntities = [
* is a need to transform data when copying it to the destination space
*/
export default function (
sourceData: OriginalSourceData, destinationData: DestinationData, customTransformers?: any, entities = spaceEntities
sourceData: OriginalSourceData, destinationData: DestinationData, customTransformers?: Partial<typeof defaultTransformers>, entities = spaceEntities
): TransformedSourceData {
const transformers = defaults(customTransformers, defaultTransformers)
const baseSpaceData = omit(sourceData, ...entities)

sourceData.locales = sortLocales(sourceData.locales)
const tagsEnabled = !!destinationData.tags

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return entities.reduce((transformedSpaceData, type) => {
// tags don't contain links to other entities, don't need to be sorted
const sortedEntities = (type === 'tags') ? sourceData[type] : sortEntries(sourceData[type])
Expand Down
36 changes: 26 additions & 10 deletions lib/transform/transformers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContentTypeProps, EntryProps, TagProps, WebhookProps } from 'contentful-management'
import { find, omit, pick, reduce } from 'lodash'
import { omit, pick, find, reduce } from 'lodash'
import { AssetProps, ContentTypeProps, EntryProps, LocaleProps, TagProps, WebhookProps } from 'contentful-management'
import { MetadataProps } from 'contentful-management/dist/typings/common-types'

/**
* Default transformer methods for each kind of entity.
Expand All @@ -8,19 +9,19 @@ import { find, omit, pick, reduce } from 'lodash'
* as the whole upload process needs to be followed again.
*/

export function contentTypes (contentType: ContentTypeProps) {
function contentTypes (contentType: ContentTypeProps) {
return contentType
}

export function tags (tag: TagProps) {
function tags (tag: TagProps) {
return tag
}

export function entries (entry: EntryProps, _, tagsEnabled = false) {
function entries (entry: EntryProps, _, tagsEnabled = false) {
return removeMetadataTags(entry, tagsEnabled)
}

export function webhooks (webhook: WebhookProps) {
function webhooks (webhook: WebhookProps) {
// Workaround for webhooks with credentials
if (webhook.httpBasicUsername) {
delete webhook.httpBasicUsername
Expand All @@ -34,17 +35,21 @@ export function webhooks (webhook: WebhookProps) {
return webhook
}

export function assets (asset, _, tagsEnabled = false) {
function assets (asset: AssetProps, _, tagsEnabled = false) {
const transformedAsset = omit(asset, 'sys')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transformedAsset.sys = pick(asset.sys, 'id')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transformedAsset.fields = pick(asset.fields, 'title', 'description')
transformedAsset.fields.file = reduce(
asset.fields.file,
(newFile, localizedFile, locale) => {
newFile[locale] = pick(localizedFile, 'contentType', 'fileName')
if (!localizedFile.uploadFrom) {
const assetUrl = localizedFile.url || localizedFile.upload
newFile[locale].upload = `${/^(http|https):\/\//i.test(assetUrl) ? '' : 'https:'}${assetUrl}`
newFile[locale].upload = `${/^(http|https):\/\//i.test(assetUrl!) ? '' : 'https:'}${assetUrl}`
} else {
newFile[locale].uploadFrom = localizedFile.uploadFrom
}
Expand All @@ -55,7 +60,7 @@ export function assets (asset, _, tagsEnabled = false) {
return removeMetadataTags(transformedAsset, tagsEnabled)
}

export function locales (locale, destinationLocales) {
function locales (locale: LocaleProps, destinationLocales: Array<LocaleProps>): LocaleProps {
const transformedLocale = pick(locale, 'code', 'name', 'contentManagementApi', 'contentDeliveryApi', 'fallbackCode', 'optional')
const destinationLocale = find(destinationLocales, { code: locale.code })
if (destinationLocale) {
Expand All @@ -66,12 +71,23 @@ export function locales (locale, destinationLocales) {
transformedLocale.sys = pick(destinationLocale.sys, 'id')
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return transformedLocale
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this ignore here, what's the issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retuned shape only has a subset of data in the sys field (only the id field)

}

function removeMetadataTags (entity, tagsEnabled = false) {
function removeMetadataTags<T extends { metadata?: MetadataProps }> (entity: T, tagsEnabled = false): T {
if (!tagsEnabled) {
delete entity.metadata
}
return entity
}

export const transformers = {
contentTypes,
tags,
entries,
webhooks,
assets,
locales
}
5 changes: 5 additions & 0 deletions lib/usageParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,10 @@ export default yargs
type: 'string',
describe: 'Pass an additional HTTP Header'
})
.option('force', {
describe: 'force omitted fields to be deleted before importing',
type: 'boolean',
default: false
})
.config('config', 'An optional configuration JSON file containing all the options for a single run')
.argv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cloneMock } from 'contentful-batch-libs/test/mocks/'
import { ContentTypeProps } from 'contentful-management'
import { forceDeleteOmittedFieldTransform } from '../../../lib/transform/force-delete-omitted-field-transform'

test('It should transform content types by dropping omitted fields', () => {
const contentTypeMock = cloneMock('contentType') as ContentTypeProps
contentTypeMock.fields[0].omitted = true
expect(contentTypeMock.fields).toHaveLength(1)
const transformedContentTypeMock = forceDeleteOmittedFieldTransform(contentTypeMock)
expect(transformedContentTypeMock.fields).toHaveLength(0)
})
2 changes: 2 additions & 0 deletions test/unit/transform/transform-space.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ test('applies transformers to give space data', () => {

test('applies custom transformers to give space data', () => {
const result = transformSpace(space, destinationSpace, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
entries: () => 'transformed'
})
expect(result.entries?.[0]?.transformed).toBe('transformed')
Expand Down
6 changes: 3 additions & 3 deletions test/unit/transform/transformers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cloneMock } from 'contentful-batch-libs/test/mocks/'

import * as transformers from '../../../lib/transform/transformers'
import { transformers } from '../../../lib/transform/transformers'

const _ = {}

Expand Down Expand Up @@ -60,8 +60,8 @@ test('It should transform unprocessed asset with uploadFrom', () => {
const transformedAsset = transformers.assets(assetMock, _)
expect(transformedAsset.fields.file['en-US'].uploadFrom).toBeTruthy()
expect(transformedAsset.fields.file['de-DE'].uploadFrom).toBeTruthy()
expect(transformedAsset.fields.file['en-US'].uploadFrom.sys.id).toBe(assetMock.fields.file['en-US'].uploadFrom.sys.id)
expect(transformedAsset.fields.file['de-DE'].uploadFrom.sys.id).toBe(assetMock.fields.file['de-DE'].uploadFrom.sys.id)
expect(transformedAsset.fields.file['en-US'].uploadFrom?.sys.id).toBe(assetMock.fields.file['en-US'].uploadFrom.sys.id)
expect(transformedAsset.fields.file['de-DE'].uploadFrom?.sys.id).toBe(assetMock.fields.file['de-DE'].uploadFrom.sys.id)
})

test('It should transform webhook with credentials to normal webhook', () => {
Expand Down