@xrmforge/typegen 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +14 -3
- package/dist/index.d.ts +203 -2
- package/docs/architecture/00-README.md +26 -0
- package/docs/architecture/01-executive-summary.md +11 -0
- package/docs/architecture/02-packages.md +110 -0
- package/docs/architecture/03-generated-types.md +176 -0
- package/docs/architecture/04-cli.md +58 -0
- package/docs/architecture/05-build.md +50 -0
- package/docs/architecture/06-incremental.md +42 -0
- package/docs/architecture/07-http-client.md +59 -0
- package/docs/architecture/08-authentication.md +18 -0
- package/docs/architecture/09-testing.md +55 -0
- package/docs/architecture/10-eslint-plugin.md +82 -0
- package/docs/architecture/11-agent-md.md +38 -0
- package/docs/architecture/12-xrm-pitfalls.md +14 -0
- package/docs/architecture/13-helpers.md +50 -0
- package/docs/architecture/14-showcases.md +21 -0
- package/docs/architecture/15-ci-cd.md +49 -0
- package/docs/architecture/16-technical-debt.md +17 -0
- package/docs/architecture/17-roadmap.md +25 -0
- package/docs/architecture/18-design-principles.md +22 -0
- package/docs/architektur/00-README.md +26 -0
- package/docs/architektur/01-zusammenfassung.md +11 -0
- package/docs/architektur/02-packages.md +110 -0
- package/docs/architektur/03-generierte-typen.md +176 -0
- package/docs/architektur/04-cli.md +58 -0
- package/docs/architektur/05-build.md +50 -0
- package/docs/architektur/06-inkrementell.md +42 -0
- package/docs/architektur/07-http-client.md +59 -0
- package/docs/architektur/08-authentifizierung.md +18 -0
- package/docs/architektur/09-testing.md +55 -0
- package/docs/architektur/10-eslint-plugin.md +82 -0
- package/docs/architektur/11-agent-md.md +38 -0
- package/docs/architektur/12-xrm-fallstricke.md +14 -0
- package/docs/architektur/13-helpers.md +50 -0
- package/docs/architektur/14-showcases.md +21 -0
- package/docs/architektur/15-ci-cd.md +49 -0
- package/docs/architektur/16-technische-schulden.md +17 -0
- package/docs/architektur/17-roadmap.md +25 -0
- package/docs/architektur/18-designprinzipien.md +22 -0
- package/package.json +4 -8
- package/dist/helpers.d.ts +0 -203
- package/dist/helpers.js +0 -120
- package/dist/helpers.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 2. Package-Architektur
|
|
2
|
+
|
|
3
|
+
## 2.1 Package-Übersicht
|
|
4
|
+
|
|
5
|
+
| Package | Version | Tests | Beschreibung |
|
|
6
|
+
|---------|---------|-------|--------------|
|
|
7
|
+
| @xrmforge/typegen | 0.6.0 | 444 | Kern: Typgenerierungs-Engine, Metadaten-Client, HTTP-Client, Hilfsfunktionen |
|
|
8
|
+
| @xrmforge/cli | 0.4.2 | 10 | CLI: generate-, build-, init-Befehle |
|
|
9
|
+
| @xrmforge/testing | 0.2.0 | 76 | Test-Hilfsmittel: createFormMock, fireOnChange, setupXrmMock |
|
|
10
|
+
| @xrmforge/helpers | 0.1.0 | 59 | Browsersichere Laufzeit: select(), parseLookup(), typedForm(), Xrm-Konstanten, Action-Executors |
|
|
11
|
+
| @xrmforge/webapi | 0.1.0 | 45 | Typsicherer Xrm.WebApi-Client mit QueryBuilder |
|
|
12
|
+
| @xrmforge/devkit | 0.4.0 | 42 | Build-Orchestrierung, Scaffolding, AGENT.md-Generierung |
|
|
13
|
+
| @xrmforge/eslint-plugin | 0.2.0 | 32 | 5 D365-spezifische ESLint-Regeln |
|
|
14
|
+
|
|
15
|
+
**Gesamt:** 708 Tests über 7 Packages.
|
|
16
|
+
|
|
17
|
+
## 2.2 Abhängigkeitsgraph
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
@xrmforge/cli
|
|
21
|
+
|-- @xrmforge/typegen (generate-Befehl)
|
|
22
|
+
|-- @xrmforge/devkit (build- + init-Befehle)
|
|
23
|
+
'-- commander (CLI-Framework)
|
|
24
|
+
|
|
25
|
+
@xrmforge/typegen
|
|
26
|
+
|-- @azure/identity (Authentifizierung)
|
|
27
|
+
'-- fast-xml-parser (FormXml-Parsing)
|
|
28
|
+
|
|
29
|
+
@xrmforge/devkit
|
|
30
|
+
'-- esbuild (IIFE-Bundling)
|
|
31
|
+
|
|
32
|
+
@xrmforge/testing (keine Laufzeit-Abhängigkeiten)
|
|
33
|
+
@xrmforge/helpers (keine Laufzeit-Abhängigkeiten)
|
|
34
|
+
@xrmforge/webapi (keine Laufzeit-Abhängigkeiten)
|
|
35
|
+
@xrmforge/eslint-plugin (ESLint Peer-Abhängigkeit)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 2.3 Package-Details
|
|
39
|
+
|
|
40
|
+
### @xrmforge/typegen
|
|
41
|
+
|
|
42
|
+
Das Kern-Package. Enthält:
|
|
43
|
+
|
|
44
|
+
- **TypeGenerationOrchestrator** - Koordiniert die gesamte Generierungs-Pipeline
|
|
45
|
+
- **MetadataClient** - Fragt Dataverse-Metadaten ab (Entitäten, Formulare, OptionSets, Custom APIs)
|
|
46
|
+
- **DataverseHttpClient** - Belastbarer REST-Client mit Retry, Rate Limiting, Nebenläufigkeitssteuerung
|
|
47
|
+
- **ChangeDetector** - Inkrementelle Generierung über RetrieveMetadataChanges
|
|
48
|
+
- **MetadataCache** - Dateisystem-basiertes Caching mit Versionsstempeln
|
|
49
|
+
- **Generators** - Entitäts-Interfaces, Formular-Interfaces, OptionSet-Enums, Fields-Enums, EntityNames, Navigations-Properties, Action/Function-Executors
|
|
50
|
+
- **Helpers** - select(), parseLookup(), parseFormattedValue() (verschoben nach @xrmforge/helpers)
|
|
51
|
+
- **Xrm-Konstanten** - DisplayState, FormNotificationLevel, RequiredLevel, SubmitMode, SaveMode, ClientType, ClientState (verschoben nach @xrmforge/helpers)
|
|
52
|
+
- **Authentifizierung** - createCredential()-Factory für 4 Authentifizierungsmethoden
|
|
53
|
+
- **Logging** - Scope-basierte Logger mit austauschbaren Senken (Console, JSON, Silent)
|
|
54
|
+
- **Fehler** - Strukturierte Fehlerhierarchie mit ErrorCode-Enum (AUTH_1xxx, API_2xxx, META_3xxx, GEN_4xxx, CONFIG_5xxx)
|
|
55
|
+
|
|
56
|
+
### @xrmforge/cli
|
|
57
|
+
|
|
58
|
+
Kommandozeilen-Interface basierend auf commander.js. Drei Befehle:
|
|
59
|
+
- `xrmforge generate` - Orchestriert den TypeGenerationOrchestrator
|
|
60
|
+
- `xrmforge build` - Delegiert an devkit build()
|
|
61
|
+
- `xrmforge init` - Delegiert an devkit scaffoldProject()
|
|
62
|
+
|
|
63
|
+
### @xrmforge/testing
|
|
64
|
+
|
|
65
|
+
FormContext-Mocking für Unit-Tests:
|
|
66
|
+
- `createFormMock<TForm>(values)` - Erstellt einen vollständigen Mock aus einfachen Schlüssel-Wert-Paaren
|
|
67
|
+
- `MockAttribute` - getValue/setValue, Dirty-Tracking, onChange-Handler, Required Level, Submit Mode
|
|
68
|
+
- `MockControl` - Sichtbar, Deaktiviert, Label, Benachrichtigungen
|
|
69
|
+
- `MockUi` - Formular-Benachrichtigungen, Tab/Section-Stubs
|
|
70
|
+
- `MockEntity` - Entitäts-ID, Name, Primärattribut
|
|
71
|
+
- `fireOnChange(fieldName)` - Löst registrierte onChange-Handler aus
|
|
72
|
+
- `setupXrmMock(options)` / `teardownXrmMock()` - Globaler Xrm-Mock mit WebApi/Navigation-Stubs
|
|
73
|
+
|
|
74
|
+
### @xrmforge/helpers
|
|
75
|
+
|
|
76
|
+
Bündelt allen browsersicheren Laufzeitcode. Keine Node.js-Abhängigkeiten. Enthält:
|
|
77
|
+
- **Web-API-Helpers** - select(), parseLookup(), parseFormattedValue()
|
|
78
|
+
- **Xrm-Konstanten** - DisplayState, SubmitMode, RequiredLevel, SaveMode, ClientType, ClientState, FormNotificationLevel, OperationType
|
|
79
|
+
- **Action/Function-Executors** - createBoundAction(), executeRequest(), withProgress()
|
|
80
|
+
- **typedForm()-Proxy** - Proxy-basierter FormContext-Wrapper, bei dem `form.name` an `getAttribute('name')` delegiert
|
|
81
|
+
|
|
82
|
+
### @xrmforge/webapi
|
|
83
|
+
|
|
84
|
+
Typsicherer Wrapper um Xrm.WebApi:
|
|
85
|
+
- `retrieve<T>(entityName, id, query)` - Einzelner Datensatz
|
|
86
|
+
- `retrieveMultiple<T>(entityName, query, options)` - Mit Paginierung (maxPages)
|
|
87
|
+
- `create(entityName, data)` - Gibt Datensatz-ID zurück
|
|
88
|
+
- `update(entityName, id, data)` - Void
|
|
89
|
+
- `remove(entityName, id)` - Void
|
|
90
|
+
- `QueryBuilder` - Fluent API: `.select().filter().orderBy().top().expand().build()`
|
|
91
|
+
- `WebApiError` - Strukturierte Fehler mit statusCode, errorCode, innerMessage
|
|
92
|
+
|
|
93
|
+
### @xrmforge/devkit
|
|
94
|
+
|
|
95
|
+
Build-Orchestrierung und Projekt-Scaffolding:
|
|
96
|
+
- `build(config)` - Parallele esbuild-IIFE-Builds über Promise.allSettled
|
|
97
|
+
- `watch(config)` - esbuild-Watch-Modus mit Rebuild-Callbacks
|
|
98
|
+
- `scaffoldProject(config)` - Generiert 11 Projektdateien aus Vorlagen
|
|
99
|
+
- `validateBuildConfig(config)` / `resolveBuildConfig(config)` - Konfigurationsvalidierung
|
|
100
|
+
- `BuildError` mit Codes: CONFIG_INVALID, ENTRY_NOT_FOUND, BUILD_FAILED, WATCH_ERROR
|
|
101
|
+
- Vorlagensystem: 7 Textvorlagen in `src/scaffold/templates/`, geladen über `template-loader.ts`
|
|
102
|
+
|
|
103
|
+
### @xrmforge/eslint-plugin
|
|
104
|
+
|
|
105
|
+
5 Regeln für D365-Formularskripte (ESLint v9 Flat Config):
|
|
106
|
+
- `no-xrm-page` (error) - Verbietet die veraltete Xrm.Page-API
|
|
107
|
+
- `no-magic-optionset` (warn) - Verbietet Magic Numbers in OptionSet-Vergleichen
|
|
108
|
+
- `no-sync-webapi` (error) - Verbietet synchrone XMLHttpRequest
|
|
109
|
+
- `require-error-handling` (warn) - Verlangt try/catch in asynchronen on*-Event-Handlern
|
|
110
|
+
- `require-namespace` (warn) - Verbietet window/globalThis-Zuweisungen
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# 3. Generierte Typen
|
|
2
|
+
|
|
3
|
+
Die Ausführung von `xrmforge generate` erzeugt die folgenden TypeScript-Deklarationen:
|
|
4
|
+
|
|
5
|
+
## 3.1 Entitäts-Interfaces (`entities/{entity}.d.ts`)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
declare namespace XrmForge.Entities {
|
|
9
|
+
/** Account | Konto */
|
|
10
|
+
interface Account {
|
|
11
|
+
/** Account Name | Kontoname */
|
|
12
|
+
name: string | null;
|
|
13
|
+
accountid: string | null;
|
|
14
|
+
revenue: number | null;
|
|
15
|
+
_parentaccountid_value: string | null; // Lookup GUID
|
|
16
|
+
// ...
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Typ-Zuordnung:** String/Memo/EntityName zu `string`, Integer/BigInt/Decimal/Double/Money zu `number`, Boolean zu `boolean`, DateTime/Uniqueidentifier/Lookup zu `string`, Picklist/State/Status zu `number`.
|
|
22
|
+
|
|
23
|
+
## 3.2 Entity Fields Enums (`entities/{entity}.d.ts`)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
declare namespace XrmForge.Entities {
|
|
27
|
+
const enum AccountFields {
|
|
28
|
+
/** Account Name | Kontoname */
|
|
29
|
+
Name = 'name',
|
|
30
|
+
Revenue = 'revenue',
|
|
31
|
+
// alle lesbaren Attribute
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Verwendet für Web API `$select`: `select(AccountFields.Name, AccountFields.Revenue)`.
|
|
37
|
+
|
|
38
|
+
## 3.3 Navigations-Properties (`entities/{entity}.d.ts`)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
declare namespace XrmForge.Entities {
|
|
42
|
+
const enum AccountNavigation {
|
|
43
|
+
PrimaryContactId = 'primarycontactid',
|
|
44
|
+
ContactCustomerAccounts = 'contact_customer_accounts',
|
|
45
|
+
// OneToMany- + ManyToMany-Beziehungen
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 3.4 Formular-Interfaces (`forms/{entity}.d.ts`)
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
declare namespace XrmForge.Forms.Account {
|
|
54
|
+
// Union-Typ, der gültige Feldnamen einschränkt
|
|
55
|
+
type AccountMainFormFields = 'name' | 'telephone1' | 'revenue';
|
|
56
|
+
|
|
57
|
+
// Gemappter Typ: Feldname zu Xrm-Attributtyp
|
|
58
|
+
type AccountMainFormAttributeMap = {
|
|
59
|
+
name: Xrm.Attributes.StringAttribute;
|
|
60
|
+
telephone1: Xrm.Attributes.StringAttribute;
|
|
61
|
+
revenue: Xrm.Attributes.NumberAttribute;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Gemappter Typ: Feldname zu Xrm-Steuerelementtyp
|
|
65
|
+
type AccountMainFormControlMap = {
|
|
66
|
+
name: Xrm.Controls.StringControl;
|
|
67
|
+
telephone1: Xrm.Controls.StringControl;
|
|
68
|
+
revenue: Xrm.Controls.NumberControl;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Fields-Enum für Autovervollständigung
|
|
72
|
+
const enum AccountMainFormFieldsEnum {
|
|
73
|
+
/** Account Name | Kontoname */
|
|
74
|
+
AccountName = 'name',
|
|
75
|
+
Telephone1 = 'telephone1',
|
|
76
|
+
Revenue = 'revenue',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Typsicherer FormContext mit überladenen getAttribute/getControl
|
|
80
|
+
interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
|
|
81
|
+
getAttribute<K extends AccountMainFormFields>(name: K): AccountMainFormAttributeMap[K];
|
|
82
|
+
getAttribute(index: number): Xrm.Attributes.Attribute;
|
|
83
|
+
getAttribute(): Xrm.Attributes.Attribute[];
|
|
84
|
+
|
|
85
|
+
getControl<K extends AccountMainFormFields>(name: K): AccountMainFormControlMap[K];
|
|
86
|
+
getControl(index: number): Xrm.Controls.Control;
|
|
87
|
+
getControl(): Xrm.Controls.Control[];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Spezielle Steuerelemente** werden anhand ihrer FormXml-ClassID typisiert:
|
|
93
|
+
- Subgrid: `Xrm.Controls.GridControl`
|
|
94
|
+
- Editierbares Grid: `Xrm.Controls.GridControl`
|
|
95
|
+
- Quick View: `Xrm.Controls.QuickFormControl`
|
|
96
|
+
- Web Resource / iFrame: `Xrm.Controls.IframeControl`
|
|
97
|
+
|
|
98
|
+
## 3.5 Tabs/Sections/Subgrids/QuickViews Enums
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const enum AccountMainFormTabs { Summary = 'SUMMARY_TAB', Details = 'DETAILS_TAB' }
|
|
102
|
+
const enum AccountMainFormSections { General = 'GENERAL', Address = 'ADDRESS' }
|
|
103
|
+
const enum AccountMainFormSubgrids { Contacts = 'Contacts_Subgrid' }
|
|
104
|
+
const enum AccountMainFormQuickViews { ContactPreview = 'ContactQuickView' }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 3.6 OptionSet Enums (`optionsets/{entity}.d.ts`)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
declare namespace XrmForge.OptionSets.Account {
|
|
111
|
+
/** Account Category Code | Kontokategoriecode */
|
|
112
|
+
const enum AccountCategoryCode {
|
|
113
|
+
/** Preferred Customer | Bevorzugter Kunde */
|
|
114
|
+
PreferredCustomer = 1,
|
|
115
|
+
Standard = 2,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Umfasst Picklist-, Status-, State- und MultiSelectPicklist-Attribute. Doppelte Labels werden mit dem Suffix `_{Value}` disambiguiert.
|
|
121
|
+
|
|
122
|
+
## 3.7 EntityNames Enum (`entity-names.d.ts`)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
declare namespace XrmForge {
|
|
126
|
+
const enum EntityNames {
|
|
127
|
+
Account = 'account',
|
|
128
|
+
Contact = 'contact',
|
|
129
|
+
// alle Entitäten im Scope
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 3.8 MockValues-Typen (in Formular-Interfaces)
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
type AccountMainFormMockValues = {
|
|
138
|
+
name?: string | null;
|
|
139
|
+
telephone1?: string | null;
|
|
140
|
+
revenue?: number | null;
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Verwendet mit `createFormMock<AccountMainForm, AccountMainFormMockValues>({ name: 'Test' })`.
|
|
145
|
+
|
|
146
|
+
## 3.9 Action/Function Executors (`actions/{entity|global}.d.ts` + `.ts`)
|
|
147
|
+
|
|
148
|
+
**Deklaration (.d.ts):**
|
|
149
|
+
```typescript
|
|
150
|
+
declare namespace XrmForge.Actions {
|
|
151
|
+
interface NormalizePhoneParams { Input: string; AllowSuspicious?: boolean; }
|
|
152
|
+
interface NormalizePhoneResult { Normalized: string; Status: number; }
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Laufzeitmodul (.ts):**
|
|
157
|
+
```typescript
|
|
158
|
+
import { createUnboundAction } from '@xrmforge/typegen';
|
|
159
|
+
export const NormalizePhone = createUnboundAction<NormalizePhoneParams, NormalizePhoneResult>(
|
|
160
|
+
'markant_NormalizePhone',
|
|
161
|
+
{ Input: { typeName: 'String', structuralProperty: 1 } }
|
|
162
|
+
);
|
|
163
|
+
// Verwendung: const result = await NormalizePhone.execute({ Input: '123' });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Factory-Funktionen: `createBoundAction`, `createUnboundAction`, `createBoundFunction`, `createUnboundFunction`. Batch-Ausführung über `executeMultiple()`, Fortschritts-UI über `withProgress()`.
|
|
167
|
+
|
|
168
|
+
## 3.10 Zweisprachige Labels
|
|
169
|
+
|
|
170
|
+
Alle generierten JSDoc-Kommentare unterstützen zweisprachige Labels:
|
|
171
|
+
```typescript
|
|
172
|
+
/** Account Name | Kontoname */
|
|
173
|
+
Name = 'name',
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Deutsche Umlaute werden in Bezeichnern transliteriert: ae, oe, ue, ss (z.B. "Übergeordnet" wird zu `Uebergeordnet`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 4. CLI-Befehle
|
|
2
|
+
|
|
3
|
+
## 4.1 `xrmforge generate`
|
|
4
|
+
|
|
5
|
+
Generiert TypeScript-Deklarationen aus einer Dataverse-Umgebung.
|
|
6
|
+
|
|
7
|
+
| Flag | Typ | Standard | Beschreibung |
|
|
8
|
+
|------|-----|----------|--------------|
|
|
9
|
+
| `--url <url>` | string | erforderlich | Dataverse-Umgebungs-URL |
|
|
10
|
+
| `--auth <method>` | string | erforderlich | Authentifizierungsmethode: client-credentials, interactive, device-code, token |
|
|
11
|
+
| `--tenant-id <id>` | string | variiert | Azure AD Tenant-ID |
|
|
12
|
+
| `--client-id <id>` | string | variiert | Azure AD Application-ID |
|
|
13
|
+
| `--client-secret <s>` | string | variiert | Client Secret (nur client-credentials) |
|
|
14
|
+
| `--token <token>` | string | variiert | Vorab erworbenes Bearer-Token (nur Token-Auth) |
|
|
15
|
+
| `--entities <list>` | string | - | Kommagetrennte logische Entitätsnamen |
|
|
16
|
+
| `--solutions <list>` | string | - | Kommagetrennte eindeutige Lösungsnamen |
|
|
17
|
+
| `--output <dir>` | string | ./typings | Ausgabeverzeichnis |
|
|
18
|
+
| `--label-language <n>` | string | 1033 | Primäre Label-Sprache (LCID) |
|
|
19
|
+
| `--secondary-language <n>` | string | - | Sekundäre Label-Sprache für JSDoc |
|
|
20
|
+
| `--no-forms` | flag | - | Formular-Interface-Generierung überspringen |
|
|
21
|
+
| `--no-optionsets` | flag | - | OptionSet-Enum-Generierung überspringen |
|
|
22
|
+
| `--actions` | flag | false | Custom-API-Executors generieren |
|
|
23
|
+
| `--actions-filter <prefix>` | string | - | Custom APIs nach Uniquename-Präfix filtern |
|
|
24
|
+
| `--cache` | flag | false | Metadaten-Caching für inkrementelle Generierung aktivieren |
|
|
25
|
+
| `--no-cache` | flag | - | Vollständige Metadaten-Aktualisierung erzwingen |
|
|
26
|
+
| `--cache-dir <dir>` | string | .xrmforge/cache | Cache-Verzeichnis |
|
|
27
|
+
| `-v, --verbose` | flag | false | Debug-Logging |
|
|
28
|
+
|
|
29
|
+
Mindestens eines von `--entities` oder `--solutions` ist erforderlich.
|
|
30
|
+
|
|
31
|
+
## 4.2 `xrmforge build`
|
|
32
|
+
|
|
33
|
+
Baut WebResources als IIFE-Bundles mit esbuild (über @xrmforge/devkit).
|
|
34
|
+
|
|
35
|
+
| Flag | Typ | Standard | Beschreibung |
|
|
36
|
+
|------|-----|----------|--------------|
|
|
37
|
+
| `--watch` | flag | false | Watch-Modus mit inkrementellen Rebuilds |
|
|
38
|
+
| `--minify` | flag | aus Konfiguration | Minifizierungseinstellung überschreiben |
|
|
39
|
+
| `--no-sourcemap` | flag | - | Source Maps deaktivieren |
|
|
40
|
+
| `--out-dir <dir>` | string | aus Konfiguration | Ausgabeverzeichnis überschreiben |
|
|
41
|
+
| `-v, --verbose` | flag | false | Fehlerstacks anzeigen |
|
|
42
|
+
|
|
43
|
+
Liest die Konfiguration aus `xrmforge.config.json`.
|
|
44
|
+
|
|
45
|
+
## 4.3 `xrmforge init`
|
|
46
|
+
|
|
47
|
+
Erstellt ein neues D365-Formularskript-Projekt.
|
|
48
|
+
|
|
49
|
+
| Flag | Typ | Standard | Beschreibung |
|
|
50
|
+
|------|-----|----------|--------------|
|
|
51
|
+
| `[dir]` | positional | . | Zielverzeichnis |
|
|
52
|
+
| `--name <name>` | string | Verzeichnisname | Projektname für package.json |
|
|
53
|
+
| `--prefix <prefix>` | string | contoso | Publisher-Präfix |
|
|
54
|
+
| `--namespace <ns>` | string | PascalCase(prefix) | Basis-Namespace für Skripte |
|
|
55
|
+
| `--skip-install` | flag | false | npm install überspringen |
|
|
56
|
+
| `--force` | flag | false | Nicht-leere Verzeichnisse erlauben |
|
|
57
|
+
|
|
58
|
+
Generiert 11 Dateien: package.json, tsconfig.json, xrmforge.config.json, vitest.config.ts, .gitignore, AGENT.md, example-form.ts, example-form.test.ts, typings/.gitkeep, GitHub Actions CI, Azure DevOps Pipeline.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 5. Build-Architektur
|
|
2
|
+
|
|
3
|
+
## 5.1 esbuild IIFE-Bundles
|
|
4
|
+
|
|
5
|
+
XrmForge verwendet esbuild, um IIFE-Bundles (Immediately Invoked Function Expression) für Dynamics 365 zu erstellen. D365 erfordert, dass Skripte als namespace.function registriert werden (z.B. `Contoso.Account.onLoad`).
|
|
6
|
+
|
|
7
|
+
**esbuild-Konfiguration pro Eintrag:**
|
|
8
|
+
```
|
|
9
|
+
format: 'iife'
|
|
10
|
+
bundle: true
|
|
11
|
+
globalName: entry.namespace // z.B. 'Contoso.Account'
|
|
12
|
+
target: config.target // Standard: 'es2020'
|
|
13
|
+
minify: config.minify
|
|
14
|
+
sourcemap: config.sourcemap
|
|
15
|
+
external: config.external // z.B. ['fs', 'path'] für Node.js-Abhängigkeiten
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Alle Einträge werden parallel mit `Promise.allSettled()` gebaut, was Teilerfolge ermöglicht.
|
|
19
|
+
|
|
20
|
+
## 5.2 xrmforge.config.json Schema
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"build": {
|
|
25
|
+
"outDir": "./dist/prefix_/JS",
|
|
26
|
+
"target": "es2020",
|
|
27
|
+
"sourcemap": true,
|
|
28
|
+
"minify": true,
|
|
29
|
+
"external": [],
|
|
30
|
+
"entries": {
|
|
31
|
+
"entry_name": {
|
|
32
|
+
"input": "./src/forms/account-form.ts",
|
|
33
|
+
"namespace": "Contoso.Account",
|
|
34
|
+
"out": "Account/OnLoad.js"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 5.3 globalName-Behandlung
|
|
42
|
+
|
|
43
|
+
esbuild erstellt automatisch verschachtelte Globals aus gepunkteten Namespaces:
|
|
44
|
+
```javascript
|
|
45
|
+
// namespace: "Contoso.Account" erzeugt:
|
|
46
|
+
var Contoso = Contoso || {};
|
|
47
|
+
Contoso.Account = (() => { return { onLoad, onSave }; })();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
D365-Event-Registrierung: `Contoso.Account.onLoad`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 6. Inkrementelle Generierung
|
|
2
|
+
|
|
3
|
+
## 6.1 Übersicht
|
|
4
|
+
|
|
5
|
+
Die inkrementelle Generierung verwendet die Dataverse-Funktion `RetrieveMetadataChanges`, um zu erkennen, welche Entitäten sich seit der letzten Generierung geändert haben. Dies reduziert die Generierungszeit von Sekunden auf Millisekunden (gemessen: 4720ms auf 473ms, 10-fache Verbesserung).
|
|
6
|
+
|
|
7
|
+
## 6.2 Komponenten
|
|
8
|
+
|
|
9
|
+
**ChangeDetector** (`src/metadata/change-detector.ts`):
|
|
10
|
+
- `getInitialVersionStamp()` - Erster Lauf: holt den initialen ServerVersionStamp
|
|
11
|
+
- `detectChanges(clientVersionStamp)` - Folgeläufe: gibt changedEntityNames, deletedEntityNames, newVersionStamp zurück
|
|
12
|
+
|
|
13
|
+
**MetadataCache** (`src/metadata/cache.ts`):
|
|
14
|
+
- Dateisystem-basiert: `.xrmforge/cache/metadata.json`
|
|
15
|
+
- Speichert: Manifest (Version, Umgebungs-URL, ServerVersionStamp, letzter Refresh, Entitätsliste) + entityTypeInfos pro Entität
|
|
16
|
+
- Validierung: prüft Cache-Version, Umgebungs-URL-Übereinstimmung, Dateiexistenz
|
|
17
|
+
|
|
18
|
+
## 6.3 Ablauf
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Erster Lauf (kein Cache):
|
|
22
|
+
1. Alle Entitäts-Metadaten abrufen
|
|
23
|
+
2. getInitialVersionStamp()
|
|
24
|
+
3. Cache mit ServerVersionStamp speichern
|
|
25
|
+
|
|
26
|
+
Folgelauf (Cache vorhanden):
|
|
27
|
+
1. Cache laden, Umgebungs-URL validieren
|
|
28
|
+
2. detectChanges(cachedVersionStamp)
|
|
29
|
+
3. Nur geänderte Entitäten abrufen
|
|
30
|
+
4. Gelöschte Entitäten aus Cache entfernen
|
|
31
|
+
5. Cache mit neuem ServerVersionStamp speichern
|
|
32
|
+
|
|
33
|
+
Abgelaufener Stempel (>90 Tage):
|
|
34
|
+
Fehlercode 0x80044352 erkannt
|
|
35
|
+
Rückfall auf vollständigen Refresh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 6.4 RetrieveMetadataChanges API
|
|
39
|
+
|
|
40
|
+
- **Typ:** OData-Funktion (GET, nicht POST)
|
|
41
|
+
- **URL:** `/RetrieveMetadataChanges(Query=@q,ClientVersionStamp=@s)?@q={...}&@s='...'`
|
|
42
|
+
- **Antwort:** EntityMetadata[] mit HasChanged-Flag, ServerVersionStamp, DeletedMetadata
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 7. HTTP-Client
|
|
2
|
+
|
|
3
|
+
## 7.1 DataverseHttpClient
|
|
4
|
+
|
|
5
|
+
Der zentrale HTTP-Client (`src/http/client.ts`) bietet belastbare Kommunikation mit der Dataverse Web API.
|
|
6
|
+
|
|
7
|
+
**Kernmethoden:**
|
|
8
|
+
- `get<T>(path, signal?)` - Einzelne GET-Anfrage mit Retry
|
|
9
|
+
- `getAll<T>(path, signal?)` - GET mit automatischer @odata.nextLink-Paginierung (max 100 Seiten)
|
|
10
|
+
|
|
11
|
+
## 7.2 Nur-Lesen-Standard
|
|
12
|
+
|
|
13
|
+
Der Client ist standardmäßig auf `readOnly: true` eingestellt und blockiert POST/PATCH/PUT/DELETE-Anfragen. Dies verhindert versehentliche Datenänderungen während der Typgenerierung. Schreibzugriff erfordert explizites `readOnly: false`.
|
|
14
|
+
|
|
15
|
+
## 7.3 Retry mit exponentiellem Backoff
|
|
16
|
+
|
|
17
|
+
- **Basisverzögerung:** 1000ms (konfigurierbar)
|
|
18
|
+
- **Maximaler Backoff:** 60 Sekunden
|
|
19
|
+
- **Jitter:** Zufällige Verzögerung bis zur Basisverzögerung
|
|
20
|
+
- **Formel:** `min(baseDelay * 2^(attempt-1) + jitter, 60000)`
|
|
21
|
+
- **Maximale Retries:** konfigurierbar (Standard: 3)
|
|
22
|
+
|
|
23
|
+
## 7.4 Rate Limiting (HTTP 429)
|
|
24
|
+
|
|
25
|
+
- **Separater Zähler** von Standard-Retries (nicht vermischt)
|
|
26
|
+
- **Retry-After-Header** wird respektiert (Sekunden in Millisekunden umgerechnet)
|
|
27
|
+
- **Maximal 10 aufeinanderfolgende 429-Retries** (DEFAULT_MAX_RATE_LIMIT_RETRIES)
|
|
28
|
+
- 429-Antworten inkrementieren den Standard-Retry-Zähler NICHT
|
|
29
|
+
|
|
30
|
+
## 7.5 Nebenläufigkeitssteuerung
|
|
31
|
+
|
|
32
|
+
Nicht-rekursives Semaphor-Muster:
|
|
33
|
+
- **maxConcurrency:** 5 (Standard)
|
|
34
|
+
- Warteschlange mit FIFO-Reihenfolge
|
|
35
|
+
- Alle Retries erfolgen innerhalb eines einzelnen Slots (verhindert Slot-Erschöpfung)
|
|
36
|
+
|
|
37
|
+
## 7.6 Token-Caching
|
|
38
|
+
|
|
39
|
+
- Nur im Speicher (wird niemals auf die Festplatte persistiert)
|
|
40
|
+
- 5-Minuten-Puffer vor Ablauf (TOKEN_BUFFER_MS = 300000)
|
|
41
|
+
- Ausstehende Token-Refresh-Promise verhindert gleichzeitige Token-Anfragen
|
|
42
|
+
|
|
43
|
+
## 7.7 Eingabe-Sanitisierung
|
|
44
|
+
|
|
45
|
+
OData-Injection-Prävention:
|
|
46
|
+
- `sanitizeIdentifier()` - Regex `[a-zA-Z_][a-zA-Z0-9_]*`
|
|
47
|
+
- `sanitizeGuid()` - GUID-Format-Validierung
|
|
48
|
+
- `escapeODataString()` - Verdopplung einfacher Anführungszeichen
|
|
49
|
+
|
|
50
|
+
## 7.8 Fehlerbehandlung
|
|
51
|
+
|
|
52
|
+
| HTTP-Status | Verhalten | Retry |
|
|
53
|
+
|-------------|-----------|-------|
|
|
54
|
+
| 2xx | Erfolg | Nein |
|
|
55
|
+
| 401 | Token-Cache leeren, einmal wiederholen | Ja (1x) |
|
|
56
|
+
| 429 | Retry-After respektieren, separater Zähler | Ja (bis zu 10x) |
|
|
57
|
+
| 5xx | Exponentieller Backoff | Ja (bis zu maxRetries) |
|
|
58
|
+
| 404, 403 | Nicht wiederholbar | Nein |
|
|
59
|
+
| Netzwerkfehler | Exponentieller Backoff | Ja |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 8. Authentifizierung
|
|
2
|
+
|
|
3
|
+
## 8.1 Credential Factory
|
|
4
|
+
|
|
5
|
+
`createCredential(config: AuthConfig)` gibt ein `TokenCredential` (aus @azure/identity) basierend auf der Authentifizierungsmethode zurück:
|
|
6
|
+
|
|
7
|
+
## 8.2 Vier Authentifizierungsabläufe
|
|
8
|
+
|
|
9
|
+
| Methode | Konfiguration | @azure/identity-Klasse | Anwendungsfall |
|
|
10
|
+
|---------|---------------|------------------------|----------------|
|
|
11
|
+
| client-credentials | tenantId, clientId, clientSecret | ClientSecretCredential | CI/CD, automatisierte Pipelines |
|
|
12
|
+
| interactive | tenantId, clientId? | InteractiveBrowserCredential | Entwickler-Arbeitsplatz |
|
|
13
|
+
| device-code | tenantId, clientId? | DeviceCodeCredential | Headless-CLI-Umgebungen |
|
|
14
|
+
| token | token (string) | StaticTokenCredential | Vorab erworbene Tokens (z.B. aus TokenVault) |
|
|
15
|
+
|
|
16
|
+
## 8.3 Token-Scope
|
|
17
|
+
|
|
18
|
+
Alle Authentifizierungsabläufe fordern den Scope an: `{environmentUrl}/.default`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 9. Test-Framework
|
|
2
|
+
|
|
3
|
+
## 9.1 createFormMock
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { createFormMock } from '@xrmforge/testing';
|
|
7
|
+
import type { AccountMainForm, AccountMainFormMockValues } from '../typings/forms/account';
|
|
8
|
+
|
|
9
|
+
const mock = createFormMock<AccountMainForm, AccountMainFormMockValues>({
|
|
10
|
+
name: 'Contoso Ltd',
|
|
11
|
+
statuscode: 0,
|
|
12
|
+
revenue: 1000000,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Verwendung in Tests:
|
|
16
|
+
onLoad(mock.executionContext);
|
|
17
|
+
expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Was gemockt wird:**
|
|
21
|
+
- Attribute: MockAttribute-Instanzen mit getValue/setValue, Dirty-Tracking, onChange-Handlern, Required Level, Submit Mode
|
|
22
|
+
- Steuerelemente: MockControl-Instanzen mit Sichtbarkeits-/Deaktiviert-/Label-/Benachrichtigungszustand
|
|
23
|
+
- UI: Formular-Benachrichtigungen, Tab/Section-Stubs
|
|
24
|
+
- Entität: ID, Entitätsname, Primärattribut
|
|
25
|
+
- Daten: refresh(), save() Stubs, die Promise-ähnliches zurückgeben
|
|
26
|
+
- Navigation: openForm/openAlertDialog Stubs
|
|
27
|
+
|
|
28
|
+
**Lazy-Initialisierung:** Attribute, die über `getAttribute()` angesprochen werden und nicht in den initialen Werten enthalten waren, werden spontan mit null-Wert erstellt.
|
|
29
|
+
|
|
30
|
+
## 9.2 fireOnChange
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
mock.fireOnChange('statuscode');
|
|
34
|
+
// Löst alle über getAttribute('statuscode').addOnChange(handler) registrierten Handler aus
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Erstellt einen MockEventContext mit dem Attribut als Eventquelle.
|
|
38
|
+
|
|
39
|
+
## 9.3 setupXrmMock / teardownXrmMock
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
|
|
43
|
+
|
|
44
|
+
beforeEach(() => setupXrmMock());
|
|
45
|
+
afterEach(() => teardownXrmMock());
|
|
46
|
+
|
|
47
|
+
// Mit WebApi-Überschreibungen:
|
|
48
|
+
setupXrmMock({
|
|
49
|
+
webApiOverrides: {
|
|
50
|
+
retrieveMultipleRecords: async () => ({ entities: [{ name: 'Test' }] }),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Richtet ein globales `Xrm`-Objekt auf `globalThis` ein mit minimalen WebApi-, Navigation- und Utility-Stubs.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 10. ESLint Plugin
|
|
2
|
+
|
|
3
|
+
## 10.1 Installation
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
// eslint.config.js (Flat Config, ESLint v9)
|
|
7
|
+
import xrmforge from '@xrmforge/eslint-plugin';
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
xrmforge.configs.recommended,
|
|
11
|
+
// oder einzelne Regeln auswählen
|
|
12
|
+
];
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 10.2 Regeln
|
|
16
|
+
|
|
17
|
+
### no-xrm-page (error)
|
|
18
|
+
|
|
19
|
+
Verbietet die veraltete `Xrm.Page`-API (entfernt in D365 v9.0+).
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Falsch
|
|
23
|
+
Xrm.Page.getAttribute("name");
|
|
24
|
+
|
|
25
|
+
// Richtig
|
|
26
|
+
const form = executionContext.getFormContext();
|
|
27
|
+
form.getAttribute("name");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### no-magic-optionset (warn)
|
|
31
|
+
|
|
32
|
+
Verbietet rohe Zahlen (>= 2) in Vergleichen mit `.getValue()`.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Falsch
|
|
36
|
+
if (attr.getValue() === 595300000) { }
|
|
37
|
+
|
|
38
|
+
// Richtig
|
|
39
|
+
import { StatusCode } from '../typings/optionsets/account';
|
|
40
|
+
if (attr.getValue() === StatusCode.Active) { }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### no-sync-webapi (error)
|
|
44
|
+
|
|
45
|
+
Verbietet synchrone XMLHttpRequest (`new XMLHttpRequest()` und `.open()` mit `async=false`).
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Falsch
|
|
49
|
+
xhr.open("GET", url, false);
|
|
50
|
+
|
|
51
|
+
// Richtig
|
|
52
|
+
const data = await Xrm.WebApi.retrieveRecord("account", id);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### require-error-handling (warn)
|
|
56
|
+
|
|
57
|
+
Verlangt try/catch in exportierten async-Funktionen, die mit "on" beginnen (Event-Handler).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Falsch
|
|
61
|
+
export async function onLoad(ctx) {
|
|
62
|
+
await fetch("/api"); // keine Fehlerbehandlung
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Richtig
|
|
66
|
+
export async function onLoad(ctx) {
|
|
67
|
+
try { await fetch("/api"); }
|
|
68
|
+
catch (error) { console.error(error); }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### require-namespace (warn)
|
|
73
|
+
|
|
74
|
+
Verbietet direkte `window.X = ...` oder `globalThis.X = ...` Zuweisungen. Stattdessen sollen Modul-Exports mit esbuild globalName verwendet werden.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Falsch
|
|
78
|
+
window.Contoso = { onLoad: function() {} };
|
|
79
|
+
|
|
80
|
+
// Richtig
|
|
81
|
+
export function onLoad(ctx: Xrm.Events.EventContext) {}
|
|
82
|
+
```
|