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

Users/jan.losenicky/set bpf stage #184

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,25 @@ When using faker syntax, you must also annotate number or date fields using `@fa

You can also dynamically set lookups by alias using `<lookup>@alias.bind` (this is limited to aliased records in other files - not the current file).

#### Record in specific Business Process Flow stage
In case you need a record in specific BPF (Business Process Flow) stage, you can specify '@bpf' parameter in your data file which needs a logical name of target BPF and an id of stage. Example:

```json
{
"@extends": "../an opportunity",
"@logicalName": "talxis_opportunityheader",
"@alias": "the opportunity in won stage",
"@bpf": {
"@logicalName": "talxis_opportunitybusinessprocessflow",
"@activestageid": "b573544e-8c04-4b6d-ac3d-84d810d92ac1"
}
}
```
You can use WebApi to get the stageid you need where _processid_value is equal to id of your BPF, like this:

`await Xrm.WebApi.retrieveMultipleRecords("processstage", "?$filter=_processid_value eq BA1AF566-5105-4E24-8B75-0A7F01A24079");`


## Contributing

Please refer to the [Contributing](./CONTRIBUTING.md) guide.
Expand Down
14,862 changes: 1,160 additions & 13,702 deletions driver/package-lock.json

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions driver/src/data/dataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export default class DataManager {
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,
Expand All @@ -48,14 +48,14 @@ export default class 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
*/
* 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 Down Expand Up @@ -100,11 +100,11 @@ 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;

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 = {
'[email protected]': `/processstages(${bpfValue['@activestageid']})`,
};
repo.updateRecord(bpfValue?.['@logicalName'], bpfEntityId, bpfRecord);
}
}
}
}
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;
}
52 changes: 26 additions & 26 deletions driver/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ export default class Driver {
private readonly dataManager: DataManager;

/**
* Creates an instance of Driver.
* @param {TestDataManager} [dataManager] A test data manager.
* @memberof Driver
*/
* Creates an instance of Driver.
* @param {TestDataManager} [dataManager] A test data manager.
* @memberof Driver
*/
constructor(dataManager: DataManager) {
this.dataManager = dataManager;
}

/**
* Loads test data into CDS from a JSON string. See Microsoft's docs on a 'Deep Insert'.
* Should contain metadata to allow it to parse directly to an ITestRecord @see ITestRecord
*
* @param {string} json A JSON object.
* @memberof Driver
*/
* Loads test data into CDS from a JSON string. See Microsoft's docs on a 'Deep Insert'.
* Should contain metadata to allow it to parse directly to an ITestRecord @see ITestRecord
*
* @param {string} json A JSON object.
* @memberof Driver
*/
public async loadTestData(json: string): Promise<Xrm.LookupValue> {
const testRecord = JSON.parse(json) as TestRecord;
const logicalName = testRecord['@logicalName'];
Expand All @@ -34,10 +34,10 @@ export default class Driver {
}

/**
*
* @param json a JSON object.
* @param userToImpersonate The username of the user to impersonate.
*/
*
* @param json a JSON object.
* @param userToImpersonate The username of the user to impersonate.
*/
public async loadTestDataAsUser(
json: string,
userToImpersonate: string,
Expand All @@ -53,20 +53,20 @@ export default class Driver {
}

/**
* Deletes data that has been created as a result of any requests to load @see loadJsonData
* @memberof Driver
*/
* Deletes data that has been created as a result of any requests to load @see loadJsonData
* @memberof Driver
*/
public deleteTestData(): Promise<(Xrm.LookupValue | void)[]> {
return this.dataManager.cleanup();
}

/**
* Opens a test record.
*
* @param {string} alias The alias of the test record.
* @returns {Xrm.Async.PromiseLike<Xrm.Navigation.OpenFormResult} Open form result.
* @memberof Driver
*/
* Opens a test record.
*
* @param {string} alias The alias of the test record.
* @returns {Xrm.Async.PromiseLike<Xrm.Navigation.OpenFormResult} Open form result.
* @memberof Driver
*/
public openTestRecord(alias: string): Xrm.Async.PromiseLike<Xrm.Navigation.OpenFormResult> {
if (this.dataManager.refsByAlias[alias] === undefined) {
throw new Error(`Test record with alias '${alias}' does not exist`);
Expand All @@ -79,9 +79,9 @@ export default class Driver {
}

/**
* Gets a reference to a test record.
* @param alias The alias of the test record.
*/
* Gets a reference to a test record.
* @param alias The alias of the test record.
*/
public getRecordReference(alias: string): Xrm.LookupValue {
return this.dataManager.refsByAlias[alias];
}
Expand Down
4 changes: 3 additions & 1 deletion driver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { default as Driver } from './driver';
export { DataManager, DeepInsertService, FakerPreprocessor } from './data';
export {
DataManager, DeepInsertService, FakerPreprocessor,
} from './data';
export { CurrentUserRecordRepository, MetadataRepository, AuthenticatedRecordRepository } from './repositories';
12 changes: 8 additions & 4 deletions driver/src/repositories/authenticatedRecordRepository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import MetadataRepository from './metadataRepository';
import RecordRepository from './recordRepository';
import Record from '../data/record';
import Record, { exludeInternalPropertiesFromPayload } from '../data/record';

/**
* Repository to handle CRUD operations for entities.
Expand Down Expand Up @@ -82,7 +82,9 @@ export default class AuthenticatedRecordRepository implements RecordRepository {
const entitySet = await this.metadataRepo.getEntitySetForEntity(logicalName);
const res = await fetch(`api/data/v9.1/${entitySet}`, {
headers: this.headers,
body: JSON.stringify(record),
body: JSON.stringify(
exludeInternalPropertiesFromPayload(record),
),
method: 'POST',
});

Expand Down Expand Up @@ -168,12 +170,14 @@ export default class AuthenticatedRecordRepository implements RecordRepository {
}));
}

private async updateRecord(logicalName: string, id: string, record: any) {
public async updateRecord(logicalName: string, id: string, record: any) {
const entitySet = await this.metadataRepo.getEntitySetForEntity(logicalName);
const res = await fetch(`api/data/v9.1/${entitySet}(${id})`,
{
headers: this.headers,
body: JSON.stringify(record),
body: JSON.stringify(
exludeInternalPropertiesFromPayload(record),
),
method: 'PATCH',
});

Expand Down
29 changes: 24 additions & 5 deletions driver/src/repositories/currentUserRecordRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RecordRepository from './recordRepository';
import { Record } from '../data';
import Record, { exludeInternalPropertiesFromPayload } from '../data/record';
import { AssociateRequest } from '../requests';

/**
Expand Down Expand Up @@ -51,7 +51,23 @@ export default class CurrentUserRecordRepository implements RecordRepository {
* @memberof RecordRepository
*/
public async createRecord(logicalName: string, record: Record): Promise<Xrm.LookupValue> {
return this.webApi.createRecord(logicalName, record);
return this.webApi.createRecord(logicalName,
exludeInternalPropertiesFromPayload(record));
}

/**
* Updates an entity record.
*
* @param {string} logicalName A logical name for the entity to update.
* @param {string} recordId A recordId to update.
* @param {Record} record A record to update.
* @returns {Xrm.LookupValue} An entity reference to the created entity.
* @memberof RecordRepository
*/
public async updateRecord(logicalName: string, recordId: string,
record: Record): Promise<Xrm.LookupValue> {
return this.webApi.updateRecord(logicalName, recordId,
exludeInternalPropertiesFromPayload(record));
}

/**
Expand All @@ -63,7 +79,8 @@ export default class CurrentUserRecordRepository implements RecordRepository {
*/
public async upsertRecord(logicalName: string, record: Record): Promise<Xrm.LookupValue> {
if (!record['@key']) {
return this.webApi.createRecord(logicalName, record);
return this.webApi.createRecord(logicalName,
exludeInternalPropertiesFromPayload(record));
}

const retrieveResponse = await this.webApi.retrieveMultipleRecords(
Expand All @@ -73,12 +90,14 @@ export default class CurrentUserRecordRepository implements RecordRepository {

if (retrieveResponse.entities.length > 0) {
const id = retrieveResponse.entities[0][`${logicalName}id`];
await this.webApi.updateRecord(logicalName, id, record);
await this.webApi.updateRecord(logicalName, id,
exludeInternalPropertiesFromPayload(record));

return { entityType: logicalName, id };
}

return this.webApi.createRecord(logicalName, record);
return this.webApi.createRecord(logicalName,
exludeInternalPropertiesFromPayload(record));
}

/**
Expand Down
1 change: 1 addition & 0 deletions driver/src/repositories/recordRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default interface RecordRepository {
retrieveRecord(logicalName: string, id:string, query?: string): Promise<any>;
retrieveMultipleRecords(logicalName: string, query?: string): Promise<Xrm.RetrieveMultipleResult>;
createRecord(logicalName: string, record: Record): Promise<Xrm.LookupValue>;
updateRecord(logicalName: string, recordId: string, record: Record): Promise<Xrm.LookupValue>;
upsertRecord(logicalName: string, record: Record): Promise<Xrm.LookupValue>;
deleteRecord(ref: Xrm.LookupValue): Promise<Xrm.LookupValue>;
associateManyToManyRecords(
Expand Down
5 changes: 4 additions & 1 deletion driver/test/data/dataManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ describe('TestDriver', () => {
'AuthenticatedRecordRepository', ['createRecord', 'deleteRecord', 'setImpersonatedUserId'],
);
deepInsertService = jasmine.createSpyObj<DeepInsertService>('DeepInsertService', ['deepInsert']);
dataManager = new DataManager(currentUserRecordRepo, deepInsertService, [], appUserRecordRepo);
dataManager = new DataManager(currentUserRecordRepo,
deepInsertService,
[],
appUserRecordRepo);
});

describe('.createData(record)', () => {
Expand Down
Loading