Skip to content

Commit 06d7861

Browse files
Merge pull request #422 from shaurabh-tiwari-git/metadataAPI
@W-19701061 - Validate and Update OmniStudio metadata API
2 parents bd95b6d + 7610dab commit 06d7861

File tree

9 files changed

+281
-5
lines changed

9 files changed

+281
-5
lines changed

messages/assess.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,7 @@
198198
"invalidTypeAssessErrorMessage": "We couldn't assess your Omnistudio components in the %s namespace. Select the correct namespace and try again",
199199
"assessmentSuccessfulMessage": "Migration assessment for org %s is complete and reports are ready for review in the folder %s",
200200
"needManualInterventionAsSpecialCharsInChildFlexcardName": "Need manual intervention as child flexcards have special characters in their name",
201-
"needManualInterventionAsSpecialCharsInFlexcardName": "Need manual intervention as flexcard has special characters in name"
201+
"needManualInterventionAsSpecialCharsInFlexcardName": "Need manual intervention as flexcard has special characters in name",
202+
"errorCheckingOmniStudioMetadata": "We couldn't check whether the Omnistudio Metadata is enabled: %s. Try again later.",
203+
"omniStudioSettingsMetadataAlreadyEnabled": "The Omnistudio Metadata setting is already enabled with standard data model. No need for migration."
202204
}

messages/migrate.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,5 +254,13 @@
254254
"errorCheckingMetadataTables": "Error checking Omnistudio metadata tables: %s",
255255
"metadataCleanupCompleted": "The Omnistudio metadata table cleanup process is complete. Total records cleaned: %s",
256256
"metadataCleanupConsentMessage": "By proceeding further, you hereby consent to clean up the Omnistudio metadata tables. Proceeding with the cleanup process will permanently delete all records from OmniUiCardConfig, OmniScriptConfig, OmniIntegrationProcConfig, and OmniDataTransformConfig tables. Do you want to proceed? [y/n]",
257-
"metadataCleanupConsentNotGiven": "You’ve not consented to proceed with the Omnistudio metadata table cleanup. We’ll not be able to proceed with the migration."
257+
"metadataCleanupConsentNotGiven": "You’ve not consented to proceed with the Omnistudio metadata table cleanup. We’ll not be able to proceed with the migration.",
258+
"omniStudioMetadataEnableConsentMessage": "As part of the migration process, the Omnistudio Metadata setting will be enabled. After the setting is enabled, you cannot disable it. Do you want to proceed? [y/n]",
259+
"errorCheckingOmniStudioMetadata": "We couldn't check whether the Omnistudio Metadata is enabled: %s. Try again later.",
260+
"omniStudioSettingsMetadataAlreadyEnabled": "The Omnistudio Metadata setting is already enabled with standard data model. No need for migration.",
261+
"omniStudioSettingsMetadataEnabled": "The Omnistudio Metadata setting is enabled with standard data model.",
262+
"errorEnablingOmniStudioSettingsMetadata": "We couldn't enable the Omnistudio Metadata setting: %s. Enable it manually.",
263+
"manuallyEnableOmniStudioSettingsMetadata": "Manually enable the Omnistudio Metadata setting in your org’s Omnistudio Settings page.",
264+
"omniStudioMetadataEnableConsentNotGiven": "You’ve not consented to proceed with enabling the Omnistudio Metadata setting. We’ll not be able to proceed with the migration.",
265+
"enablingOmniStudioSettingsMetadataStatus": "Enabling OmniStudio Settings Metadata..."
258266
}

src/commands/omnistudio/migration/migrate.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,15 @@ export default class Migrate extends OmniStudioBaseCommand {
131131
const preMigrate: PreMigrate = new PreMigrate(namespace, conn, this.logger, messages, this.ux);
132132
const isExperienceBundleMetadataAPIProgramaticallyEnabled: { value: boolean } = { value: false };
133133

134-
// Handle config tables cleanup for standard data model migration
135134
if (isStandardDataModel()) {
135+
// Get user consent to enable OmniStudio Metadata for standard data model migration
136+
const omniStudioMetadataEnableConsent = await preMigrate.getOmniStudioMetadataEnableConsent();
137+
if (!omniStudioMetadataEnableConsent) {
138+
Logger.error(messages.getMessage('omniStudioMetadataEnableConsentNotGiven'));
139+
return;
140+
}
141+
142+
// Handle config tables cleanup for standard data model migration
136143
const isMetadataCleanupSuccess = await preMigrate.handleOmniStudioMetadataCleanup();
137144
if (!isMetadataCleanupSuccess) {
138145
return;

src/migration/postMigrate.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { BaseMigrationTool } from './base';
1212
import { Deployer } from './deployer';
1313
import * as fs from 'fs';
1414
import * as path from 'path';
15+
import { isStandardDataModel } from '../utils/dataModelService';
16+
import { OmniStudioMetadataCleanupService } from '../utils/config/OmniStudioMetadataCleanupService';
1517

1618
export class PostMigrate extends BaseMigrationTool {
1719
private readonly org: Org;
@@ -48,6 +50,9 @@ export class PostMigrate extends BaseMigrationTool {
4850
if (designerOk) {
4951
await this.enableStandardRuntimeIfNeeded(userActionMessage);
5052
}
53+
if (isStandardDataModel()) {
54+
await this.enableOmniStudioSettingsMetadataIfNeeded(userActionMessage);
55+
}
5156
return userActionMessage;
5257
}
5358

@@ -125,6 +130,48 @@ export class PostMigrate extends BaseMigrationTool {
125130
}
126131
}
127132

133+
private async enableOmniStudioSettingsMetadataIfNeeded(userActionMessage: string[]): Promise<void> {
134+
try {
135+
Logger.log(this.messages.getMessage('enablingOmniStudioSettingsMetadataStatus'));
136+
const result = await this.settingsPrefManager.enableOmniStudioSettingsMetadata();
137+
if (result === null) {
138+
Logger.logVerbose(this.messages.getMessage('omniStudioSettingsMetadataAlreadyEnabled'));
139+
} else if (result?.success === true) {
140+
/* The API call returns true if the metadata enabling call was successful.
141+
But it takes time for the checks to run and the metadata to be enabled or reverted back.
142+
We need to wait and check if the config tables are populated or not.
143+
That will ensure that the metadata is enabled.
144+
*/
145+
const maxAttempts = 6;
146+
let attempts = 0;
147+
while (attempts < maxAttempts) {
148+
await new Promise((resolve) => setTimeout(resolve, 20000));
149+
const metadataService = new OmniStudioMetadataCleanupService(this.connection, this.messages);
150+
const isConfigTablesEmpty = await metadataService.hasCleanOmniStudioMetadataTables(); //Check is the config tables are populated or not.
151+
if (!isConfigTablesEmpty) {
152+
// If the config tables are populated, means the metadata is enabled.
153+
Logger.log(this.messages.getMessage('omniStudioSettingsMetadataEnabled'));
154+
break;
155+
}
156+
attempts++;
157+
}
158+
if (attempts === maxAttempts) {
159+
// TODO: We need to figure out and show the user that what is the actual issue which is causing the metadata to not be enabled.
160+
Logger.error(this.messages.getMessage('errorEnablingOmniStudioSettingsMetadata', ['Unknown error']));
161+
userActionMessage.push(this.messages.getMessage('manuallyEnableOmniStudioSettingsMetadata'));
162+
}
163+
} else {
164+
const errors = result?.errors?.join(', ') || 'Unknown error';
165+
Logger.error(this.messages.getMessage('errorEnablingOmniStudioSettingsMetadata', [errors]));
166+
userActionMessage.push(this.messages.getMessage('manuallyEnableOmniStudioSettingsMetadata'));
167+
}
168+
} catch (error) {
169+
const errMsg = error instanceof Error ? error.message : String(error);
170+
Logger.error(this.messages.getMessage('errorEnablingOmniStudioSettingsMetadata', [errMsg]));
171+
userActionMessage.push(this.messages.getMessage('manuallyEnableOmniStudioSettingsMetadata'));
172+
}
173+
}
174+
128175
// If we processed exp sites and switched metadata api from off->on then only we revert it
129176
public async restoreExperienceAPIMetadataSettings(
130177
isExperienceBundleMetadataAPIProgramaticallyEnabled: {

src/migration/premigrate.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,41 @@ export class PreMigrate extends BaseMigrationTool {
110110
return deploymentConfig;
111111
}
112112

113+
/**
114+
* Gets user consent for OmniStudio metadata cleanup
115+
*
116+
* @returns Promise<boolean> - true if user consents, false otherwise
117+
*/
118+
public async getOmniStudioMetadataEnableConsent(): Promise<boolean> {
119+
const askWithTimeOut = PromptUtil.askWithTimeOut(this.messages);
120+
let validResponse = false;
121+
let consent = false;
122+
123+
while (!validResponse) {
124+
try {
125+
const resp = await askWithTimeOut(
126+
Logger.prompt.bind(Logger),
127+
this.messages.getMessage('omniStudioMetadataEnableConsentMessage')
128+
);
129+
const response = typeof resp === 'string' ? resp.trim().toLowerCase() : '';
130+
131+
if (response === YES_SHORT || response === YES_LONG) {
132+
consent = true;
133+
validResponse = true;
134+
} else if (response === NO_SHORT || response === NO_LONG) {
135+
consent = false;
136+
validResponse = true;
137+
} else {
138+
Logger.error(this.messages.getMessage('invalidYesNoResponse'));
139+
}
140+
} catch (err) {
141+
Logger.error(this.messages.getMessage('requestTimedOut'));
142+
process.exit(1);
143+
}
144+
}
145+
return consent;
146+
}
147+
113148
/**
114149
* Handles OmniStudio metadata tables cleanup with user consent
115150
*

src/utils/OmnistudioSettingsPrefManager.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,36 @@ export class OmnistudioSettingsPrefManager {
7878
}
7979
return null; // Already enabled, no action needed
8080
}
81+
82+
// OmniStudio Metadata methods
83+
public async isOmniStudioSettingsMetadataEnabled(): Promise<boolean> {
84+
try {
85+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
86+
const result = (await this.connection.metadata.read('OmniStudioSettings', ['OmniStudio'])) as unknown;
87+
88+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
89+
const metadata = result as MetadataInfo;
90+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
91+
return metadata?.enableOmniStudioMetadata === 'true' || false;
92+
} catch (error) {
93+
const errMsg = error instanceof Error ? error.message : String(error);
94+
Logger.error(this.messages.getMessage('errorCheckingOmniStudioMetadata', [errMsg]));
95+
return false;
96+
}
97+
}
98+
99+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100+
public async enableOmniStudioSettingsMetadata(): Promise<any> {
101+
const isMetadataEnabled = await this.isOmniStudioSettingsMetadataEnabled();
102+
if (isMetadataEnabled) {
103+
return null;
104+
}
105+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
106+
return await this.connection.metadata.update('OmniStudioSettings', [
107+
{
108+
fullName: 'OmniStudio',
109+
enableOmniStudioMetadata: 'true',
110+
} as MetadataInfo,
111+
]);
112+
}
81113
}

src/utils/validatorService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Connection, Messages } from '@salesforce/core';
22
import { Logger } from '../utils/logger';
33
import { OmnistudioOrgDetails } from './orgUtils';
44
import { isStandardDataModel } from './dataModelService';
5+
import { OmnistudioSettingsPrefManager } from './OmnistudioSettingsPrefManager';
56

67
export class ValidatorService {
78
private readonly messages: Messages;
@@ -20,9 +21,16 @@ export class ValidatorService {
2021
}
2122

2223
// If data model is standard no need to check for the licences
23-
// TODO: Add metadata toggle validation
2424
const isStandard = isStandardDataModel();
2525
if (isStandard) {
26+
// Check if OmniStudio Metadata is already enabled for standard data model
27+
const omniStudioSettingsPrefManager = new OmnistudioSettingsPrefManager(this.connection, this.messages);
28+
const isOmniStudioSettingsMetadataEnabled =
29+
await omniStudioSettingsPrefManager.isOmniStudioSettingsMetadataEnabled();
30+
if (isOmniStudioSettingsMetadataEnabled) {
31+
Logger.error(this.messages.getMessage('omniStudioSettingsMetadataAlreadyEnabled'));
32+
return false;
33+
}
2634
return true;
2735
}
2836

test/utils/OmnistudioSettingsPrefManager.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,107 @@ describe('OmnistudioSettingsPrefManager', () => {
250250
});
251251
});
252252
});
253+
254+
// OmniStudio Metadata Tests
255+
describe('OmniStudio Metadata functionality', () => {
256+
describe('isOmniStudioSettingsMetadataEnabled()', () => {
257+
it('should return true when omni studio metadata is enabled', async () => {
258+
const readStub = connection.metadata.read as sinon.SinonStub;
259+
readStub.resolves({ enableOmniStudioMetadata: 'true' });
260+
261+
const result = await prefManager.isOmniStudioSettingsMetadataEnabled();
262+
263+
expect(result).to.be.true;
264+
expect(readStub.calledOnce).to.be.true;
265+
expect(readStub.firstCall.args[0]).to.equal('OmniStudioSettings');
266+
expect(readStub.firstCall.args[1]).to.deep.equal(['OmniStudio']);
267+
});
268+
269+
it('should return false when omni studio metadata is disabled', async () => {
270+
const readStub = connection.metadata.read as sinon.SinonStub;
271+
readStub.resolves({ enableOmniStudioMetadata: 'false' });
272+
273+
const result = await prefManager.isOmniStudioSettingsMetadataEnabled();
274+
275+
expect(result).to.be.false;
276+
});
277+
278+
it('should return false when omni studio metadata preference is not set', async () => {
279+
const readStub = connection.metadata.read as sinon.SinonStub;
280+
readStub.resolves({});
281+
282+
const result = await prefManager.isOmniStudioSettingsMetadataEnabled();
283+
284+
expect(result).to.be.false;
285+
});
286+
287+
it('should handle metadata read failures', async () => {
288+
const readStub = connection.metadata.read as sinon.SinonStub;
289+
readStub.rejects(new Error('Metadata read failed'));
290+
291+
const result = await prefManager.isOmniStudioSettingsMetadataEnabled();
292+
293+
expect(result).to.be.false;
294+
});
295+
296+
it('should return false when metadata is null', async () => {
297+
const readStub = connection.metadata.read as sinon.SinonStub;
298+
readStub.resolves(null);
299+
300+
const result = await prefManager.isOmniStudioSettingsMetadataEnabled();
301+
302+
expect(result).to.be.false;
303+
});
304+
});
305+
306+
describe('enableOmniStudioSettingsMetadata()', () => {
307+
it('should return null when metadata is already enabled', async () => {
308+
const readStub = connection.metadata.read as sinon.SinonStub;
309+
const updateStub = connection.metadata.update as sinon.SinonStub;
310+
readStub.resolves({ enableOmniStudioMetadata: 'true' });
311+
312+
const result = await prefManager.enableOmniStudioSettingsMetadata();
313+
314+
expect(result).to.be.null;
315+
expect(readStub.calledOnce).to.be.true;
316+
expect(updateStub.called).to.be.false;
317+
});
318+
319+
it('should enable metadata when disabled', async () => {
320+
const readStub = connection.metadata.read as sinon.SinonStub;
321+
const updateStub = connection.metadata.update as sinon.SinonStub;
322+
const expectedResult = { success: true };
323+
readStub.resolves({ enableOmniStudioMetadata: 'false' });
324+
updateStub.resolves(expectedResult);
325+
326+
const result = await prefManager.enableOmniStudioSettingsMetadata();
327+
328+
expect(result).to.deep.equal(expectedResult);
329+
expect(readStub.calledOnce).to.be.true;
330+
expect(updateStub.calledOnce).to.be.true;
331+
expect(updateStub.firstCall.args[0]).to.equal('OmniStudioSettings');
332+
expect(updateStub.firstCall.args[1]).to.deep.equal([
333+
{
334+
fullName: 'OmniStudio',
335+
enableOmniStudioMetadata: 'true',
336+
},
337+
]);
338+
});
339+
340+
it('should handle update errors', async () => {
341+
const readStub = connection.metadata.read as sinon.SinonStub;
342+
const updateStub = connection.metadata.update as sinon.SinonStub;
343+
const error = new Error('Update failed');
344+
readStub.resolves({ enableOmniStudioMetadata: 'false' });
345+
updateStub.rejects(error);
346+
347+
try {
348+
await prefManager.enableOmniStudioSettingsMetadata();
349+
expect.fail('Expected an error to be thrown');
350+
} catch (err) {
351+
expect(err).to.equal(error);
352+
}
353+
});
354+
});
355+
});
253356
});

test/utils/validatorService.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ValidatorService } from '../../src/utils/validatorService';
55
import { Logger } from '../../src/utils/logger';
66
import { OmnistudioOrgDetails } from '../../src/utils/orgUtils';
77
import * as dataModelService from '../../src/utils/dataModelService';
8+
import { OmnistudioSettingsPrefManager } from '../../src/utils/OmnistudioSettingsPrefManager';
89

910
describe('ValidatorService', () => {
1011
let connection: Connection;
@@ -13,6 +14,7 @@ describe('ValidatorService', () => {
1314
let loggerWarnStub: sinon.SinonStub;
1415
let loggerErrorStub: sinon.SinonStub;
1516
let isStandardDataModelStub: sinon.SinonStub;
17+
let omnistudioSettingsPrefManagerStub: sinon.SinonStub;
1618

1719
beforeEach(() => {
1820
sandbox = sinon.createSandbox();
@@ -33,6 +35,12 @@ describe('ValidatorService', () => {
3335

3436
// Mock dataModelService
3537
isStandardDataModelStub = sandbox.stub(dataModelService, 'isStandardDataModel');
38+
39+
// Mock OmnistudioSettingsPrefManager
40+
omnistudioSettingsPrefManagerStub = sandbox.stub(
41+
OmnistudioSettingsPrefManager.prototype,
42+
'isOmniStudioSettingsMetadataEnabled'
43+
);
3644
});
3745

3846
afterEach(() => {
@@ -392,14 +400,15 @@ describe('ValidatorService', () => {
392400
expect(result).to.be.true;
393401
});
394402

395-
it('should return true for standard data model without checking licenses', async () => {
403+
it('should return true for standard data model when metadata is not enabled', async () => {
396404
// Arrange
397405
const orgs: OmnistudioOrgDetails = {
398406
hasValidNamespace: true,
399407
packageDetails: { namespace: 'TestNamespace' },
400408
omniStudioOrgPermissionEnabled: true,
401409
} as OmnistudioOrgDetails;
402410
isStandardDataModelStub.returns(true); // Standard data model
411+
omnistudioSettingsPrefManagerStub.resolves(false); // Metadata not enabled
403412
const validator = new ValidatorService(orgs, messages, connection);
404413

405414
// Act
@@ -408,6 +417,31 @@ describe('ValidatorService', () => {
408417
// Assert
409418
expect(result).to.be.true;
410419
expect((connection.query as sinon.SinonStub).called).to.be.false; // License validation should be skipped
420+
expect(omnistudioSettingsPrefManagerStub.calledOnce).to.be.true;
421+
});
422+
423+
it('should return false for standard data model when metadata is already enabled', async () => {
424+
// Arrange
425+
const orgs: OmnistudioOrgDetails = {
426+
hasValidNamespace: true,
427+
packageDetails: { namespace: 'TestNamespace' },
428+
omniStudioOrgPermissionEnabled: true,
429+
} as OmnistudioOrgDetails;
430+
isStandardDataModelStub.returns(true); // Standard data model
431+
omnistudioSettingsPrefManagerStub.resolves(true); // Metadata already enabled
432+
(messages.getMessage as sinon.SinonStub)
433+
.withArgs('omniStudioSettingsMetadataAlreadyEnabled')
434+
.returns('OmniStudio Metadata is already enabled');
435+
const validator = new ValidatorService(orgs, messages, connection);
436+
437+
// Act
438+
const result = await validator.validate();
439+
440+
// Assert
441+
expect(result).to.be.false;
442+
expect(loggerErrorStub.calledOnce).to.be.true;
443+
expect(loggerErrorStub.firstCall.args[0]).to.equal('OmniStudio Metadata is already enabled');
444+
expect(omnistudioSettingsPrefManagerStub.calledOnce).to.be.true;
411445
});
412446

413447
it('should return false when package validation fails', async () => {

0 commit comments

Comments
 (0)