diff --git a/src/commands/export.ts b/src/commands/export.ts index 03857bf9..da2374df 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -16,7 +16,7 @@ import { languageVariantsExportEntity } from "./importExportEntities/entities/la import { previewUrlsExportEntity } from "./importExportEntities/entities/previewUrls.js"; import { rolesExportEntity } from "./importExportEntities/entities/roles.js"; import { spacesExportEntity } from "./importExportEntities/entities/spaces.js"; -import { taxonomiesExportEntity } from "./importExportEntities/entities/taxonomies.js"; +import { taxonomiesEntity } from "./importExportEntities/entities/taxonomies.js"; import { workflowsExportEntity } from "./importExportEntities/entities/workflows.js"; import { EntityDefinition } from "./importExportEntities/entityDefinition.js"; @@ -50,7 +50,7 @@ export const register: RegisterCommand = yargs => yargs.command({ const entityDefinitions: ReadonlyArray> = [ collectionsEntity, spacesExportEntity, - taxonomiesExportEntity, + taxonomiesEntity, languagesEntity, previewUrlsExportEntity, rolesExportEntity, diff --git a/src/commands/import.ts b/src/commands/import.ts index e4145cc6..ee0925e0 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -6,6 +6,7 @@ import { RegisterCommand } from "../types/yargs.js"; import { serially } from "../utils/requests.js"; import { collectionsEntity } from "./importExportEntities/entities/collections.js"; import { languagesEntity } from "./importExportEntities/entities/languages.js"; +import { taxonomiesEntity } from "./importExportEntities/entities/taxonomies.js"; import { EntityDefinition, ImportContext } from "./importExportEntities/entityDefinition.js"; export const register: RegisterCommand = yargs => yargs.command({ @@ -34,7 +35,8 @@ export const register: RegisterCommand = yargs => yargs.command({ // Keep in mind that there are dependencies between entities so the order is important. const entityDefinitions: ReadonlyArray> = [ collectionsEntity, - languagesEntity + languagesEntity, + taxonomiesEntity, ]; type ImportEntitiesParams = Readonly<{ @@ -55,7 +57,9 @@ const importEntities = async (params: ImportEntitiesParams) => { let context: ImportContext = { collectionIdsByOldIds: new Map(), - languageIdsByOldIds: new Map() + languageIdsByOldIds: new Map(), + taxonomyGroupIdsByOldIds: new Map(), + taxonomyTermIdsByOldIds: new Map(), }; await serially(entityDefinitions.map(def => async () => { @@ -69,12 +73,12 @@ const importEntities = async (params: ImportEntitiesParams) => { ?? context; console.log(`${def.name} imported`); - - console.log(`All entities were successfully imported into environment ${params.environmentId}.`); } catch (err) { console.error(`Failed to import entity ${def.name} due to error ${JSON.stringify(err)}. Stopping import...`); process.exit(1); } })); + + console.log(`All entities were successfully imported into environment ${params.environmentId}.`); }; diff --git a/src/commands/importExportEntities/entities/taxonomies.ts b/src/commands/importExportEntities/entities/taxonomies.ts index 42a25714..5b705f70 100644 --- a/src/commands/importExportEntities/entities/taxonomies.ts +++ b/src/commands/importExportEntities/entities/taxonomies.ts @@ -1,11 +1,39 @@ import { TaxonomyContracts } from "@kontent-ai/management-sdk"; +import { zip } from "../../../utils/array.js"; +import { serially } from "../../../utils/requests.js"; import { EntityDefinition } from "../entityDefinition.js"; -export const taxonomiesExportEntity: EntityDefinition> = { +export const taxonomiesEntity: EntityDefinition> = { name: "taxonomies", fetchEntities: client => client.listTaxonomies().toAllPromise().then(res => res.data.items.map(t => t._raw)), serializeEntities: taxonomies => JSON.stringify(taxonomies), - importEntities: () => { throw new Error("Not supported yet.")}, - deserializeEntities: () => { throw new Error("Not supported yet.")}, + importEntities: async (client, fileTaxonomies, context) => { + const projectTaxonomies = await serially Promise>>( + fileTaxonomies.map(taxonomy => () => + client + .addTaxonomy() + .withData(addExternalIds(taxonomy)) + .toPromise() + .then(res => res.data._raw)) + ); + + return { + ...context, + taxonomyGroupIdsByOldIds: new Map(zip(fileTaxonomies.map(t => t.id), projectTaxonomies.map(t => t.id))), + taxonomyTermIdsByOldIds: new Map(zip(fileTaxonomies.flatMap(t => t.terms), projectTaxonomies.flatMap(t => t.terms)).flatMap(extractTermIdsEntries)), + }; + }, + deserializeEntities: JSON.parse, }; + +const addExternalIds = (taxonomy: TaxonomyContracts.ITaxonomyContract): TaxonomyContracts.ITaxonomyContract => ({ + ...taxonomy, + external_id: taxonomy.external_id ?? taxonomy.codename, + terms: taxonomy.terms.map(addExternalIds), +}); + +const extractTermIdsEntries = ([fileTaxonomy, projectTaxonomy]: readonly [TaxonomyContracts.ITaxonomyContract, TaxonomyContracts.ITaxonomyContract]): ReadonlyArray => [ + [fileTaxonomy.id, projectTaxonomy.id] as const, + ...zip(fileTaxonomy.terms, projectTaxonomy.terms).flatMap(extractTermIdsEntries), +]; diff --git a/src/commands/importExportEntities/entityDefinition.ts b/src/commands/importExportEntities/entityDefinition.ts index d8946b75..5653f369 100644 --- a/src/commands/importExportEntities/entityDefinition.ts +++ b/src/commands/importExportEntities/entityDefinition.ts @@ -13,4 +13,8 @@ export type EntityDefinition = Readonly<{ export type ImportContext = Readonly<{ collectionIdsByOldIds: ReadonlyMap; languageIdsByOldIds: ReadonlyMap; + taxonomyGroupIdsByOldIds: IdsMap; + taxonomyTermIdsByOldIds: IdsMap; }>; + +type IdsMap = ReadonlyMap; diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 00000000..779d4b10 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,29 @@ +export const zip = , T2 extends ReadonlyArray>(arr1: T1, arr2: T2): Zip => + arr1 + .slice(0, Math.min(arr1.length, arr2.length)) + .map((el1, i) => [el1, arr2[i]] as const) as unknown as Zip; + +type Zip, T2 extends ReadonlyArray> = + true extends IsEmptyTuple | IsEmptyTuple + ? readonly [] + : [IsNonEmptyTuple, IsNonEmptyTuple] extends [true, true] + ? ZipTuples + : ZipArrays; + +type IsEmptyTuple> = T extends readonly [] ? true : false; + +type IsNonEmptyTuple> = T extends readonly [any, ...ReadonlyArray] ? true : false; + +/** + * Handles zip of two types where at least one is an array of unknown length. +*/ +type ZipArrays, T2 extends ReadonlyArray> = ReadonlyArray; + +/** + * Handles zip of two tuples (arrays of known length and exact types for different positions). + * This type expects two tuples and doesn't work well with arrays of unknown length. +*/ +type ZipTuples, T2 extends ReadonlyArray, Accum extends ReadonlyArray = readonly []> = + [T1, T2] extends [readonly [infer First1, ...infer Rest1 extends ReadonlyArray], readonly [infer First2, ...infer Rest2 extends ReadonlyArray]] + ? ZipTuples + : Accum;