Skip to content

Commit

Permalink
Merge pull request #466 from trivir/feature/realm-exports-and-imports
Browse files Browse the repository at this point in the history
Add Realm Exports and Imports
  • Loading branch information
vscheuber authored Nov 14, 2024
2 parents dde5061 + a2e459e commit 25b0af9
Show file tree
Hide file tree
Showing 4 changed files with 389 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/ops/RealmOps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ describe('RealmOps', () => {
}
});

describe('createRealmExportTemplate()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.createRealmExportTemplate).toBeDefined();
});

test('1: Create Realm Export Template', async () => {
const response = RealmOps.createRealmExportTemplate({ state });
expect(response).toMatchSnapshot({
meta: expect.any(Object),
});
});
});

describe('getRealms()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.getRealms).toBeDefined();
Expand All @@ -59,6 +72,19 @@ describe('RealmOps', () => {
});
});

describe('exportRealms()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.exportRealms).toBeDefined();
});

test('1: Export Realms', async () => {
const response = await RealmOps.exportRealms({ state });
expect(response).toMatchSnapshot({
meta: expect.any(Object),
});
});
});

describe('createRealm()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.createRealm).toBeDefined();
Expand All @@ -73,6 +99,13 @@ describe('RealmOps', () => {
//TODO: create tests
});

describe('importRealms()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.importRealms).toBeDefined();
});
//TODO: create tests
});

describe('getRealm()', () => {
test('0: Method is implemented', async () => {
expect(RealmOps.getRealm).toBeDefined();
Expand Down
159 changes: 159 additions & 0 deletions src/ops/RealmOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ import {
RealmSkeleton,
} from '../api/RealmApi';
import { State } from '../shared/State';
import {
createProgressIndicator,
debugMessage,
stopProgressIndicator,
updateProgressIndicator,
} from '../utils/Console';
import { getMetadata } from '../utils/ExportImportUtils';
import { getRealmName } from '../utils/ForgeRockUtils';
import { FrodoError } from './FrodoError';
import { ExportMetaData } from './OpsTypes';

export type Realm = {
/**
Expand All @@ -27,6 +36,11 @@ export type Realm = {
* @returns {Promise<RealmSkeleton>} a promise resolving to a realm object
*/
readRealmByName(realmName: string): Promise<RealmSkeleton>;
/**
* Export all realms. The response can be saved to file as is.
* @returns {Promise<RealmExportInterface>} Promise resolving to a RealmExportInterface object.
*/
exportRealms(): Promise<RealmExportInterface>;
/**
* Create realm
* @param {string} realmName realm name
Expand All @@ -47,6 +61,18 @@ export type Realm = {
realmId: string,
realmData: RealmSkeleton
): Promise<RealmSkeleton>;
/**
* Import realms
* @param {RealmExportInterface} importData realm import data
* @param {string} realmId Optional realm id. If supplied, only the realm of that id is imported. Takes priority over realmName if both are provided.
* @param {string} realmName Optional realm name. If supplied, only the realm of that name is imported.
* @returns {Promise<RealmSkeleton[]>} the imported realms
*/
importRealms(
importData: RealmExportInterface,
realmId?: string,
realmName?: string
): Promise<RealmSkeleton[]>;
/**
* Delete realm
* @param {string} realmId realm id
Expand Down Expand Up @@ -123,6 +149,9 @@ export default (state: State): Realm => {
readRealmByName(realmName: string): Promise<RealmSkeleton> {
return getRealmByName({ realmName, state });
},
exportRealms(): Promise<RealmExportInterface> {
return exportRealms({ state });
},
createRealm(
realmName: string,
realmData?: RealmSkeleton
Expand All @@ -135,6 +164,13 @@ export default (state: State): Realm => {
): Promise<RealmSkeleton> {
return updateRealm({ realmId, realmData, state });
},
importRealms(
importData: RealmExportInterface,
realmId?: string,
realmName?: string
): Promise<RealmSkeleton[]> {
return importRealms({ realmId, realmName, importData, state });
},
deleteRealm(realmId: string): Promise<RealmSkeleton> {
return deleteRealm({ realmId, state });
},
Expand Down Expand Up @@ -171,6 +207,26 @@ export default (state: State): Realm => {
};
};

export interface RealmExportInterface {
meta?: ExportMetaData;
realm: Record<string, RealmSkeleton>;
}

/**
* Create an empty realm export template
* @returns {RealmExportInterface} an empty realm export template
*/
export function createRealmExportTemplate({
state,
}: {
state: State;
}): RealmExportInterface {
return {
meta: getMetadata({ state }),
realm: {},
};
}

/**
* Get all realms
* @returns {Promise} a promise that resolves to an object containing an array of realm objects
Expand All @@ -180,6 +236,55 @@ export async function getRealms({ state }: { state: State }) {
return result;
}

/**
* Export all realms. The response can be saved to file as is.
* @returns {Promise<RealmExportInterface>} Promise resolving to a RealmExportInterface object.
*/
export async function exportRealms({
state,
}: {
state: State;
}): Promise<RealmExportInterface> {
let indicatorId: string;
try {
debugMessage({ message: `RealmOps.exportRealms: start`, state });
const exportData = createRealmExportTemplate({ state });
const realms = await getRealms({ state });
indicatorId = createProgressIndicator({
total: realms.length,
message: 'Exporting realms...',
state,
});
for (const realm of realms) {
updateProgressIndicator({
id: indicatorId,
message: `Exporting realm ${realm.name}`,
state,
});
// For root realm, it will export with null parent path, which on import causes an HTTP 500 error. With '' parent path, no 500 error is thrown on import while still meaning the same thing, so we use '' instead.
if (realm.parentPath === null) {
realm.parentPath = '';
}
exportData.realm[realm._id] = realm;
}
stopProgressIndicator({
id: indicatorId,
message: `Exported ${realms.length} realms.`,
state,
});
debugMessage({ message: `RealmOps.exportRealms: end`, state });
return exportData;
} catch (error) {
stopProgressIndicator({
id: indicatorId,
message: `Error exporting realms.`,
status: 'fail',
state,
});
throw new FrodoError(`Error reading realms`, error);
}
}

/**
* Create realm
* @param {string} realmName realm name
Expand Down Expand Up @@ -217,6 +322,60 @@ export async function updateRealm({
return _putRealm({ realmId, realmData, state });
}

/**
* Import realms
* @param {string} realmId Optional realm id. If supplied, only the realm of that id is imported. Takes priority over realmName if both are provided.
* @param {string} realmName Optional realm name. If supplied, only the realm of that name is imported.
* @param {RealmExportInterface} importData realm import data
* @returns {Promise<RealmSkeleton[]>} the imported realms
*/
export async function importRealms({
realmId,
realmName,
importData,
state,
}: {
realmId?: string;
realmName?: string;
importData: RealmExportInterface;
state: State;
}): Promise<RealmSkeleton[]> {
const errors = [];
try {
debugMessage({ message: `RealmOps.importRealms: start`, state });
const response = [];
for (const realm of Object.values(importData.realm)) {
try {
if (
(realmId && realm._id !== realmId) ||
(realmName && realm.name !== realmName)
) {
continue;
}
const result = await updateRealm({
realmId: realm._id,
realmData: realm,
state,
});
response.push(result);
} catch (error) {
errors.push(error);
}
}
if (errors.length > 0) {
throw new FrodoError(`Error importing realms`, errors);
}
debugMessage({ message: `RealmOps.importRealms: end`, state });
return response;
} catch (error) {
// re-throw previously caught errors
if (errors.length > 0) {
throw error;
}
throw new FrodoError(`Error importing realms`, error);
}
}

/**
* Get realm
* @param {String} realmId realm id
Expand Down
Loading

0 comments on commit 25b0af9

Please sign in to comment.