UNPKG

contentful-import

Version:

this tool allows you to import JSON dump exported by contentful-export

1,415 lines (1,390 loc) 48.9 kB
import { __export, __require, version } from "./chunk-GA5GJRGP.mjs"; // lib/index.ts import Table from "cli-table3"; import differenceInSeconds from "date-fns/differenceInSeconds"; import formatDistance from "date-fns/formatDistance"; import Listr2 from "listr"; import UpdateRenderer from "listr-update-renderer"; import VerboseRenderer from "listr-verbose-renderer"; import { startCase } from "lodash"; import PQueue from "p-queue"; import { displayErrorLog, setupLogging, writeErrorLogFile } from "contentful-batch-libs/dist/logging"; import { wrapTask as wrapTask2 } from "contentful-batch-libs/dist/listr"; // lib/tasks/init-client.ts import { createClient } from "contentful-management"; import { logEmitter } from "contentful-batch-libs/dist/logging"; function logHandler(level, data) { logEmitter.emit(level, data); } function initClient(opts) { const defaultOpts = { timeout: 3e4, logHandler }; const config = { ...defaultOpts, ...opts }; return createClient(config); } // lib/tasks/get-destination-data.ts import Promise2 from "bluebird"; import { logEmitter as logEmitter2 } from "contentful-batch-libs/dist/logging"; var BATCH_CHAR_LIMIT = 1990; var BATCH_SIZE_LIMIT = 100; var METHODS = { contentTypes: { name: "content types", method: "getContentTypes" }, locales: { name: "locales", method: "getLocales" }, entries: { name: "entries", method: "getEntries" }, assets: { name: "assets", method: "getAssets" }, tags: { name: "tags", method: "getTags" } }; async function batchedIdQuery({ environment, type, ids, requestQueue }) { const method = METHODS[type].method; const entityTypeName = METHODS[type].name; const batches = getIdBatches(ids); let totalFetched = 0; const allPendingResponses = batches.map((idBatch) => { return requestQueue.add(async () => { const response = await environment[method]({ "sys.id[in]": idBatch, limit: idBatch.split(",").length }); totalFetched = totalFetched + response.items.length; logEmitter2.emit("info", `Fetched ${totalFetched} of ${response.total} ${entityTypeName}`); return response.items; }); }); const responses = await Promise2.all(allPendingResponses); return responses.flat(); } async function batchedPageQuery({ environment, type, requestQueue }) { const method = METHODS[type].method; const entityTypeName = METHODS[type].name; let totalFetched = 0; const { items, total } = await requestQueue.add(async () => { const response = await environment[method]({ skip: 0, limit: BATCH_SIZE_LIMIT }); totalFetched += response.items.length; logEmitter2.emit("info", `Fetched ${totalFetched} of ${response.total} ${entityTypeName}`); return { items: response.items, total: response.total }; }); const batches = getPagedBatches(totalFetched, total); const remainingTotalResponses = batches.map(({ skip }) => { return requestQueue.add(async () => { const response = await environment[method]({ skip, limit: BATCH_SIZE_LIMIT }); totalFetched = totalFetched + response.items.length; logEmitter2.emit("info", `Fetched ${totalFetched} of ${response.total} ${entityTypeName}`); return response.items; }); }); const remainingResponses = await Promise2.all(remainingTotalResponses); return items.concat(remainingResponses.flat()); } function getIdBatches(ids) { const batches = []; let currentBatch = ""; let currentSize = 0; while (ids.length > 0) { const id = ids.splice(0, 1); currentBatch += id; currentSize = currentSize + 1; if (currentSize === BATCH_SIZE_LIMIT || currentBatch.length > BATCH_CHAR_LIMIT || ids.length === 0) { batches.push(currentBatch); currentBatch = ""; currentSize = 0; } else { currentBatch += ","; } } return batches; } function getPagedBatches(totalFetched, total) { const batches = []; if (totalFetched >= total) { return batches; } let skip = totalFetched; while (skip < total) { batches.push({ skip }); skip += BATCH_SIZE_LIMIT; } return batches; } async function getDestinationData({ client, spaceId, environmentId, sourceData, contentModelOnly, skipLocales, skipContentModel, requestQueue }) { const space = await client.getSpace(spaceId); const environment = await space.getEnvironment(environmentId); const result = { contentTypes: [], tags: [], locales: [], entries: [], assets: [] }; sourceData = { ...result, ...sourceData }; if (!skipContentModel) { const contentTypeIds = sourceData.contentTypes?.map((e) => e.sys.id); if (contentTypeIds) { result.contentTypes = batchedIdQuery({ environment, type: "contentTypes", ids: contentTypeIds, requestQueue }); } if (!skipLocales) { const localeIds = sourceData.locales?.map((e) => e.sys.id); if (localeIds && localeIds.length) { result.locales = batchedPageQuery({ environment, type: "locales", requestQueue }); } } } try { result.tags = await batchedPageQuery({ environment, type: "tags", requestQueue }); } catch (_) { delete result.tags; } if (contentModelOnly) { return Promise2.props(result); } const entryIds = sourceData.entries?.map((e) => e.sys.id); const assetIds = sourceData.assets?.map((e) => e.sys.id); if (entryIds) { result.entries = batchedIdQuery({ environment, type: "entries", ids: entryIds, requestQueue }); } if (assetIds) { result.assets = batchedIdQuery({ environment, type: "assets", ids: assetIds, requestQueue }); } result.webhooks = []; return Promise2.props(result); } // lib/tasks/push-to-space/push-to-space.ts import Listr from "listr"; import verboseRenderer from "listr-verbose-renderer"; import { logEmitter as logEmitter6 } from "contentful-batch-libs/dist/logging"; import { wrapTask } from "contentful-batch-libs/dist/listr"; // lib/tasks/push-to-space/assets.ts import fs from "fs"; import { join } from "path"; import { promisify } from "util"; import getEntityName from "contentful-batch-libs/dist/get-entity-name"; import { logEmitter as logEmitter3 } from "contentful-batch-libs/dist/logging"; // lib/utils/errors.ts var ContentfulAssetError = class extends Error { constructor(message, filePath) { super(message); this.filePath = filePath; } }; var ContentfulEntityError = class extends Error { }; var ContentfulMultiError = class extends Error { }; // lib/tasks/push-to-space/assets.ts var stat = promisify(fs.stat); async function getAssetStreamForURL(url, assetsDirectory) { const [, assetPath] = url.split("//"); const filePath = join(assetsDirectory, assetPath); try { await stat(filePath); return fs.createReadStream(filePath); } catch (err) { const error = new ContentfulAssetError( "Cannot open asset from filesystem", filePath ); throw error; } } async function processAssetForLocale(locale, asset, processingOptions) { try { return await asset.processForLocale(locale, processingOptions); } catch (err) { if (err instanceof ContentfulEntityError) { err.entity = asset; } logEmitter3.emit("error", err); throw err; } } async function lastResult(promises) { if (!promises.length) throw new RangeError("No last result from no promises"); const results = []; await Promise.all( promises.map( (p) => p.then((v) => { results.push(v); }) ) ); return results[results.length - 1]; } async function processAssets({ assets: assets2, timeout, retryLimit, requestQueue }) { const processingOptions = Object.assign( {}, timeout && { processingCheckWait: timeout }, retryLimit && { processingCheckRetry: retryLimit } ); const pendingProcessingAssets = assets2.map(async (asset) => { logEmitter3.emit("info", `Processing Asset ${getEntityName(asset)}`); const locales2 = Object.keys(asset.fields.file || {}); let latestAssetVersion = asset; try { latestAssetVersion = await lastResult( locales2.map((locale) => { return requestQueue.add( () => processAssetForLocale(locale, asset, processingOptions) ); }) ); } catch (err) { return null; } return latestAssetVersion; }); const potentiallyProcessedAssets = await Promise.all(pendingProcessingAssets); return potentiallyProcessedAssets.filter((asset) => asset); } // lib/tasks/push-to-space/creation.ts import { find } from "lodash/collection"; import { assign, get, omitBy, omit } from "lodash/object"; import getEntityName2 from "contentful-batch-libs/dist/get-entity-name"; import { logEmitter as logEmitter4 } from "contentful-batch-libs/dist/logging"; function createEntities({ context, entities, destinationEntitiesById, skipUpdates, requestQueue }) { return createEntitiesWithConcurrency({ context, entities, destinationEntitiesById, skipUpdates, requestQueue }); } function createLocales({ context, entities, destinationEntitiesById, requestQueue }) { return createEntitiesInSequence({ context, entities, destinationEntitiesById, requestQueue }); } async function createEntitiesWithConcurrency({ context, entities, destinationEntitiesById, skipUpdates, requestQueue }) { const pendingCreatedEntities = entities.map((entity) => { const destinationEntity = getDestinationEntityForSourceEntity(destinationEntitiesById, entity.transformed); const updateOperation = skipUpdates ? "skip" : "update"; const operation = destinationEntity ? updateOperation : "create"; if (destinationEntity && skipUpdates) { creationSuccessNotifier(operation, entity.transformed); return; } return requestQueue.add(async () => { try { const createdEntity = await (destinationEntity ? updateDestinationWithSourceData(destinationEntity, entity.transformed) : createInDestination(context, entity.transformed)); creationSuccessNotifier(operation, createdEntity); return createdEntity; } catch (err) { return handleCreationErrors(entity, err); } }); }); const createdEntities = await Promise.all(pendingCreatedEntities); return createdEntities.filter((entity) => entity); } async function createEntitiesInSequence({ context, entities, destinationEntitiesById, requestQueue }) { const createdEntities = []; for (const entity of entities) { const destinationEntity = getDestinationEntityForSourceEntity(destinationEntitiesById, entity.transformed); const operation = destinationEntity ? "update" : "create"; try { const createdEntity = await requestQueue.add(async () => { const createdOrUpdatedEntity = await (destinationEntity ? updateDestinationWithSourceData(destinationEntity, entity.transformed) : createInDestination(context, entity.transformed)); return createdOrUpdatedEntity; }); creationSuccessNotifier(operation, createdEntity); createdEntities.push(createdEntity); } catch (err) { const maybeSubstituteEntity = handleCreationErrors(entity, err); if (maybeSubstituteEntity) { createdEntities.push(maybeSubstituteEntity); } } } return createdEntities; } async function createEntries({ context, entities, destinationEntitiesById, skipUpdates, requestQueue }) { const createdEntries = await Promise.all(entities.map((entry) => { return createEntry({ entry, target: context.target, skipContentModel: context.skipContentModel, destinationEntitiesById, skipUpdates, requestQueue }); })); return createdEntries.filter((entry) => entry); } async function createEntry({ entry, target, skipContentModel, destinationEntitiesById, skipUpdates, requestQueue }) { const contentTypeId = entry.original.sys.contentType.sys.id; const destinationEntry = getDestinationEntityForSourceEntity( destinationEntitiesById, entry.transformed ); const updateOperation = skipUpdates ? "skip" : "update"; const operation = destinationEntry ? updateOperation : "create"; if (destinationEntry && skipUpdates) { creationSuccessNotifier(operation, entry.transformed); return entry.transformed; } try { const createdOrUpdatedEntry = await requestQueue.add(() => { return destinationEntry ? updateDestinationWithSourceData(destinationEntry, entry.transformed) : createEntryInDestination(target, contentTypeId, entry.transformed); }); creationSuccessNotifier(operation, createdOrUpdatedEntry); return createdOrUpdatedEntry; } catch (err) { if (err instanceof Error) { if (skipContentModel && err.name === "UnknownField") { const errors = get(JSON.parse(err.message), "details.errors"); entry.transformed.fields = cleanupUnknownFields(entry.transformed.fields, errors); return createEntry({ entry, target, skipContentModel, destinationEntitiesById, skipUpdates, requestQueue }); } } if (err instanceof ContentfulEntityError) { err.entity = entry; } logEmitter4.emit("error", err); return null; } } function updateDestinationWithSourceData(destinationEntity, sourceEntity) { const plainData = getPlainData(sourceEntity); assign(destinationEntity, plainData); return destinationEntity.update(); } function createInDestination(context, sourceEntity) { const { type, target } = context; if (type === "Tag") { return createTagInDestination(context, sourceEntity); } const id = get(sourceEntity, "sys.id"); const plainData = getPlainData(sourceEntity); return id ? target[`create${type}WithId`](id, plainData) : target[`create${type}`](plainData); } function createEntryInDestination(space, contentTypeId, sourceEntity) { const id = sourceEntity.sys.id; const plainData = getPlainData(sourceEntity); return id ? space.createEntryWithId(contentTypeId, id, plainData) : space.createEntry(contentTypeId, plainData); } function createTagInDestination(context, sourceEntity) { const id = sourceEntity.sys.id; const visibility = sourceEntity.sys.visibility || "private"; const name = sourceEntity.name; return context.target.createTag(id, name, visibility); } function handleCreationErrors(entity, err) { if (get(err, "error.sys.id") === "ValidationFailed") { const errors = get(err, "error.details.errors"); if (errors && errors.length > 0 && errors[0].name === "taken") { return entity; } } err.entity = entity.original; logEmitter4.emit("error", err); return null; } function cleanupUnknownFields(fields, errors) { return omitBy(fields, (field, fieldId) => { return find(errors, (error) => { const [, errorFieldId] = error.path; return error.name === "unknown" && errorFieldId === fieldId; }); }); } function getDestinationEntityForSourceEntity(destinationEntitiesById, sourceEntity) { return destinationEntitiesById.get(get(sourceEntity, "sys.id")) || null; } function creationSuccessNotifier(method, createdEntity) { logEmitter4.emit("info", `${method.toUpperCase()} ${createdEntity.sys.type} ${getEntityName2(createdEntity)}`); return createdEntity; } function getPlainData(entity) { const data = entity.toPlainObject ? entity.toPlainObject() : entity; return omit(data, "sys"); } // lib/tasks/push-to-space/publishing.ts import getEntityName3 from "contentful-batch-libs/dist/get-entity-name"; import { logEmitter as logEmitter5 } from "contentful-batch-libs/dist/logging"; async function publishEntities({ entities, requestQueue }) { const entitiesToPublish = entities.filter((entity2) => { if (!entity2 || !entity2.publish) { logEmitter5.emit("warning", `Unable to publish ${getEntityName3(entity2)}`); return false; } return true; }); if (entitiesToPublish.length === 0) { logEmitter5.emit("info", "Skipping publishing since zero valid entities passed"); return []; } const entity = entities[0].original || entities[0]; const type = entity.sys.type || "unknown type"; logEmitter5.emit("info", `Publishing ${entities.length} ${type}s`); const result = await runQueue(entitiesToPublish, [], requestQueue); logEmitter5.emit("info", `Successfully published ${result.length} ${type}s`); return result; } async function archiveEntities({ entities, requestQueue }) { const entitiesToArchive = entities.filter((entity2) => { if (!entity2 || !entity2.archive) { logEmitter5.emit("warning", `Unable to archive ${getEntityName3(entity2)}`); return false; } return true; }); if (entitiesToArchive.length === 0) { logEmitter5.emit("info", "Skipping archiving since zero valid entities passed"); return []; } const entity = entities[0].original || entities[0]; const type = entity.sys.type || "unknown type"; logEmitter5.emit("info", `Archiving ${entities.length} ${type}s`); const pendingArchivedEntities = entitiesToArchive.map((entity2) => { return requestQueue.add(async () => { try { const archivedEntity = await entity2.archive(); return archivedEntity; } catch (err) { if (err instanceof ContentfulEntityError) { err.entity = entity2; } logEmitter5.emit("error", err); return null; } }); }); const allPossiblyArchivedEntities = await Promise.all(pendingArchivedEntities); const allArchivedEntities = allPossiblyArchivedEntities.filter((entity2) => entity2); logEmitter5.emit("info", `Successfully archived ${allArchivedEntities.length} ${type}s`); return allArchivedEntities; } async function runQueue(queue, result = [], requestQueue) { const publishedEntities = []; for (const entity of queue) { logEmitter5.emit("info", `Publishing ${entity.sys.type} ${getEntityName3(entity)}`); try { const publishedEntity = await requestQueue.add(() => entity.publish()); publishedEntities.push(publishedEntity); } catch (err) { if (err instanceof ContentfulEntityError) { err.entity = entity; } logEmitter5.emit("error", err); } } result = [ ...result, ...publishedEntities ]; const publishedEntityIds = new Set(publishedEntities.map((entity) => entity.sys.id)); const unpublishedEntities = queue.filter((entity) => !publishedEntityIds.has(entity.sys.id)); if (unpublishedEntities.length > 0) { if (queue.length === unpublishedEntities.length) { const unpublishedEntityNames = unpublishedEntities.map(getEntityName3).join(", "); logEmitter5.emit("error", `Could not publish the following entities: ${unpublishedEntityNames}`); } else { return runQueue(unpublishedEntities, result, requestQueue); } } return result; } // lib/tasks/push-to-space/push-to-space.ts var DEFAULT_CONTENT_STRUCTURE = { entries: [], assets: [], contentTypes: [], tags: [], locales: [], webhooks: [], editorInterfaces: [] }; function pushToSpace({ sourceData, destinationData = {}, client, spaceId, environmentId, contentModelOnly, skipContentModel, skipContentUpdates, skipLocales, skipContentPublishing, timeout, retryLimit, listrOptions, uploadAssets, skipAssetUpdates, assetsDirectory, requestQueue }) { sourceData = { ...DEFAULT_CONTENT_STRUCTURE, ...sourceData }; destinationData = { ...DEFAULT_CONTENT_STRUCTURE, ...destinationData }; listrOptions = listrOptions || { renderer: verboseRenderer }; const destinationDataById = {}; for (const [entityType, entities] of Object.entries(destinationData)) { const entitiesById = /* @__PURE__ */ new Map(); for (const entity of entities) { entitiesById.set(entity.sys.id, entity); } destinationDataById[entityType] = entitiesById; } return new Listr([ { title: "Connecting to space", task: wrapTask(async (ctx) => { const space = await client.getSpace(spaceId); const environment = await space.getEnvironment(environmentId); ctx.space = space; ctx.environment = environment; }) }, { title: "Importing Locales", task: wrapTask(async (ctx) => { if (!destinationDataById.locales) { return; } const locales2 = await createLocales({ context: { target: ctx.environment, type: "Locale" }, entities: sourceData.locales, destinationEntitiesById: destinationDataById.locales, requestQueue }); ctx.data.locales = locales2; }), skip: () => skipContentModel || skipLocales }, { title: "Importing Content Types", task: wrapTask(async (ctx) => { if (!destinationDataById.contentTypes) { return; } const contentTypes2 = await createEntities({ context: { target: ctx.environment, type: "ContentType" }, entities: sourceData.contentTypes, destinationEntitiesById: destinationDataById.contentTypes, skipUpdates: false, requestQueue }); ctx.data.contentTypes = contentTypes2; }), skip: () => skipContentModel }, { title: "Publishing Content Types", task: wrapTask(async (ctx) => { const publishedContentTypes = await publishEntities2({ entities: ctx.data.contentTypes, sourceEntities: sourceData.contentTypes, requestQueue }); ctx.data.contentTypes = publishedContentTypes; }), skip: () => skipContentModel }, { title: "Importing Tags", task: wrapTask(async (ctx) => { if (sourceData.tags && destinationDataById.tags) { const tags2 = await createEntities({ context: { target: ctx.environment, type: "Tag" }, entities: sourceData.tags, destinationEntitiesById: destinationDataById.tags, skipUpdates: false, requestQueue }); ctx.data.tags = tags2; } }), // we remove `tags` from destination data if an error was thrown trying to access them // this means the user doesn't have access to this feature, skip importing tags skip: () => !destinationDataById.tags }, { title: "Importing Editor Interfaces", task: wrapTask(async (ctx) => { const allEditorInterfacesBeingFetched = ctx.data.contentTypes.map(async (contentType) => { if (!sourceData.editorInterfaces) { return; } const editorInterface = sourceData.editorInterfaces.find((editorInterface2) => { return editorInterface2.sys.contentType.sys.id === contentType.sys.id; }); if (!editorInterface) { return; } try { const ctEditorInterface = await requestQueue.add(() => ctx.environment.getEditorInterfaceForContentType(contentType.sys.id)); logEmitter6.emit("info", `Fetched editor interface for ${contentType.name}`); ctEditorInterface.controls = editorInterface.controls; ctEditorInterface.groupControls = editorInterface.groupControls; ctEditorInterface.editorLayout = editorInterface.editorLayout; ctEditorInterface.sidebar = editorInterface.sidebar; ctEditorInterface.editors = editorInterface.editors; const updatedEditorInterface = await requestQueue.add(() => ctEditorInterface.update()); return updatedEditorInterface; } catch (err) { if (err instanceof ContentfulEntityError) { err.entity = editorInterface; } throw err; } }); const allEditorInterfaces = await Promise.all(allEditorInterfacesBeingFetched); const editorInterfaces = allEditorInterfaces.filter((editorInterface) => editorInterface); ctx.data.editorInterfaces = editorInterfaces; }), skip: (ctx) => skipContentModel || ctx.data.contentTypes.length === 0 }, { title: "Uploading Assets", task: wrapTask(async (ctx) => { const allPendingUploads = []; for (const asset of sourceData.assets) { for (const file of Object.values(asset.transformed.fields.file)) { allPendingUploads.push(requestQueue.add(async () => { try { logEmitter6.emit("info", `Uploading Asset file ${file.upload}`); const assetStream = await getAssetStreamForURL(file.upload, assetsDirectory); const upload = await ctx.environment.createUpload({ fileName: asset.transformed.sys.id, file: assetStream }); delete file.upload; file.uploadFrom = { sys: { type: "Link", linkType: "Upload", id: upload.sys.id } }; return upload; } catch (err) { logEmitter6.emit("error", err); } })); } } const uploads = await Promise.all(allPendingUploads); ctx.data.uploadedAssetFiles = uploads; }), skip: () => !uploadAssets || !sourceData.assets.length }, { title: "Importing Assets", task: wrapTask(async (ctx) => { if (!destinationDataById.assets) { return; } const assetsToProcess = await createEntities({ context: { target: ctx.environment, type: "Asset" }, entities: sourceData.assets, destinationEntitiesById: destinationDataById.assets, skipUpdates: skipAssetUpdates, requestQueue }); const processedAssets = await processAssets({ assets: assetsToProcess, timeout, retryLimit, requestQueue }); ctx.data.assets = processedAssets; }), skip: () => contentModelOnly }, { title: "Publishing Assets", task: wrapTask(async (ctx) => { const publishedAssets = await publishEntities2({ entities: ctx.data.assets, sourceEntities: sourceData.assets, requestQueue }); ctx.data.publishedAssets = publishedAssets; }), skip: () => contentModelOnly || skipContentPublishing }, { title: "Archiving Assets", task: wrapTask(async (ctx) => { const archivedAssets = await archiveEntities2({ entities: ctx.data.assets, sourceEntities: sourceData.assets, requestQueue }); ctx.data.archivedAssets = archivedAssets; }), skip: () => contentModelOnly || skipContentPublishing }, { title: "Importing Content Entries", task: wrapTask(async (ctx) => { const entries2 = await createEntries({ context: { target: ctx.environment, skipContentModel }, entities: sourceData.entries, destinationEntitiesById: destinationDataById.entries, skipUpdates: skipContentUpdates, requestQueue }); ctx.data.entries = entries2; }), skip: () => contentModelOnly }, { title: "Publishing Content Entries", task: wrapTask(async (ctx) => { const publishedEntries = await publishEntities2({ entities: ctx.data.entries, sourceEntities: sourceData.entries, requestQueue }); ctx.data.publishedEntries = publishedEntries; }), skip: () => contentModelOnly || skipContentPublishing }, { title: "Archiving Entries", task: wrapTask(async (ctx) => { const archivedEntries = await archiveEntities2({ entities: ctx.data.entries, sourceEntities: sourceData.entries, requestQueue }); ctx.data.archivedEntries = archivedEntries; }), skip: () => contentModelOnly || skipContentPublishing }, { title: "Creating Web Hooks", task: wrapTask(async (ctx) => { if (!sourceData.webhooks || !destinationDataById.webhooks) { return; } const webhooks2 = await createEntities({ context: { target: ctx.space, type: "Webhook" }, entities: sourceData.webhooks, destinationEntitiesById: destinationDataById.webhooks, requestQueue }); ctx.data.webhooks = webhooks2; }), skip: () => contentModelOnly || environmentId !== "master" && "Webhooks can only be imported in master environment" } ], listrOptions); } function archiveEntities2({ entities, sourceEntities, requestQueue }) { const entityIdsToArchive = sourceEntities.filter(({ original }) => original.sys.archivedVersion).map(({ original }) => original.sys.id); const entitiesToArchive = entities.filter((entity) => entityIdsToArchive.indexOf(entity.sys.id) !== -1); return archiveEntities({ entities: entitiesToArchive, requestQueue }); } function publishEntities2({ entities, sourceEntities, requestQueue }) { const entityIdsToPublish = sourceEntities.filter(({ original }) => original.sys.publishedVersion).map(({ original }) => original.sys.id); const entitiesToPublish = entities.filter((entity) => entityIdsToPublish.indexOf(entity.sys.id) !== -1); return publishEntities({ entities: entitiesToPublish, requestQueue }); } // lib/transform/transform-space.ts import { omit as omit3, defaults } from "lodash/object"; // lib/transform/transformers.ts var transformers_exports = {}; __export(transformers_exports, { assets: () => assets, contentTypes: () => contentTypes, entries: () => entries, locales: () => locales, tags: () => tags, webhooks: () => webhooks }); import { find as find2, omit as omit2, pick, reduce } from "lodash"; function contentTypes(contentType) { return contentType; } function tags(tag) { return tag; } function entries(entry, _, tagsEnabled = false) { return removeMetadataTags(entry, tagsEnabled); } function webhooks(webhook) { if (webhook.httpBasicUsername) { delete webhook.httpBasicUsername; } if (webhook.headers) { webhook.headers = webhook.headers.filter((header) => !header.secret); } return webhook; } function assets(asset, _, tagsEnabled = false) { const transformedAsset = omit2(asset, "sys"); transformedAsset.sys = pick(asset.sys, "id"); 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}`; } else { newFile[locale].uploadFrom = localizedFile.uploadFrom; } return newFile; }, {} ); return removeMetadataTags(transformedAsset, tagsEnabled); } function locales(locale, destinationLocales) { const transformedLocale = pick(locale, "code", "name", "contentManagementApi", "contentDeliveryApi", "fallbackCode", "optional"); const destinationLocale = find2(destinationLocales, { code: locale.code }); if (destinationLocale) { transformedLocale.sys = pick(destinationLocale.sys, "id"); } return transformedLocale; } function removeMetadataTags(entity, tagsEnabled = false) { if (!tagsEnabled) { delete entity.metadata; } return entity; } // lib/utils/sort-entries.ts import { some, filter, map } from "lodash/collection"; import * as _o from "lodash/object"; import { flatten } from "lodash/array"; function sortEntries(entries2) { const linkedEntries = getLinkedEntries(entries2); const mergedLinkedEntries = mergeSort(linkedEntries, (a) => { return hasLinkedIndexesInFront(a); }); return map(mergedLinkedEntries, (linkInfo) => entries2[linkInfo.index]); function hasLinkedIndexesInFront(item) { if (hasLinkedIndexes(item)) { return some(item.linkIndexes, (index) => index > item.index) ? 1 : -1; } return 0; } function hasLinkedIndexes(item) { return item.linkIndexes.length > 0; } } function getLinkedEntries(entries2) { return map(entries2, function(entry) { const entryIndex = entries2.indexOf(entry); const rawLinks = map(entry.fields, (field) => { field = _o.values(field)[0]; if (isEntryLink(field)) { return getFieldEntriesIndex(field, entries2); } else if (isEntityArray(field) && isEntryLink(field[0])) { return map(field, (item) => getFieldEntriesIndex(item, entries2)); } }); return { index: entryIndex, linkIndexes: filter(flatten(rawLinks), (index) => index >= 0) }; }); } function getFieldEntriesIndex(field, entries2) { const id = _o.get(field, "sys.id"); return entries2.findIndex((entry) => entry.sys.id === id); } function isEntryLink(item) { return _o.get(item, "sys.type") === "Entry" || _o.get(item, "sys.linkType") === "Entry"; } function isEntityArray(item) { return Array.isArray(item) && item.length > 0 && _o.has(item[0], "sys"); } function mergeSort(arr, compareFn) { 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, b) { return a < b ? -1 : a > b ? 1 : 0; } function merge(left, right, compareFn) { const result = []; while (left.length && right.length) { if (compareFn(left[0], right[0]) <= 0) { result.push(left.shift()); } else { result.push(right.shift()); } } if (left.length) result.push(...left); if (right.length) result.push(...right); return result; } // lib/utils/sort-locales.ts function sortLocales(locales2) { const localeByFallback = {}; locales2.forEach((locale) => { if (locale.fallbackCode === null) { locale.fallbackCode = void 0; } if (!localeByFallback[locale.fallbackCode]) { localeByFallback[locale.fallbackCode] = []; } localeByFallback[locale.fallbackCode].push(locale); }); return sortByFallbackKey(localeByFallback); } function sortByFallbackKey(localeByFallback, key) { if (!localeByFallback[`${key}`]) { return []; } const sortedLocales = localeByFallback[`${key}`]; sortedLocales.forEach((locale) => { sortByFallbackKey(localeByFallback, locale.code).forEach( (x) => sortedLocales.push(x) ); }); sortedLocales.forEach((locale) => { if (!locale.fallbackCode) { locale.fallbackCode = null; } }); return sortedLocales; } // lib/transform/transform-space.ts var spaceEntities = [ "contentTypes", "entries", "assets", "locales", "webhooks", "tags" ]; function transform_space_default(sourceData, destinationData, customTransformers, entities = spaceEntities) { const transformers = defaults(customTransformers, transformers_exports); const baseSpaceData = omit3(sourceData, ...entities); sourceData.locales = sortLocales(sourceData.locales); const tagsEnabled = !!destinationData.tags; return entities.reduce((transformedSpaceData, type) => { const sortedEntities = type === "tags" ? sourceData[type] : sortEntries(sourceData[type]); const transformedEntities = sortedEntities.map((entity) => ({ original: entity, transformed: transformers[type](entity, destinationData[type], tagsEnabled) })); transformedSpaceData[type] = transformedEntities; return transformedSpaceData; }, baseSpaceData); } // lib/utils/schema.ts import Joi from "joi"; var entrySchema = { sys: Joi.object(), fields: Joi.object() }; var tagSchema = { name: Joi.string().required(), sys: Joi.object() }; var contentTypeSchema = { sys: Joi.object(), fields: Joi.array().required().items(Joi.object().keys({ id: Joi.string().required(), name: Joi.string().required(), type: Joi.string().required().regex(/^Symbol|Text|Integer|Number|Date|Object|Boolean|Array|Link|Location$/), validations: Joi.array(), disabled: Joi.boolean(), omitted: Joi.boolean(), required: Joi.boolean(), localized: Joi.boolean(), linkType: Joi.string().when("type", { is: "Link", then: Joi.string().regex(/^Asset|Entry$/), otherwise: Joi.forbidden() }) })) }; var assetSchema = { sys: Joi.object(), fields: Joi.object({ file: Joi.object().pattern(/.+/, Joi.object({ url: Joi.string().required(), details: Joi.object({ size: Joi.number(), image: Joi.object({ width: Joi.number(), height: Joi.number() }) }), fileName: Joi.string().required(), contentType: Joi.string().required() })) }).required() }; var editorInterfaceSchema = { sys: Joi.object(), controls: Joi.array().items({ fieldId: Joi.string(), widgetId: Joi.string() }) }; var localeSchema = { name: Joi.string().required(), internal_code: Joi.string(), code: Joi.string().required(), fallbackCode: Joi.string().allow(null), default: Joi.boolean(), contentManagementApi: Joi.boolean(), contentDeliveryApi: Joi.boolean(), optional: Joi.boolean(), sys: Joi.object() }; var webhookSchema = { name: Joi.string(), url: Joi.string().replace(/{[^}{]+?}/g, "x").regex(/^https?:\/\/[^ /}{][^ }{]*$/i).required(), topics: Joi.array().required(), httpBasicUsername: Joi.string().allow("", null) }; var payloadSchema = Joi.object({ entries: Joi.array().items(entrySchema), contentTypes: Joi.array().items(contentTypeSchema), tags: Joi.array().items(tagSchema), assets: Joi.array().items(assetSchema), locales: Joi.array().items(localeSchema), editorInterfaces: Joi.array().items(editorInterfaceSchema), webhooks: Joi.array().items(webhookSchema) }); // lib/utils/validations.ts import getEntityName4 from "contentful-batch-libs/dist/get-entity-name"; var attachEntityName = (details, payload) => { details.map((detail) => { if (detail.path.length >= 2) { detail.entity = getEntityName4(payload[detail.path[0]][detail.path[1]]); } return detail; }); }; var countInvalidEntities = (validationData) => { const entityCount = validationData.reduce((entities, currentDetail) => { if (!entities[currentDetail.path[0]]) { entities[currentDetail.path[0]] = 1; } else { entities[currentDetail.path[0]]++; } return entities; }, {}); return Object.keys(entityCount).map((key) => `${key}:${entityCount[key]}`); }; var assertPayload = (payload) => { const result = payloadSchema.validate(payload, { allowUnknown: true, abortEarly: false }); if (result.error) { attachEntityName(result.error.details, payload); const invalidEntityCount = countInvalidEntities(result.error.details).join(", "); result.error.message = `${invalidEntityCount} - Get further details in the error log file`; delete result.error._original; throw result.error; } }; var assertDefaultLocale = (source, destination) => { const sourceDefaultLocale = source.locales.find((locale) => locale.default === true); const destinationDefaultLocale = destination.locales.find((locale) => locale.default === true); if (!sourceDefaultLocale || !destinationDefaultLocale) { return; } if (sourceDefaultLocale.code !== destinationDefaultLocale.code) { throw new Error(` Please make sure the destination space have the same default locale as the source Default locale for source space : ${sourceDefaultLocale.code} Default locale for destination space: ${destinationDefaultLocale.code} `); } }; // lib/parseOptions.ts import fs2 from "fs"; import { resolve } from "path"; import format from "date-fns/format"; // lib/utils/headers.ts function getHeadersConfig(value) { if (!value) { return {}; } const values2 = Array.isArray(value) ? value : [value]; return values2.reduce((headers, value2) => { value2 = value2.trim(); const separatorIndex = value2.indexOf(":"); if (separatorIndex === -1) { return headers; } const headerKey = value2.slice(0, separatorIndex).trim(); const headerValue = value2.slice(separatorIndex + 1).trim(); return { ...headers, [headerKey]: headerValue }; }, {}); } // lib/parseOptions.ts import { proxyStringToObject, agentFromProxy } from "contentful-batch-libs/dist/proxy"; import addSequenceHeader from "contentful-batch-libs/dist/add-sequence-header"; import { parseChunked } from "@discoveryjs/json-ext"; var SUPPORTED_ENTITY_TYPES = [ "contentTypes", "tags", "entries", "assets", "locales", "webhooks", "editorInterfaces" ]; async function parseOptions(params) { const defaultOptions = { skipContentModel: false, skipLocales: false, skipContentPublishing: false, skipAssetUpdates: false, skipContentUpdates: false, useVerboseRenderer: false, environmentId: "master", rawProxy: false, uploadAssets: false, rateLimit: 7 }; const configFile = params.config ? __require(resolve(process.cwd(), params.config)) : {}; const options = { ...defaultOptions, ...configFile, ...params, headers: addSequenceHeader(params.headers || getHeadersConfig(params.header)) }; if (!options.spaceId) { throw new Error("The `spaceId` option is required."); } if (!options.managementToken) { throw new Error("The `managementToken` option is required."); } if (!options.contentFile && !options.content) { throw new Error("Either the `contentFile` or `content` option are required."); } if (options.contentModelOnly && options.skipContentModel) { throw new Error("`contentModelOnly` and `skipContentModel` cannot be used together"); } if (options.skipLocales && !options.contentModelOnly) { throw new Error("`skipLocales` can only be used together with `contentModelOnly`"); } const proxySimpleExp = /.+:\d+/; const proxyAuthExp = /.+:.+@.+:\d+/; if (typeof options.proxy === "string" && options.proxy && !(proxySimpleExp.test(options.proxy) || proxyAuthExp.test(options.proxy))) { throw new Error("Please provide the proxy config in the following format:\nhost:port or user:password@host:port"); } options.startTime = /* @__PURE__ */ new Date(); if (!options.errorLogFile) { options.errorLogFile = resolve(process.cwd(), `contentful-import-error-log-${options.spaceId}-${format(options.startTime, "yyyy-MM-dd'T'HH-mm-ss")}.json`); } else { options.errorLogFile = resolve(process.cwd(), options.errorLogFile); } options.accessToken = options.managementToken; if (!options.content) { const fileStream = fs2.createReadStream(options.contentFile, { encoding: "utf8" }); options.content = await parseChunked(fileStream); } Object.keys(options.content).forEach((type) => { if (SUPPORTED_ENTITY_TYPES.indexOf(type) === -1) { delete options.content[type]; } }); SUPPORTED_ENTITY_TYPES.forEach((type) => { options.content[type] = options.content[type] || []; }); if (typeof options.proxy === "string") { options.proxy = proxyStringToObject(options.proxy); } if (!options.rawProxy && options.proxy) { options.httpsAgent = agentFromProxy(options.proxy); delete options.proxy; } options.application = options.managementApplication || `contentful.import/${version}`; options.feature = options.managementFeature || "library-import"; return options; } // lib/index.ts var ONE_SECOND = 1e3; var tableOptions = { // remove ANSI color codes for better CI/CD compatibility style: { head: [], border: [] } }; function createListrOptions(options) { if (options.useVerboseRenderer) { return { renderer: VerboseRenderer }; } return { renderer: UpdateRenderer, collapse: false }; } async function runContentfulImport(params) { const log = []; const options = await parseOptions(params); const listrOptions = createListrOptions(options); const requestQueue = new PQueue({ interval: ONE_SECOND, intervalCap: options.rateLimit, carryoverConcurrencyCount: true }); setupLogging(log); const infoTable = new Table(tableOptions); infoTable.push([{ colSpan: 2, content: "The following entities are going to be imported:" }]); Object.keys(options.content).forEach((type) => { if (options.skipLocales && type === "locales") { return; } if (options.skipContentModel && ["contentTypes", "editorInterfaces"].indexOf(type) >= 0) { return; } if (options.contentModelOnly && !(["contentTypes", "editorInterfaces", "locales"].indexOf(type) >= 0)) { return; } infoTable.push([startCase(type), options.content[type].length]); }); console.log(infoTable.toString()); const tasks = new Listr2([ { title: "Validating content-file", task: () => { assertPayload(options.content); } }, { title: "Initialize client", task: wrapTask2(async (ctx) => { ctx.client = initClient({ ...options, content: void 0 }); }) }, { title: "Checking if destination space already has any content and retrieving it", task: wrapTask2(async (ctx) => { const destinationData = await getDestinationData({ client: ctx.client, spaceId: options.spaceId, environmentId: options.environmentId, sourceData: options.content, skipLocales: options.skipLocales, skipContentModel: options.skipContentModel, requestQueue }); ctx.sourceDataUntransformed = options.content; ctx.destinationData = destinationData; assertDefaultLocale(ctx.sourceDataUntransformed, ctx.destinationData); }) }, { title: "Apply transformations to source data", task: wrapTask2(async (ctx) => { const transformedSourceData = transform_space_default(ctx.sourceDataUntransformed, ctx.destinationData); ctx.sourceData = transformedSourceData; }) }, { title: "Push content to destination space", task: (ctx) => { return pushToSpace({ sourceData: ctx.sourceData, destinationData: ctx.destinationData, client: ctx.client, spaceId: options.spaceId, environmentId: options.environmentId, contentModelOnly: options.contentModelOnly, skipLocales: options.skipLocales, skipContentModel: options.skipContentModel, skipContentPublishing: options.skipContentPublishing, skipAssetUpdates: options.skipAssetUpdates, skipContentUpdates: options.skipContentUpdates, timeout: options.timeout, retryLimit: options.retryLimit, uploadAssets: options.uploadAssets, assetsDirectory: options.assetsDirectory, listrOptions, requestQueue }); } } ], listrOptions); return tasks.run({ data: {} }).then((ctx) => { console.log("Finished importing all data"); const resultTypes = Object.keys(ctx.data); if (resultTypes.length) { const resultTable = new Table(tableOptions); resultTable.push([{ colSpan: 2, content: "Imported entities" }]); resultTypes.forEach((type) => { resultTable.push([startCase(type), ctx.data[type].length]); }); console.log(resultTable.toString()); } else { console.log("No data was imported"); } const endTime = /* @__PURE__ */ new Date(); const durationHuman = formatDistance(endTime, options.startTime); const durationSeconds = differenceInSeconds(endTime, options.startTime); console.log(`The import took ${durationHuman} (${durationSeconds}s)`); return ctx.data; }).catch((err) => { log.push({ ts: (/* @__PURE__ */ new Date()).toJSON(), level: "error", error: err }); }).then((data) => { const errorLog = log.filter((logMessage) => logMessage.level !== "info" && logMessage.level !== "warning"); const displayLog = log.filter((logMessage) => logMessage.level !== "info"); displayErrorLog(displayLog); if (errorLog.length) { return writeErrorLogFile(options.errorLogFile, errorLog).then(() => { const multiError = new ContentfulMultiError("Errors occurred"); multiError.name = "ContentfulMultiError"; multiError.errors = errorLog; throw multiError; }); } console.log("The import was successful."); return data; }); } var index_default = runContentfulImport; module.exports = runContentfulImport; export { index_default as default };