-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Conflicts: # bin/main.js # bin/root/tools/deploy.js # bin/root/tools/resx.js # bin/root/tools/setFormCustomizable.js # package.json
- Loading branch information
Showing
26 changed files
with
272 additions
and
375 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import {Model} from '../WebApi/Model'; | ||
|
||
// Please add Entity fields here, which are (to be) used in code | ||
// Please add NavigationProperties here, referring to another model, which are (to be) used in code | ||
export interface EntityModel extends Model { | ||
interface EntityModel extends Model { | ||
EntityLogicalNameid?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
type Method = 'GET' | 'POST' | 'DELETE'; | ||
|
||
interface HttpHeaders { | ||
[index: string]: string; | ||
} | ||
|
||
interface JsonHttpHeaders extends HttpHeaders { | ||
'OData-MaxVersion': string; | ||
'OData-Version': string; | ||
'Accept': string; | ||
'Content-Type': string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
import {WebApi} from './WebApi'; | ||
|
||
export interface Model { | ||
interface Model { | ||
//Attributes for $select | ||
statecode?: number; | ||
owneridname?: string; | ||
|
@@ -30,127 +29,8 @@ export interface Model { | |
|
||
type ValidationCategory = 'Mandatory' | 'Invalid key'; | ||
|
||
export interface ModelValidation { | ||
interface ModelValidation { | ||
isValid: boolean; | ||
attribute?: string; | ||
category?: ValidationCategory; | ||
} | ||
|
||
export class Model { | ||
public static async parseCreateModel(entityLogicalName: string, model: Model): Promise<Model> { | ||
const entity = Model.parseModel(model), | ||
metadata = await Xrm.Utility.getEntityMetadata(entityLogicalName, Object.keys(entity)); | ||
Model.cleanCloneModel(entity, metadata); | ||
return WebApi.populateBindings(entity, metadata); | ||
} | ||
|
||
private static cleanCloneModel(model: Model, metadata: Xrm.Metadata.EntityMetadata): void { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
delete (model as any)[metadata.PrimaryIdAttribute]; | ||
delete model.statuscode; | ||
delete model.statecode; | ||
|
||
delete model.ownerid; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
delete (model as any)['[email protected]']; | ||
// delete model.modifiedon; | ||
// delete model.createdon; | ||
// delete model.versionnumber; | ||
} | ||
|
||
private static parseModel(model: Model): Model { | ||
const keys = Object.keys(model), | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
cleanModel: any = {...model}; | ||
for (const key of keys) { | ||
if (cleanModel[key] === null) { | ||
delete cleanModel[key]; | ||
} else if (key.startsWith('@')) { | ||
delete cleanModel[key]; | ||
} else if (key.endsWith('FormattedValue')) { | ||
delete cleanModel[key]; | ||
} else if (key.startsWith('_') && key.endsWith('_value') && !key.includes('@')) { | ||
const navigationProperty = cleanModel[`${key}@Microsoft.Dynamics.CRM.associatednavigationproperty`], | ||
trimmedKey = key.substr(1, key.length - 7); | ||
if (navigationProperty) { | ||
cleanModel[trimmedKey] = cleanModel[key]; | ||
delete cleanModel[`${key}@Microsoft.Dynamics.CRM.associatednavigationproperty`]; | ||
} else { | ||
delete cleanModel[`${key}@Microsoft.Dynamics.CRM.lookuplogicalname`]; | ||
} | ||
delete cleanModel[key]; | ||
delete cleanModel[`${trimmedKey}_LogicalName`]; // Added in WebApi#parseValues | ||
} | ||
} | ||
return cleanModel; | ||
} | ||
|
||
public static async validateRecord(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
let validation = await Model.validateRequiredAttributes(entityLogicalName, model); | ||
if (validation.isValid) { | ||
validation = await Model.validateMesh(entityLogicalName, model); | ||
} | ||
return validation; | ||
} | ||
|
||
private static async validateMesh(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
const keys = Object.keys(model), | ||
wrongKey = keys.find(key => key.startsWith('_') || key.endsWith('FormattedValue') || key.endsWith('LogicalName')); | ||
if (wrongKey) { | ||
return { | ||
isValid: false, | ||
attribute: wrongKey, | ||
category: 'Invalid key' | ||
}; | ||
} | ||
return {isValid: true}; | ||
} | ||
|
||
private static async validateRequiredAttributes(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
const requiredAttributeLogicalNames = await Model.getRequiredAttributeLogicalNames(entityLogicalName), | ||
filteredAttributeMetadatas = await Xrm.Utility.getEntityMetadata(entityLogicalName, requiredAttributeLogicalNames), | ||
keys = Object.keys(model); | ||
for (const name of requiredAttributeLogicalNames) { | ||
const attributeDescriptor = Model.getAttributeDescriptor(name, filteredAttributeMetadatas), | ||
property = attributeDescriptor.LogicalName, | ||
bindings: string[] = []; | ||
const targets: string[] = attributeDescriptor.Targets || []; | ||
for (const target of targets) { | ||
const binding = await WebApi.getBinding(property, 'dummy', filteredAttributeMetadatas, target); | ||
bindings.push(Object.keys(binding)[0]); | ||
} | ||
if (!keys.includes(property) && !keys.includes(`${property}@odata.bind`) && !bindings.some(binding => keys.includes(binding))) { | ||
return { | ||
isValid: false, | ||
attribute: name, | ||
category: 'Mandatory' | ||
}; | ||
} | ||
} | ||
return {isValid: true}; | ||
} | ||
|
||
private static getAttributeDescriptor(attribute: string, entityMetadata: Xrm.Metadata.EntityMetadata): { | ||
AttributeOf?: string; | ||
LogicalName: string; | ||
Targets: string[]; | ||
} { | ||
const attributeMetadata = entityMetadata.Attributes.get(attribute); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | ||
// @ts-ignore | ||
let attributeDescriptor = attributeMetadata.attributeDescriptor; | ||
if (attributeDescriptor.AttributeOf) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | ||
// @ts-ignore | ||
attributeDescriptor = entityMetadata.Attributes.get(attributeDescriptor.AttributeOf).attributeDescriptor; | ||
} | ||
return attributeDescriptor; | ||
} | ||
|
||
private static async getRequiredAttributeLogicalNames(entityLogicalName: string): Promise<string[]> { | ||
const rawAttributesMetadatas = await WebApi.getAttributesMetadata(entityLogicalName, ['LogicalName', 'RequiredLevel']); | ||
return rawAttributesMetadatas | ||
.filter((attrMetadata: {RequiredLevel: {Value: string}}) => attrMetadata.RequiredLevel.Value === 'ApplicationRequired') | ||
.map((requiredAttributeMetadata: {LogicalName: string}) => requiredAttributeMetadata.LogicalName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import {WebApi} from './WebApi'; | ||
|
||
export class Service { | ||
public static async parseCreateModel(entityLogicalName: string, model: Model): Promise<Model> { | ||
const entity = Service.parseModel(model), | ||
metadata = await Xrm.Utility.getEntityMetadata(entityLogicalName, Object.keys(entity)); | ||
Service.cleanCloneModel(entity, metadata); | ||
return WebApi.populateBindings(entity, metadata); | ||
} | ||
|
||
private static cleanCloneModel(model: Model, metadata: Xrm.Metadata.EntityMetadata): void { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
delete (model as any)[metadata.PrimaryIdAttribute]; | ||
delete model.statuscode; | ||
delete model.statecode; | ||
|
||
delete model.ownerid; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
delete (model as any)['[email protected]']; | ||
// delete model.modifiedon; | ||
// delete model.createdon; | ||
// delete model.versionnumber; | ||
} | ||
|
||
private static parseModel(model: Model): Model { | ||
const keys = Object.keys(model), | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
cleanModel: any = {...model}; | ||
for (const key of keys) { | ||
if (cleanModel[key] === null) { | ||
delete cleanModel[key]; | ||
} else if (key.startsWith('@')) { | ||
delete cleanModel[key]; | ||
} else if (key.endsWith('FormattedValue')) { | ||
delete cleanModel[key]; | ||
} else if (key.startsWith('_') && key.endsWith('_value') && !key.includes('@')) { | ||
const navigationProperty = cleanModel[`${key}@Microsoft.Dynamics.CRM.associatednavigationproperty`], | ||
trimmedKey = key.substr(1, key.length - 7); | ||
if (navigationProperty) { | ||
cleanModel[trimmedKey] = cleanModel[key]; | ||
delete cleanModel[`${key}@Microsoft.Dynamics.CRM.associatednavigationproperty`]; | ||
} else { | ||
delete cleanModel[`${key}@Microsoft.Dynamics.CRM.lookuplogicalname`]; | ||
} | ||
delete cleanModel[key]; | ||
delete cleanModel[`${trimmedKey}_LogicalName`]; // Added in WebApi#parseValues | ||
} | ||
} | ||
return cleanModel; | ||
} | ||
|
||
public static async validateRecord(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
let validation = await Service.validateRequiredAttributes(entityLogicalName, model); | ||
if (validation.isValid) { | ||
validation = await Service.validateMesh(entityLogicalName, model); | ||
} | ||
return validation; | ||
} | ||
|
||
private static async validateMesh(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
const keys = Object.keys(model), | ||
wrongKey = keys.find(key => key.startsWith('_') || key.endsWith('FormattedValue') || key.endsWith('LogicalName')); | ||
if (wrongKey) { | ||
return { | ||
isValid: false, | ||
attribute: wrongKey, | ||
category: 'Invalid key' | ||
}; | ||
} | ||
return {isValid: true}; | ||
} | ||
|
||
private static async validateRequiredAttributes(entityLogicalName: string, model: Model): Promise<ModelValidation> { | ||
const requiredAttributeLogicalNames = await Service.getRequiredAttributeLogicalNames(entityLogicalName), | ||
filteredAttributeMetadatas = await Xrm.Utility.getEntityMetadata(entityLogicalName, requiredAttributeLogicalNames), | ||
keys = Object.keys(model); | ||
for (const name of requiredAttributeLogicalNames) { | ||
const attributeDescriptor = Service.getAttributeDescriptor(name, filteredAttributeMetadatas), | ||
property = attributeDescriptor.LogicalName, | ||
bindings: string[] = []; | ||
const targets: string[] = attributeDescriptor.Targets || []; | ||
for (const target of targets) { | ||
const binding = await WebApi.getBinding(property, 'dummy', filteredAttributeMetadatas, target); | ||
bindings.push(Object.keys(binding)[0]); | ||
} | ||
if (!keys.includes(property) && !keys.includes(`${property}@odata.bind`) && !bindings.some(binding => keys.includes(binding))) { | ||
return { | ||
isValid: false, | ||
attribute: name, | ||
category: 'Mandatory' | ||
}; | ||
} | ||
} | ||
return {isValid: true}; | ||
} | ||
|
||
private static getAttributeDescriptor(attribute: string, entityMetadata: Xrm.Metadata.EntityMetadata): { | ||
AttributeOf?: string; | ||
LogicalName: string; | ||
Targets: string[]; | ||
} { | ||
const attributeMetadata = entityMetadata.Attributes.get(attribute); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | ||
// @ts-ignore | ||
let attributeDescriptor = attributeMetadata.attributeDescriptor; | ||
if (attributeDescriptor.AttributeOf) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore | ||
// @ts-ignore | ||
attributeDescriptor = entityMetadata.Attributes.get(attributeDescriptor.AttributeOf).attributeDescriptor; | ||
} | ||
return attributeDescriptor; | ||
} | ||
|
||
private static async getRequiredAttributeLogicalNames(entityLogicalName: string): Promise<string[]> { | ||
const rawAttributesMetadatas = await WebApi.getAttributesMetadata(entityLogicalName, ['LogicalName', 'RequiredLevel']); | ||
return rawAttributesMetadatas | ||
.filter((attrMetadata: {RequiredLevel: {Value: string}}) => attrMetadata.RequiredLevel.Value === 'ApplicationRequired') | ||
.map((requiredAttributeMetadata: {LogicalName: string}) => requiredAttributeMetadata.LogicalName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
|
||
interface Expand { | ||
attribute: string; | ||
select: string[]; | ||
} | ||
|
||
interface SystemQueryOptions { | ||
select: string[]; | ||
expands?: Expand[]; | ||
} | ||
|
||
type Order = 'asc' | 'desc'; | ||
interface OrderBy { | ||
attribute: string; | ||
order?: Order; | ||
} | ||
|
||
// https://docs.microsoft.com/en-us/dynamics365/customer-engagement/web-api/tomorrow?view=dynamics-ce-odata-9 | ||
type QueryFunction = 'Above' | 'AboveOrEqual' | 'Between' | 'Contains' | 'ContainValues' | 'DoesNotContainValues' | 'EqualBusinessId' | 'EqualUserId' | | ||
'EqualUserLanguage' | 'EqualUserOrUserHierarchy' | 'EqualUserOrHierarchyAndTeams' | 'EqualUserOrUserTeams' | 'EqualUserTeams' | 'In' | 'InFiscalPeriod' | | ||
'InFiscalPeriodAndYear' | 'InFiscalYear' | 'InOrAfterFiscalPeriodAndYear' | 'InOrBeforeFiscalPeriodAndYear' | 'Last7Days' | 'LastFiscalPeriod' | 'LastFiscalYear' | | ||
'LastMonth' | 'LastWeek' | 'LastXDays' | 'LastXFiscalPeriods' | 'LastXFiscalYears' | 'LastXHours' | 'LastXMonths' | 'LastXWeeks' | 'LastXYears' | 'LastYear' | | ||
'Next7Days' | 'NextFiscalPeriod' | 'NextFiscalYear' | 'NextMonth' | 'NextWeek' | 'NextXDays' | 'NextXFiscalPeriods' | 'NextXFiscalYears' | 'NextXHours' | | ||
'NextXMonths' | 'NextXWeeks' | 'NextXYears' | 'NextYear' | 'NotBetween' | 'NotEqualBusinessId' | 'NotEqualUserId' | 'NotIn' | 'NotUnder' | 'OlderThanXDays' | | ||
'OlderThanXHours' | 'OlderThanXMinutes' | 'OlderThanXMonths' | 'OlderThanXWeeks' | 'OlderThanXYears' | 'On' | 'OnOrAfter' | 'OnOrBefore' | 'ThisFiscalPerios' | | ||
'ThisFiscalYear' | 'ThisMonth' | 'ThisWeek' | 'ThisYear' | 'Today' | 'Tomorrow' | 'Under' | 'UnderOrEqual' | 'Yesterday'; | ||
const filterConditions = ['eq' , 'ne', 'gt', 'ge', 'lt', 'le'] as const; | ||
type FilterCondition = typeof filterConditions[number]; // 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le'; | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
interface Condition { | ||
attribute: string; | ||
operator?: FilterCondition | QueryFunction; | ||
value?: any; | ||
} | ||
/* eslint-enable @typescript-eslint/no-explicit-any */ | ||
|
||
type FilterType = 'and' | 'or' | 'not'; | ||
interface Filter { | ||
type?: FilterType; | ||
conditions: Condition[]; | ||
filters?: Filter[]; | ||
} | ||
|
||
interface MultipleSystemQueryOptions extends SystemQueryOptions { | ||
filters?: Filter[]; | ||
orders?: OrderBy[]; | ||
top?: number; | ||
} |
Oops, something went wrong.