diff --git a/src/cli/config/config-export.ts b/src/cli/config/config-export.ts index a899c75c6..30516f647 100644 --- a/src/cli/config/config-export.ts +++ b/src/cli/config/config-export.ts @@ -67,6 +67,12 @@ export default function setup() { 'Export sync.idm.json mappings separately in their own directory. Ignored with -a.' ) ) + .addOption( + new Option( + '-o, --separate-objects', + 'Export managed.idm.json objects separately in their own directory. Ignored with -a.' + ) + ) .addOption( new Option( '--include-active-values', @@ -145,6 +151,7 @@ export default function setup() { const outcome = await exportEverythingToFiles( options.extract, options.separateMappings, + options.separateObjects, options.metadata, { useStringArrays: options.useStringArrays, diff --git a/src/cli/idm/idm-export.ts b/src/cli/idm/idm-export.ts index 8dd61dbf9..1ed3ced81 100644 --- a/src/cli/idm/idm-export.ts +++ b/src/cli/idm/idm-export.ts @@ -55,6 +55,12 @@ export default function setup() { 'Export sync.idm.json mappings separately in their own directory. Ignored with -a.' ) ) + .addOption( + new Option( + '-o, --separate-objects', + 'Export managed.idm.json objects separately in their own directory. Ignored with -a.' + ) + ) .addOption( new Option( '-N, --no-metadata', @@ -95,6 +101,7 @@ export default function setup() { options.file, options.envFile, options.separateMappings, + options.separateObjects, options.metadata ); if (!outcome) process.exitCode = 1; @@ -136,6 +143,7 @@ export default function setup() { options.entitiesFile, options.envFile, options.separateMappings, + options.separateObjects, options.metadata ); if (!outcome) process.exitCode = 1; diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index c67372af5..36cd70918 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -6,7 +6,10 @@ import { FullImportOptions, FullRealmExportInterface, } from '@rockcarver/frodo-lib/types/ops/ConfigOps'; -import { SyncSkeleton } from '@rockcarver/frodo-lib/types/ops/MappingOps'; +import { + ManagedSkeleton, + SyncSkeleton, +} from '@rockcarver/frodo-lib/types/ops/MappingOps'; import { ScriptExportInterface } from '@rockcarver/frodo-lib/types/ops/ScriptOps'; import fs from 'fs'; @@ -17,7 +20,10 @@ import { } from '../utils/Config'; import { cleanupProgressIndicators, printError } from '../utils/Console'; import { saveServersToFiles } from './classic/ServerOps'; -import { writeSyncJsonToDirectory } from './MappingOps'; +import { + writeManagedJsonToDirectory, + writeSyncJsonToDirectory, +} from './MappingOps'; import { extractScriptsToFiles } from './ScriptOps'; const { @@ -72,6 +78,7 @@ export async function exportEverythingToFile( * Export everything to separate files * @param {boolean} extract Extracts the scripts from the exports into separate files if true * @param {boolean} separateMappings separate sync.idm.json mappings if true, otherwise keep them in a single file + * @param {boolean} separateObjects separate managed.idm.json objects if true, otherwise keep them in a single file * @param {boolean} includeMeta true to include metadata, false otherwise. Default: true * @param {FullExportOptions} options export options * @return {Promise} a promise that resolves to true if successful, false otherwise @@ -79,6 +86,7 @@ export async function exportEverythingToFile( export async function exportEverythingToFiles( extract: boolean = false, separateMappings: boolean = false, + separateObjects: boolean = false, includeMeta: boolean = true, options: FullExportOptions = { useStringArrays: true, @@ -106,7 +114,8 @@ export async function exportEverythingToFiles( `${baseDirectory}/global`, includeMeta, extract, - separateMappings + separateMappings, + separateObjects ) ); Object.entries(exportData.realm).forEach(([realm, data]: [string, any]) => @@ -118,7 +127,8 @@ export async function exportEverythingToFiles( `${baseDirectory}/realm/${realm}`, includeMeta, extract, - separateMappings + separateMappings, + separateObjects ) ) ); @@ -141,6 +151,7 @@ export async function exportEverythingToFiles( * @param {boolean} includeMeta true to include metadata, false otherwise. Default: true * @param {boolean} extract Extracts the scripts from the exports into separate files if true * @param {boolean} separateMappings separate sync.idm.json mappings if true, otherwise keep them in a single file + * @param {boolean} separateObjects separate managed.idm.json objects if true, otherwise keep them in a single file */ function exportItem( exportData, @@ -149,7 +160,8 @@ function exportItem( baseDirectory, includeMeta, extract, - separateMappings = false + separateMappings = false, + separateObjects = false ) { if (!obj || !Object.keys(obj).length) { return; @@ -253,6 +265,12 @@ function exportItem( `${baseDirectory.substring(getWorkingDirectory(false).length + 1)}/${fileType}/sync`, includeMeta ); + } else if (separateObjects && id === 'managed') { + writeManagedJsonToDirectory( + value as ManagedSkeleton, + `${baseDirectory.substring(getWorkingDirectory(false).length + 1)}/${fileType}/managed`, + includeMeta + ); } else { const filename = `${id}.idm.json`; if (filename.includes('/')) { diff --git a/src/ops/IdmOps.ts b/src/ops/IdmOps.ts index fd7d40596..1272f42ab 100644 --- a/src/ops/IdmOps.ts +++ b/src/ops/IdmOps.ts @@ -1,7 +1,10 @@ import { frodo, FrodoError } from '@rockcarver/frodo-lib'; import { type IdObjectSkeletonInterface } from '@rockcarver/frodo-lib/types/api/ApiTypes'; import { type ConfigEntityExportInterface } from '@rockcarver/frodo-lib/types/ops/IdmConfigOps'; -import { SyncSkeleton } from '@rockcarver/frodo-lib/types/ops/MappingOps'; +import { + ManagedSkeleton, + SyncSkeleton, +} from '@rockcarver/frodo-lib/types/ops/MappingOps'; import fs from 'fs'; import path from 'path'; import propertiesReader from 'properties-reader'; @@ -14,6 +17,8 @@ import { } from '../utils/Console'; import { getLegacyMappingsFromFiles, + getManagedObjectsFromFiles, + writeManagedJsonToDirectory, writeSyncJsonToDirectory, } from './MappingOps'; @@ -83,6 +88,7 @@ export async function listAllConfigEntities(): Promise { * @param {string} file optional export file name (or directory name if exporting mappings separately) * @param {string} envFile File that defines environment specific variables for replacement during configuration export/import * @param {boolean} separateMappings separate sync.idm.json mappings if true (and id is "sync"), otherwise keep them in a single file + * @param {boolean} separateObjects separate managed.idm.json objects if true (and id is "managed"), otherwise keep them in a single file * @param {boolean} includeMeta true to include metadata, false otherwise. Default: true * @return {Promise} a promise that resolves to true if successful, false otherwise */ @@ -91,6 +97,7 @@ export async function exportConfigEntityToFile( file?: string, envFile?: string, separateMappings: boolean = false, + separateObjects: boolean = false, includeMeta: boolean = true ): Promise { try { @@ -107,6 +114,14 @@ export async function exportConfigEntityToFile( ); return true; } + if (separateObjects && id === 'managed') { + writeManagedJsonToDirectory( + exportData.idm[id] as ManagedSkeleton, + file, + includeMeta + ); + return true; + } let fileName = file; if (!fileName) { fileName = getTypedFilename(`${id}`, 'idm'); @@ -156,12 +171,14 @@ export async function exportAllConfigEntitiesToFile( * @param {string} entitiesFile JSON file that specifies the config entities to export/import * @param {string} envFile File that defines environment specific variables for replacement during configuration export/import * @param {boolean} separateMappings separate sync.idm.json mappings if true, otherwise keep them in a single file + * @param {boolean} separateObjects separate managed.idm.json objects if true, otherwise keep them in a single file * @return {Promise} a promise that resolves to true if successful, false otherwise */ export async function exportAllConfigEntitiesToFiles( entitiesFile?: string, envFile?: string, separateMappings: boolean = false, + separateObjects: boolean = false, includeMeta: boolean = true ): Promise { const errors: Error[] = []; @@ -177,6 +194,14 @@ export async function exportAllConfigEntitiesToFiles( writeSyncJsonToDirectory(obj as SyncSkeleton, 'sync', includeMeta); continue; } + if (separateObjects && id === 'managed') { + writeManagedJsonToDirectory( + obj as ManagedSkeleton, + 'managed', + includeMeta + ); + continue; + } saveToFile( 'idm', obj, @@ -232,6 +257,14 @@ export async function importConfigEntityByIdFromFile( }, ]); importData = { idm: { sync: syncData } }; + } else if (entityId === 'managed') { + const managedData = getManagedObjectsFromFiles([ + { + content: fileData, + path: `${filePath.substring(0, filePath.lastIndexOf('/'))}/managed.idm.json`, + }, + ]); + importData = { idm: { managed: managedData } }; } else { importData = JSON.parse(fileData); } @@ -292,6 +325,14 @@ export async function importFirstConfigEntityFromFile( }, ]); } + if (entityId === 'managed') { + importData.idm.managed = getManagedObjectsFromFiles([ + { + content: fileData, + path: `${filePath.substring(0, filePath.lastIndexOf('/'))}/managed.idm.json`, + }, + ]); + } const options = getIdmImportExportOptions(undefined, envFile); @@ -434,9 +475,13 @@ export async function getIdmImportDataFromIdmDirectory( ); // Process sync mapping file(s) importData.idm.sync = getLegacyMappingsFromFiles(idmConfigFiles); + importData.idm.managed = getManagedObjectsFromFiles(idmConfigFiles); // Process other files for (const f of idmConfigFiles.filter( - (f) => !f.path.endsWith('sync.idm.json') && f.path.endsWith('.idm.json') + (f) => + !f.path.endsWith('sync.idm.json') && + !f.path.endsWith('managed.idm.json') && + f.path.endsWith('.idm.json') )) { const entities = Object.values( JSON.parse(f.content).idm diff --git a/src/ops/MappingOps.ts b/src/ops/MappingOps.ts index 1d56aca38..8938dfe22 100644 --- a/src/ops/MappingOps.ts +++ b/src/ops/MappingOps.ts @@ -1,5 +1,6 @@ import { frodo, FrodoError } from '@rockcarver/frodo-lib'; import { + ManagedSkeleton, MappingExportInterface, MappingExportOptions, MappingImportOptions, @@ -479,6 +480,31 @@ export function writeSyncJsonToDirectory( ); } +/** + * Helper that writes mappings in a managed.idm.json config entity to a directory + * @param managed The managed.idm.json config entity + * @param directory The directory to save the mappings + */ +export function writeManagedJsonToDirectory( + managed: ManagedSkeleton, + directory: string = 'managed', + includeMeta: boolean = true +) { + const objectPaths = []; + for (const object of managed.objects) { + const fileName = getTypedFilename(object.name, 'managed'); + objectPaths.push(extractDataToFile(object, fileName, directory)); + } + managed.objects = objectPaths; + saveToFile( + 'idm', + managed, + '_id', + getFilePath(`${directory}/managed.idm.json`, true), + includeMeta + ); +} + /** * Helper that returns the sync.idm.json object containing all the mappings in it by looking through the files * @@ -516,6 +542,49 @@ export function getLegacyMappingsFromFiles( return sync; } +/** + * Helper that returns the managed.idm.json object containing all the mappings in it by looking through the files + * + * @param files the files to get managed.idm.json object from + * @returns the managed.idm.json object + */ +export function getManagedObjectsFromFiles( + files: { path: string; content: string }[] +): ManagedSkeleton { + const managedFiles = files.filter((f) => + f.path.endsWith('/managed.idm.json') + ); + if (managedFiles.length > 1) { + throw new FrodoError( + 'Multiple managed.idm.json files found in idm directory' + ); + } + const managed = { + _id: 'managed', + objects: [], + }; + if (managedFiles.length === 1) { + const jsonData = JSON.parse(managedFiles[0].content); + const managedData = jsonData.managed + ? jsonData.managed + : jsonData.idm.managed; + const managedJsonDir = managedFiles[0].path.substring( + 0, + managedFiles[0].path.indexOf('/managed.idm.json') + ); + if (managedData.objects) { + for (const object of managedData.objects) { + if (typeof object === 'string') { + managed.objects.push(getExtractedJsonData(object, managedJsonDir)); + } else { + managed.objects.push(object); + } + } + } + } + return managed; +} + /** * Helper that gets a mapping's type (either 'sync' or 'mapping') from it's id * @param {string} mappingId the mapping id diff --git a/src/utils/Config.ts b/src/utils/Config.ts index d84e8a54c..20a12417d 100644 --- a/src/utils/Config.ts +++ b/src/utils/Config.ts @@ -9,7 +9,10 @@ import fs from 'fs'; import os from 'os'; import { readServersFromFiles } from '../ops/classic/ServerOps'; -import { getLegacyMappingsFromFiles } from '../ops/MappingOps'; +import { + getLegacyMappingsFromFiles, + getManagedObjectsFromFiles, +} from '../ops/MappingOps'; import { getScriptExportByScriptFile } from '../ops/ScriptOps'; import { printMessage } from './Console'; @@ -159,7 +162,9 @@ export async function getConfig( !f.path.endsWith('.script.json') && !f.path.endsWith('.server.json') && !f.path.endsWith('/sync.idm.json') && - !f.path.endsWith('sync.json') + !f.path.endsWith('sync.json') && + !f.path.endsWith('/managed.idm.json') && + !f.path.endsWith('managed.json') ); // Handle all other json files for (const f of allOtherFiles) { @@ -183,6 +188,10 @@ export async function getConfig( if (sync.mappings.length > 0) { (exportConfig as FullGlobalExportInterface).sync = sync; } + const managed = await getManagedObjectsFromFiles(jsonFiles); + if (managed.objects.length > 0) { + (exportConfig as FullGlobalExportInterface).idm.managed = managed; + } // Handle saml files if ( samlFiles.length > 0 &&