Skip to content
Merged
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
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ git tag xrmquery-v0.1.0 && git push origin xrmquery-v0.1.0 # -> npm

## Installation

The generated declarations build on the community-maintained standard Xrm declarations and the
modern XrmQuery package instead of shipping a private copy of the full client API surface:

```bash
npm install --save-dev @types/xrm
npm install @delegateas/xrmquery
```

When form generation is enabled, XDT emits `_internal/XrmDefinitelyTyped.d.ts`. This is a small,
generator-owned augmentation layer over `@types/xrm` that carries the additional precision XDT can
provide: lookup target unions, typed form collections, concrete quick-view forms, and typed option
set controls. It does not redeclare the complete `Xrm` namespace.

## Configuration

## Usage
Expand All @@ -48,8 +61,47 @@ xdt -o typings --generate web # web entity types for @delegateas/xrmquery
```

Web entity types land in `typings/Web/<entity>.d.ts` (or a single `typings/Web/WebEntities.d.ts` with
`--single-file`) inside the `XDT` namespace, overridable with `--web-namespace`. Option-set types are
written to `typings/_internal/Enum/` by whichever generator needs them.
`--single-file`) inside the `XDT` namespace, overridable with `--web-namespace`. Option-set numeric
union declarations are written to `typings/_internal/Enum/` by whichever generator needs them.

Named option-set values are emitted as modern `as const` runtime objects in `typings/OptionSets.ts`:

```typescript
import { ctx_subscription_statuscode } from "./typings/OptionSets";

const status: ctx_subscription_statuscode = ctx_subscription_statuscode.Active;
```

Importing the object provides discoverable named values while its same-named type remains the exact
union of the Dataverse numeric values. Unlike an ambient `const enum`, this works with
`isolatedModules` and module-based transpilers. Because it is a runtime value, the generated module
must be imported by application code that uses its members.

### Shared entity interfaces

Use `--intersect` (`-i`) to name the common contract of existing entities:

```bash
xdt -o typings --intersect "ICustomer:account;contact, IActivity:phonecall;email;task"
```

`ICustomer` represents what accounts and contacts have in common. With both generators enabled,
this produces shared form types such as `Form.ICustomer.Main.Information` from the common
controls, attributes, tabs and sections of forms with matching names and form types on every
member entity, plus `XDT.ICustomer_Select`,
`XDT.ICustomer_Filter`, `XDT.ICustomer_Result` and the other shared Web API interfaces.
The ordinary entity and form declarations are still generated.

Shared attribute metadata must have matching TypeScript and attribute types; incompatible fields
are omitted. Read/create/update permissions are retained only when supported by every entity.
A shared query contract has no entity set: query the actual `accounts` or `contacts` endpoint,
not an invented `ICustomer` endpoint. Different primary ID fields are not part of the common contract.

Include the named entities in the metadata selection (`--entities` / `--solutions`) if you filter it.
Missing entity metadata causes an error rather than silently using only some member entities.
Unmatched forms and matched forms with no compatible common controls or tabs are omitted.
An entity without forms does not prevent generation of its shared XrmQuery interfaces.
Intersection mappings accept entity logical names, not form GUIDs.

## Configuration Options

Expand Down
37 changes: 20 additions & 17 deletions docs/content/form-intersection.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
Form intersection
=================
Entity and form intersection
============================

Got client-side logic that needs to run on multiple forms which have similar attributes, control, tabs, etc.,
that you need to use in your code?
that you need to use in your code?

Make a declaration file using form intersection!

How it works
------------

Form intersection evaluates the forms specified, and combines all common attributes and controls,
tabs and sections into a single form interface.
Entity intersection names a shared contract, such as `ICustomer` for accounts and contacts or
`IActivity` for phone calls, emails and tasks. It matches fetched supported forms by name and form type across every named entity,
then emits their common attributes, controls, tabs and sections as shared form interfaces.
Unmatched forms and matched forms with no compatible common controls or tabs are omitted.
Entities without forms still participate in the shared XrmQuery contract.
The web generator also emits shared XrmQuery interfaces from compatible common entity attributes.

This makes it possible to create client-side code that can be used and shared safely across multiple forms.

<center><img src="img/form-intersection.png" /></center><br />

You can generate as many form intersection files as you want, and they are defined by passing in the GUIDs
of the forms you want to intersect with the `formIntersect`-argument. See [usage for more details](tool-usage.html).
Define intersections using entity logical names with `--intersect` (`-i`):

The intersection form interfaces can be found at `Form._special.<NAME-OF-FORM>`.
```bash
xdt -o typings --intersect "ICustomer:account;contact, IActivity:phonecall;email;task"
```

Include these entities in your metadata selection. All ordinary forms and entity types are still
emitted. Shared attributes must have compatible types; permissions must be supported by every
member entity. An intersection defines interfaces, not a new Dataverse table or entity set.
Query the real entity endpoints using the shared XrmQuery select/filter/result contracts.

How to find the GUID of a form
------------------------------
Intersection mappings take entity logical names, not form GUIDs.

My recommended way is to open the desired form, and execute `Xrm.Page.ui.formSelector.getCurrentItem().getId()`
in a developer console.

Alternatively, you can find it by opening the form in the form editor through your CRM solution,
and examining the `formId` query-parameter in the URL:

<center><img src="img/form-id-through-ui.png" /></center>
Shared form interfaces can be found at `Form.<INTERFACE-NAME>.<FORM-TYPE>.<FORM-NAME>`,
for example `Form.ICustomer.Main.Information`.
2 changes: 1 addition & 1 deletion docs/content/tool-usage.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Here is the full list of arguments for configuring the tool:
| oneFile | of | Set to true if all the dynamic parts of the generated declarations should be put into a single file.
| skipForms | sf | Set to true to skip generation of form declaration files.
| skipInactiveForms | sif | Set to true to avoid generating types for inactive forms.
| formIntersect | fi | Comma-separated list of named semicolon-separated lists of form GUIDs that should be intersected. Example: <br /> <code>MyAccountIntersect: 284FF02B-BDD1-4BB0-9BCF-6CFDBDA130D4;16068A3B-D428-4430-AEF6-397CE2AEFE07, MyContactIntersect: F4B3397C-C1A5-40BE-89DD-CEA5F7064D1D;824CFA3C-3EB4-4746-BA3C-7F1DFCA114C0</code>
| intersect | i | Comma-separated list of named semicolon-separated lists of entity logical names to intersect into shared form and XrmQuery interfaces. Example: <code>ICustomer:account;contact, IActivity:phonecall;email;task</code>. Only matching forms with compatible overlap are emitted; mappings accept entity logical names, not form GUIDs.
| useconfig | uc | Also applies the arguments found in the `.config` file.

You can also view this list of arguments using the "***/help***" argument.
Expand Down
10 changes: 9 additions & 1 deletion src/XrmDefinitelyTyped.Core/Domain/ControlModel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
namespace XrmDefinitelyTyped.Core.Domain;

public sealed record ControlModel(string Id, string? DataFieldName, string ClassId);
public sealed record ControlModel(
string Id,
string? DataFieldName,
string ClassId,
IReadOnlyList<string>? TargetEntityTypes = null,
bool CanBeNull = false,
bool IsBusinessProcessFlow = false,
Guid? QuickViewFormId = null,
string? QuickViewEntityLogicalName = null);
10 changes: 9 additions & 1 deletion src/XrmDefinitelyTyped.Core/Domain/FormModel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
namespace XrmDefinitelyTyped.Core.Domain;

public sealed record FormModel(Guid Id, string EntityLogicalName, string Name, FormType FormType, IReadOnlyList<TabModel> Tabs);
public sealed record FormModel(
Guid Id,
string EntityLogicalName,
string Name,
FormType FormType,
IReadOnlyList<TabModel> Tabs,
IReadOnlyList<ControlModel>? AdditionalControls = null,
bool IsIntersection = false,
string? MetadataEntityLogicalName = null);
6 changes: 3 additions & 3 deletions src/XrmDefinitelyTyped.Core/Domain/FormType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ public enum FormType
Main = 2,
MiniCampaignBO = 3,
Preview = 4,
MobileExpress = 5,
QuickView = 6,
Mobile = 5,
Quick = 6,
QuickCreate = 7,
Dialog = 8,
TaskFlowForm = 9,
InteractionCentricDashboard = 10,
Card = 11,
MainInteractiveExperience = 12,
MainInteractionCentric = 12,
ContextualDashboard = 13,
Other = 100,
MainBackup = 101,
Expand Down
62 changes: 32 additions & 30 deletions src/XrmDefinitelyTyped.Core/Generation/ClassIdMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,43 @@ namespace XrmDefinitelyTyped.Core.Generation;

internal static class ClassIdMap
{
internal const string WebResourceClassId = "WEBRESOURCE";
private static readonly IReadOnlyDictionary<string, ControlTypeInfo> Map =
new Dictionary<string, ControlTypeInfo>(StringComparer.OrdinalIgnoreCase)
{
["B0C6723A-8503-4FD7-BB28-C8A06AC933C2"] = new("Xrm.OptionSetControl<boolean>", "Xrm.OptionSetAttribute<boolean>"),
["5B773807-9FB2-42DB-97C3-7A91EFF8ADFF"] = new("Xrm.DateControl", "Xrm.DateAttribute"),
["C3EFE0C3-0EC6-42BE-8349-CBD9079DFD8E"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["AA987274-CE4E-4271-A803-66164311A958"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["ADA2203E-B4CD-49BE-9DDF-234642B43B52"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["6F3FB987-393B-4D2D-859F-9D0F0349B6AD"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["0D2C745A-E5A8-4C8F-BA63-C6D3BB604660"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["FD2A7985-3187-444E-908D-6624B21F69C0"] = new("Xrm.BaseControl", null),
["C6D124CA-7EDA-4A60-AEA9-7FB8D318B68F"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["671A9387-CA5A-4D1E-8AB7-06E39DDCF6B5"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["270BD3DB-D9AF-4782-9025-509E298DEC0A"] = new("Xrm.LookupControl<string>", "Xrm.LookupAttribute<string>"),
["533B9E00-756B-4312-95A0-DC888637AC78"] = new("Xrm.NumberControl", "Xrm.NumberAttribute"),
["06375649-C143-495E-A496-C962E5B4488E"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["CBFB742C-14E7-4A17-96BB-1A13F7F64AA2"] = new("Xrm.LookupControl<string>", "Xrm.LookupAttribute<string>"),
["3EF39988-22BB-4F0B-BBBE-64B5A3748AEE"] = new("Xrm.OptionSetControl<number>", "Xrm.OptionSetAttribute<number>"),
["67FAC785-CD58-4F9F-ABB3-4B7DDC6ED5ED"] = new("Xrm.OptionSetControl<boolean>", "Xrm.OptionSetAttribute<boolean>"),
["F3015350-44A2-4AA0-97B5-00166532B5E9"] = new("Xrm.LookupControl<string>", "Xrm.LookupAttribute<string>"),
["5D68B988-0661-4DB2-BC3E-17598AD3BE6C"] = new("Xrm.OptionSetControl<number>", "Xrm.OptionSetAttribute<number>"),
["E0DECE4B-6FC8-4A8F-A065-082708572369"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["4273EDBD-AC1D-40D3-9FB2-095C621B552D"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["1E1FC551-F7A8-43AF-AC34-A8DC35C7B6D4"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["7C624A0B-F59E-493D-9583-638D34759266"] = new("Xrm.OptionSetControl<number>", "Xrm.OptionSetAttribute<number>"),
["71716B6C-711E-476C-8AB8-5D11542BFB47"] = new("Xrm.StringControl", "Xrm.Attribute<string>"),
["5C5600E0-1D6E-4205-A272-BE80DA87FD42"] = new("Xrm.BaseControl", null),
["62B0DF79-0464-470F-8AF7-4483CFEA0C7D"] = new("Xrm.BaseControl", null),
["E7A81278-8635-4D9E-8D4D-59480B391C5B"] = new("Xrm.SubGridControl<string>", null),
["9C5CA0A1-AB4D-4781-BE7E-8DFBE867B87E"] = new("Xrm.BaseControl", null),
["4AA28AB7-9C13-4F57-A73D-AD894D048B5F"] = new("Xrm.MultiSelectOptionSetControl<number>", "Xrm.OptionSetAttribute<number>"),
["E616A57F-20E0-4534-8662-A101B5DDF4E0"] = new("Xrm.BaseControl", null),
["B0C6723A-8503-4FD7-BB28-C8A06AC933C2"] = new("XDTForm.BooleanControl", "Xrm.Attributes.BooleanAttribute"),
["5B773807-9FB2-42DB-97C3-7A91EFF8ADFF"] = new("Xrm.Controls.DateControl", "Xrm.Attributes.DateAttribute"),
["C3EFE0C3-0EC6-42BE-8349-CBD9079DFD8E"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["AA987274-CE4E-4271-A803-66164311A958"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["ADA2203E-B4CD-49BE-9DDF-234642B43B52"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["6F3FB987-393B-4D2D-859F-9D0F0349B6AD"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["0D2C745A-E5A8-4C8F-BA63-C6D3BB604660"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["FD2A7985-3187-444E-908D-6624B21F69C0"] = new("Xrm.Controls.IframeControl", null),
["C6D124CA-7EDA-4A60-AEA9-7FB8D318B68F"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["671A9387-CA5A-4D1E-8AB7-06E39DDCF6B5"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["270BD3DB-D9AF-4782-9025-509E298DEC0A"] = new("XDTForm.LookupControl<string>", "XDTForm.LookupAttribute<string>"),
["533B9E00-756B-4312-95A0-DC888637AC78"] = new("Xrm.Controls.NumberControl", "Xrm.Attributes.NumberAttribute"),
["06375649-C143-495E-A496-C962E5B4488E"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["CBFB742C-14E7-4A17-96BB-1A13F7F64AA2"] = new("XDTForm.LookupControl<string>", "XDTForm.LookupAttribute<string>"),
["3EF39988-22BB-4F0B-BBBE-64B5A3748AEE"] = new("XDTForm.OptionSetControl<number>", "Xrm.Attributes.OptionSetAttribute<number>"),
["67FAC785-CD58-4F9F-ABB3-4B7DDC6ED5ED"] = new("XDTForm.BooleanControl", "Xrm.Attributes.BooleanAttribute"),
["F3015350-44A2-4AA0-97B5-00166532B5E9"] = new("XDTForm.LookupControl<string>", "XDTForm.LookupAttribute<string>"),
["5D68B988-0661-4DB2-BC3E-17598AD3BE6C"] = new("XDTForm.OptionSetControl<number>", "Xrm.Attributes.OptionSetAttribute<number>"),
["E0DECE4B-6FC8-4A8F-A065-082708572369"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["4273EDBD-AC1D-40D3-9FB2-095C621B552D"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["1E1FC551-F7A8-43AF-AC34-A8DC35C7B6D4"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["7C624A0B-F59E-493D-9583-638D34759266"] = new("XDTForm.OptionSetControl<number>", "Xrm.Attributes.OptionSetAttribute<number>"),
["71716B6C-711E-476C-8AB8-5D11542BFB47"] = new("Xrm.Controls.StringControl", "Xrm.Attributes.Attribute<string>"),
["5C5600E0-1D6E-4205-A272-BE80DA87FD42"] = new("Xrm.Controls.Control", null),
["62B0DF79-0464-470F-8AF7-4483CFEA0C7D"] = new("Xrm.Controls.Control", null),
["E7A81278-8635-4D9E-8D4D-59480B391C5B"] = new("XDTForm.SubGridControl<string>", null),
["9C5CA0A1-AB4D-4781-BE7E-8DFBE867B87E"] = new("Xrm.Controls.Control", null),
["4AA28AB7-9C13-4F57-A73D-AD894D048B5F"] = new("XDTForm.MultiSelectOptionSetControl<number>", "Xrm.Attributes.MultiSelectOptionSetAttribute<number>"),
["E616A57F-20E0-4534-8662-A101B5DDF4E0"] = new("Xrm.Controls.KbSearchControl", null),
[WebResourceClassId] = new("XDTForm.WebResourceControl", null),
};

private static readonly ControlTypeInfo Fallback = new("Xrm.BaseControl", "Xrm.Attribute<any>");
private static readonly ControlTypeInfo Fallback = new("Xrm.Controls.Control", "Xrm.Attributes.Attribute");

public static ControlTypeInfo Resolve(string classId) =>
Map.TryGetValue(classId.Trim('{', '}'), out var info) ? info : Fallback;
Expand Down
16 changes: 14 additions & 2 deletions src/XrmDefinitelyTyped.Core/Generation/CodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using XrmDefinitelyTyped.Core.Domain;
using XrmDefinitelyTyped.Core.Generation.Generators;
using XrmTyped.Shared.Domain;
using XrmTyped.Shared.Generation;

namespace XrmDefinitelyTyped.Core.Generation;
Expand All @@ -11,13 +12,24 @@ public sealed class CodeGenerator : ICodeGenerator
public IEnumerable<GeneratedFile> GenerateCode(
IEnumerable<FormModel> forms,
IEnumerable<object> customApis,
XdtGenerationConfig config) =>
GenerateCode(forms, [], customApis, config);

public IEnumerable<GeneratedFile> GenerateCode(
IEnumerable<FormModel> forms,
IEnumerable<EntityModel> entities,
IEnumerable<object> customApis,
XdtGenerationConfig config)
{
ArgumentNullException.ThrowIfNull(forms);
ArgumentNullException.ThrowIfNull(entities);
ArgumentNullException.ThrowIfNull(config);

var files = new List<GeneratedFile>();
files.AddRange(_formGenerator.Generate(forms.ToList()));
var files = new List<GeneratedFile>
{
new FormSupportGenerator().Generate(),
};
files.AddRange(_formGenerator.Generate(forms.ToList(), entities.ToList(), config));

// Custom API generation is not yet implemented. When added, guard with
// config.GenerateCustomApis and consume the customApis argument here.
Expand Down
Loading
Loading