Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Aliased existing records, DeepInsert service support for Business Process Flow #183

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Capgemini.PowerApps.SpecFlowBindings
{
using System;
using System.IO;
using System.Reflection;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Steps
{
using System;
using System.Configuration;
using Capgemini.PowerApps.SpecFlowBindings;
using Microsoft.Dynamics365.UIAutomation.Browser;
Expand Down Expand Up @@ -29,6 +30,7 @@ public static void GivenIHaveOpened(string alias)
/// </summary>
/// <param name="fileName">The name of the file containing the test record.</param>
[Given(@"I have created '(.*)'")]
[Given(@"'(.*)' exists")]
public static void GivenIHaveCreated(string fileName)
{
TestDriver.LoadTestData(TestDataRepository.GetTestData(fileName));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,19 @@ public void InjectOnPage(string authToken)
scriptBuilder.AppendLine(File.ReadAllText(this.FilePath));
scriptBuilder.AppendLine($@"top.recordRepository = new {LibraryNamespace}.CurrentUserRecordRepository(Xrm.WebApi.online);
top.metadataRepository = new {LibraryNamespace}.MetadataRepository(Xrm.WebApi.online);
top.deepInsertService = new {LibraryNamespace}.DeepInsertService(top.metadataRepository, top.recordRepository);");
top.deepInsertService = new {LibraryNamespace}.DeepInsertService(top.metadataRepository, top.recordRepository);
top.recordService = new {LibraryNamespace}.RecordService(top.metadataRepository, top.recordRepository);");

if (!string.IsNullOrEmpty(authToken))
{
scriptBuilder.AppendLine(
$@"top.appUserRecordRepository = new {LibraryNamespace}.AuthenticatedRecordRepository(top.metadataRepository, '{authToken}');
top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()], top.appUserRecordRepository);");
top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, top.recordService, [new {LibraryNamespace}.FakerPreprocessor()], top.appUserRecordRepository);");
}
else
{
scriptBuilder.AppendLine(
$"top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()]);");
$"top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, top.recordService, [new {LibraryNamespace}.FakerPreprocessor()]);");
}

scriptBuilder.AppendLine($"{TestDriverReference} = new {LibraryNamespace}.Driver(top.dataManager);");
Expand Down
14,862 changes: 1,160 additions & 13,702 deletions driver/package-lock.json

Large diffs are not rendered by default.

67 changes: 45 additions & 22 deletions driver/src/data/dataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DeepInsertService from './deepInsertService';
import Preprocessor from './preprocessor';
import Record from './record';
import { CurrentUserRecordRepository } from '../repositories';
import RecordService from './recordService';

/**
* Manages the creation and cleanup of data.
Expand All @@ -14,6 +15,8 @@ import { CurrentUserRecordRepository } from '../repositories';
export default class DataManager {
public readonly refs: Xrm.LookupValue[];

public readonly preservedRefs: Xrm.LookupValue[];

public readonly refsByAlias: { [alias: string]: Xrm.LookupValue };

private readonly currentUserRecordRepo: CurrentUserRecordRepository;
Expand All @@ -22,40 +25,45 @@ export default class DataManager {

private readonly deepInsertSvc: DeepInsertService;

private readonly recordService: RecordService;

private readonly preprocessors?: Preprocessor[];

/**
* Creates an instance of DataManager.
* @param {RecordRepository} currentUserRecordRepo A record repository.
* @param {DeepInsertService} deepInsertService A deep insert parser.
* @param {Preprocessor} preprocessors Preprocessors that modify test data before creation.
* @param {AuthenticatedRecordRepository} appUserRecordRepo An app user record repository
* @memberof DataManager
*/
* Creates an instance of DataManager.
* @param {RecordRepository} currentUserRecordRepo A record repository.
* @param {DeepInsertService} deepInsertService A deep insert parser.
* @param {Preprocessor} preprocessors Preprocessors that modify test data before creation.
* @param {AuthenticatedRecordRepository} appUserRecordRepo An app user record repository
* @memberof DataManager
*/
constructor(
currentUserRecordRepo: CurrentUserRecordRepository,
deepInsertService: DeepInsertService,
recordService: RecordService,
preprocessors?: Preprocessor[],
appUserRecordRepo?: AuthenticatedRecordRepository,
) {
this.currentUserRecordRepo = currentUserRecordRepo;
this.deepInsertSvc = deepInsertService;
this.appUserRecordRepo = appUserRecordRepo;
this.preprocessors = preprocessors;
this.recordService = recordService;

this.refs = [];
this.preservedRefs = [];
this.refsByAlias = {};
}

/**
* Deep inserts a record for use in a test.
*
* @param {string} logicalName the logical name of the root entity.
* @param {Record} record The record to deep insert.
* @param {CreateOptions} opts options for creating the data.
* @returns {Promise<Xrm.LookupValue>} An entity reference to the root record.
* @memberof DataManager
*/
* Deep inserts a record for use in a test.
*
* @param {string} logicalName the logical name of the root entity.
* @param {Record} record The record to deep insert.
* @param {CreateOptions} opts options for creating the data.
* @returns {Promise<Xrm.LookupValue>} An entity reference to the root record.
* @memberof DataManager
*/
public async createData(
logicalName: string,
record: Record,
Expand All @@ -69,7 +77,6 @@ export default class DataManager {
await this.getObjectIdForUser(opts.userToImpersonate),
);
}

const res = await this.deepInsertSvc.deepInsert(
logicalName,
this.preprocess(record),
Expand All @@ -89,6 +96,21 @@ export default class DataManager {
return res.record.reference;
}

public async getData(
logicalName: string,
id: string,
record: Record,
): Promise<Xrm.LookupValue> {
const result = await this.recordService.getExistingRecord(logicalName, id);
if (record?.['@alias']) {
this.refsByAlias[record?.['@alias'] as string] = result;
}

this.refs.push(result);
this.preservedRefs.push(result);
return result;
}

private async getObjectIdForUser(username: string): Promise<string> {
const res = await this.currentUserRecordRepo.retrieveMultipleRecords('systemuser', `?$filter=internalemailaddress eq '${username}'&$select=azureactivedirectoryobjectid`);

Expand All @@ -100,15 +122,16 @@ export default class DataManager {
}

/**
* Performs cleanup by deleting all records created via the TestDataManager.
* @param authToken An optional auth token to use when deleting test data.
* @returns {Promise<void>}
* @memberof DataManager
*/
* Performs cleanup by deleting all records created via the TestDataManager.
* @param authToken An optional auth token to use when deleting test data.
* @returns {Promise<void>}
* @memberof DataManager
*/
public async cleanup(): Promise<(Xrm.LookupValue | void)[]> {
const repo = this.appUserRecordRepo || this.currentUserRecordRepo;

const deletePromises = this.refs.map(async (record) => {
const cleanupRefs = this.refs.filter((x) => !this.preservedRefs.includes(x));
const deletePromises = cleanupRefs.map(async (record) => {
let reference;
let retry = 0;
while (retry < 3) {
Expand Down
32 changes: 28 additions & 4 deletions driver/src/data/deepInsertService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class DeepInsertService {
/**
* Creates an instance of DeepInsertService.
* @param {MetadataRepository} metadataRepository A metadata repository.
* @param {RecordRepository} recordRepository A record repostiroy.
* @param {RecordRepository} recordRepository A record repository.
* @memberof DeepInsertService
*/
constructor(metadataRepository: MetadataRepository, recordRepository: RecordRepository) {
Expand Down Expand Up @@ -71,6 +71,10 @@ export default class DeepInsertService {

const recordToCreateRef = await repo.upsertRecord(logicalName, recordToCreate);

if (Object.keys(record).includes('@bpf')) {
DeepInsertService.setBusinessProcessFlowStage(record, recordToCreateRef.id, repo);
}

await Promise.all(Object.keys(collRecordsByNavProp).map(async (collNavProp) => {
const result = await this.createCollectionRecords(
logicalName, recordToCreateRef, collRecordsByNavProp, collNavProp, dataByAlias, repo,
Expand Down Expand Up @@ -113,9 +117,10 @@ export default class DeepInsertService {
return Object.keys(record)
.filter(
(key) => typeof record[key] === 'object'
&& !Array.isArray(record[key])
&& record[key] !== null
&& !(record[key] instanceof Date),
&& !Array.isArray(record[key])
&& record[key] !== null
&& !(record[key] instanceof Date)
&& (key !== '@bpf'),
)
.reduce((prev, curr) => {
// eslint-disable-next-line no-param-reassign
Expand Down Expand Up @@ -262,4 +267,23 @@ export default class DeepInsertService {
): metadata is Xrm.Metadata.OneToNRelationshipMetadata {
return metadata.RelationshipType === 'OneToManyRelationship';
}

private static async setBusinessProcessFlowStage(
record: Record,
recordId: string,
repo: RecordRepository,
) {
const bpfValue = record?.['@bpf'] as any;
const bpfKeys = Object.keys(bpfValue);
if (bpfKeys.includes('@logicalName') && bpfKeys.includes('@activestageid')) {
const bpfRecords = await repo.retrieveMultipleRecords(bpfValue?.['@logicalName'], `?$filter=_bpf_${record?.['@logicalName']}id_value eq ${recordId}&$select=businessprocessflowinstanceid`);
if (bpfRecords.entities.length === 1) {
const bpfEntityId = bpfRecords.entities[0]?.businessprocessflowinstanceid;
const bpfRecord: Record = {
'activestageid@odata.bind': `/processstages(${bpfValue['@activestageid']})`,
};
repo.updateRecord(bpfValue?.['@logicalName'], bpfEntityId, bpfRecord);
}
}
}
}
1 change: 1 addition & 0 deletions driver/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as DataManager } from './dataManager';
export { default as DeepInsertService } from './deepInsertService';
export { default as RecordService } from './recordService';
export { DeepInsertResponse } from './deepInsertResponse';
export { default as Record } from './record';
export { TestRecord } from './testRecord';
Expand Down
19 changes: 19 additions & 0 deletions driver/src/data/record.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
export default interface Record {
[attribute: string]: number | string | unknown | unknown[];
}

export const recordInternalProperties = [
'@alias',
'@logicalName',
'@key',
'@bpf',
'@extends',
'@activestageid',
];

export function exludeInternalPropertiesFromPayload(record: Record) {
const updatedRecord = { ...record } as Record;
Object.keys(record).forEach((key) => {
if (recordInternalProperties.includes(key)) {
delete updatedRecord[key];
}
});
return updatedRecord;
}
48 changes: 48 additions & 0 deletions driver/src/data/recordService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { MetadataRepository, RecordRepository } from '../repositories';

/**
* Parses deep insert objects and returns references to all created records.
*
* @export
* @class RecordService
*/
export default class RecordService {
private readonly recordRepository: RecordRepository;

private readonly metadataRepository: MetadataRepository;

/**
* Creates an instance of RecordService.
* @param {MetadataRepository} metadataRepository A metadata repository.
* @param {RecordRepository} recordRepository A record repository.
* @memberof RecordService
*/
constructor(metadataRepository: MetadataRepository, recordRepository: RecordRepository) {
this.metadataRepository = metadataRepository;
this.recordRepository = recordRepository;
}

/**
* A deep insert which returns a reference to all created records.
*
* @param {string} logicalName The entity logical name of the root record.
* @param {Record} record The deep insert object.
* @param dataByAlias References to previously created records by alias.
* @param {RecordRepository} repository An optional repository to override the default.
* @returns {Promise<DeepInsertResponse>} An async result with references to created records.
* @memberof DeepInsertService
*/
public async getExistingRecord(
logicalName: string,
id: string,
metadataRepository?: MetadataRepository,
recordRepository?: RecordRepository,
): Promise<Xrm.LookupValue> {
const metadataRepo = metadataRepository ?? this.metadataRepository;
const recordRepo = recordRepository ?? this.recordRepository;
const primaryColumnName = await metadataRepo.getPrimaryColumnName(logicalName);
const result = await recordRepo.retrieveRecord(logicalName, id, `?$select=${primaryColumnName}`);
const reference: Xrm.LookupValue = { id, name: result?.[primaryColumnName] ?? '', entityType: logicalName };
return reference;
}
}
1 change: 1 addition & 0 deletions driver/src/data/testRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import Record from './record';
export interface TestRecord extends Record {
'@alias': string;
'@logicalName': string;
'@key'?: string;
}
Loading