@mohsinonxrm/dataverse-sdk-metadata 1.0.0 → 2.0.0-beta.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/README.md CHANGED
@@ -20,7 +20,8 @@ Complete TypeScript metadata management for Microsoft Dataverse - create, read,
20
20
  - ✅ **Relationship eligibility** - check which entities can participate in relationships
21
21
  - ✅ **Metadata change tracking** - RetrieveMetadataChanges with version stamps
22
22
  - ✅ **Strict typing** - separate Read/Create/Update types, internal fields excluded
23
- - ✅ **ESM-only** - works in Node 18+ and browsers
23
+ - ✅ **Builders** - `Metadata.entity`, `Metadata.string`, `Metadata.picklist`, `Metadata.lookup`, … return the create payloads with labels, `RequiredLevel` and `@odata.type` filled in
24
+ - ✅ **ESM-only** - works in Node 22+ and browsers
24
25
 
25
26
  ## Installation
26
27
 
@@ -51,11 +52,12 @@ const entityId = await metadataClient.createEntityDefinition({
51
52
  DisplayCollectionName: {
52
53
  LocalizedLabels: [{ Label: "Projects", LanguageCode: 1033 }],
53
54
  },
54
- OwnershipType: 1, // UserOwned
55
+ OwnershipType: "UserOwned", // or OwnershipTypes.UserOwned
56
+ IsActivity: false,
55
57
  HasActivities: true,
56
58
  HasNotes: true,
57
59
  Attributes: [
58
- // Primary name column (required)
60
+ // The primary name column comes first (required)
59
61
  {
60
62
  "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
61
63
  SchemaName: "new_Name",
@@ -72,6 +74,111 @@ const entityId = await metadataClient.createEntityDefinition({
72
74
 
73
75
  ---
74
76
 
77
+ ## Builders
78
+
79
+ The `Metadata` builders write the create payloads for you. Each is a plain function that returns
80
+ the create type `MetadataClient` takes (`EntityMetadataCreate`, `StringAttributeMetadataCreate`,
81
+ `OneToManyRelationshipMetadataCreate`, …), with the parts that are easy to get wrong filled in:
82
+ the `@odata.type`, labels with their `LanguageCode`, `RequiredLevel`, a local choice's
83
+ `IsGlobal: false`, a global choice's bind path, and Dataverse's defaults.
84
+
85
+ ```typescript
86
+ import { Metadata, MetadataClient } from "@mohsinonxrm/dataverse-sdk-metadata";
87
+
88
+ // A table: its primary name column first, then any other columns
89
+ await metadataClient.createEntityDefinition(
90
+ Metadata.entity("new_Project", {
91
+ displayName: "Project",
92
+ displayCollectionName: "Projects",
93
+ description: "Projects we run",
94
+ primaryName: Metadata.primaryName("new_Name", { displayName: "Name" }),
95
+ attributes: [
96
+ Metadata.string("new_Code", { displayName: "Code", maxLength: 20 }),
97
+ Metadata.dateTime("new_DueDate", { displayName: "Due date", format: "DateOnly" }),
98
+ ],
99
+ })
100
+ );
101
+
102
+ // Columns
103
+ await metadataClient.createAttributeDefinition(
104
+ "new_project",
105
+ Metadata.picklist("new_Status", {
106
+ displayName: "Status",
107
+ requiredLevel: "ApplicationRequired",
108
+ options: ["Not started", "In progress", { label: "Done", value: 100000009 }],
109
+ })
110
+ );
111
+ await metadataClient.createAttributeDefinition(
112
+ "new_project",
113
+ Metadata.money("new_Budget", { displayName: "Budget", minValue: 0 })
114
+ );
115
+
116
+ // A lookup is created with its 1:N relationship
117
+ await metadataClient.createOneToManyRelationship(
118
+ Metadata.lookup("new_AccountId", {
119
+ displayName: "Account",
120
+ referencingEntity: "new_project", // gets the lookup column
121
+ referencedEntity: "account", // the table it points to
122
+ relationshipSchemaName: "new_account_project",
123
+ })
124
+ );
125
+ ```
126
+
127
+ | Builder | Returns | Defaults |
128
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
129
+ | `Metadata.label(text, lcid?)`, `Metadata.label({ 1033: "Name", 1036: "Nom" })` | `Label` | language 1033 |
130
+ | `Metadata.entity(schemaName, …)` | `EntityMetadataCreate` | `UserOwned`, no activities, no notes |
131
+ | `Metadata.primaryName(schemaName, …)` | `PrimaryNameAttributeMetadataCreate` | `MaxLength` 100 |
132
+ | `Metadata.string(schemaName, …)` | `StringAttributeMetadataCreate` | `MaxLength` 100, format `Text` |
133
+ | `Metadata.memo(schemaName, …)` | `MemoAttributeMetadataCreate` | `MaxLength` 2,000 |
134
+ | `Metadata.integer(schemaName, …)` | `IntegerAttributeMetadataCreate` | format `None` |
135
+ | `Metadata.decimal` / `Metadata.double(schemaName, …)` | `DecimalAttributeMetadataCreate` / `DoubleAttributeMetadataCreate` | precision 2 |
136
+ | `Metadata.money(schemaName, …)` | `MoneyAttributeMetadataCreate` | the currency's precision |
137
+ | `Metadata.dateTime(schemaName, …)` | `DateTimeAttributeMetadataCreate` | `DateAndTime` |
138
+ | `Metadata.boolean(schemaName, …)` | `BooleanAttributeMetadataCreate` | Yes (1) / No (0), default No |
139
+ | `Metadata.picklist` / `Metadata.multiSelectPicklist` | `PicklistAttributeMetadataCreate` / `MultiSelectPicklistAttributeMetadataCreate` | `options` or `globalOptionSetId`, exactly one |
140
+ | `Metadata.file` / `Metadata.image(schemaName, …)` | `FileAttributeMetadataCreate` / `ImageAttributeMetadataCreate` | 32 MB / 10 MB |
141
+ | `Metadata.lookup(schemaName, …)` | `OneToManyRelationshipMetadataCreate` | Dataverse's cascade and the referenced table's primary key |
142
+ | `Metadata.manyToMany(schemaName, …)` | `ManyToManyRelationshipMetadataCreate` | intersect table = the schema name, lowercased |
143
+ | `Metadata.globalOptionSet(name, …)` | `OptionSetMetadataCreate` | `IsGlobal: true`, `Picklist` |
144
+
145
+ Every column takes `displayName` (required), `description`, `requiredLevel` (`"None"`, the
146
+ default, `"Recommended"` or `"ApplicationRequired"`) and `languageCode`. Labels given as text
147
+ are in English (1033) unless you pass `languageCode`; Dataverse needs a label in the
148
+ organization's base language. Pass a `Label` (from `Metadata.label`) for several languages.
149
+
150
+ The types reject what Dataverse would: a choice with both `options` and a `globalOptionSetId` (or
151
+ neither), the `"DateOnly"` behavior without the `"DateOnly"` format, `SystemRequired`, a table
152
+ without its primary name or with a lookup among its `attributes`. At run time a builder throws
153
+ `DataverseInvalidRequestError` (`InvalidParameter`) for a schema name without the customization
154
+ prefix, an empty label, a length, precision or range out of bounds, a minimum above the maximum,
155
+ duplicate option values or a `globalOptionSetId` that isn't a GUID.
156
+
157
+ Choice options are labels (`"Small"`), which Dataverse numbers from the publisher's choice value
158
+ prefix, or `{ label, value, description, color }`. A global choice is bound by its `MetadataId`:
159
+
160
+ ```typescript
161
+ const id = await metadataClient.createGlobalOptionSet(
162
+ Metadata.globalOptionSet("new_priority", { displayName: "Priority", options: ["High", "Low"] })
163
+ );
164
+ await metadataClient.createAttributeDefinition(
165
+ "new_project",
166
+ Metadata.picklist("new_Priority", { displayName: "Priority", globalOptionSetId: id })
167
+ );
168
+ ```
169
+
170
+ A builder's result is an ordinary object of the create type: spread it to set a property the
171
+ builder doesn't take, and the compiler still checks it.
172
+
173
+ ```typescript
174
+ await metadataClient.createAttributeDefinition("new_project", {
175
+ ...Metadata.string("new_Code", { displayName: "Code" }),
176
+ IsAuditEnabled: { Value: true },
177
+ });
178
+ ```
179
+
180
+ ---
181
+
75
182
  ## Entity Operations
76
183
 
77
184
  ### Create Entity
@@ -83,7 +190,7 @@ const metadataId = await metadataClient.createEntityDefinition({
83
190
  DisplayName: { LocalizedLabels: [{ Label: "Task", LanguageCode: 1033 }] },
84
191
  DisplayCollectionName: { LocalizedLabels: [{ Label: "Tasks", LanguageCode: 1033 }] },
85
192
  Description: { LocalizedLabels: [{ Label: "Project task tracking", LanguageCode: 1033 }] },
86
- OwnershipType: 1, // UserOwned
193
+ OwnershipType: "UserOwned", // or OwnershipTypes.UserOwned
87
194
  IsActivity: false,
88
195
  HasActivities: true,
89
196
  HasNotes: true,
@@ -113,19 +220,106 @@ const customEntities = await metadataClient.getEntityDefinitions({
113
220
  const projectEntity = await metadataClient.getEntityDefinition("new_project", {
114
221
  expand: "Attributes",
115
222
  });
223
+
224
+ // Labels in specific languages (LCIDs), sent as LabelLanguages
225
+ const labels = await metadataClient.getEntityDefinition("account", {
226
+ select: ["DisplayName"],
227
+ labelLanguages: [1033, 1036],
228
+ });
229
+ ```
230
+
231
+ ### Entity Set Names
232
+
233
+ `getEntitySetName()` reads the entity set name used in Web API URLs from metadata, so you don't
234
+ have to guess the plural. The first call loads every table's name in one request and caches the
235
+ map on the `MetadataClient`; `clearEntitySetNameCache()` forgets it.
236
+
237
+ ```typescript
238
+ const set = await metadataClient.getEntitySetName("opportunity"); // "opportunities"
239
+ const record = await client.api(`${set}(${id})`).get();
240
+ ```
241
+
242
+ ### Read Columns by Type
243
+
244
+ Type-specific properties, such as a choice column's options, a lookup's `Targets` or a text
245
+ column's `MaxLength`, are only available through a type cast. Pass `cast` and the results are
246
+ typed accordingly:
247
+
248
+ ```typescript
249
+ // GET EntityDefinitions(LogicalName='account')/Attributes/Microsoft.Dynamics.CRM.PicklistAttributeMetadata
250
+ // ?LabelLanguages=1033&$select=LogicalName&$expand=OptionSet($select=Options),GlobalOptionSet($select=Options)
251
+ const picklists = await metadataClient.getAttributeDefinitions("account", {
252
+ cast: "PicklistAttributeMetadata",
253
+ select: ["LogicalName"],
254
+ expand: ["OptionSet($select=Options)", "GlobalOptionSet($select=Options)"],
255
+ labelLanguages: 1033,
256
+ });
257
+ const options = picklists[0]?.OptionSet?.Options; // picklists is PicklistAttributeMetadata[]
258
+
259
+ const lookups = await metadataClient.getAttributeDefinitions("account", {
260
+ cast: "LookupAttributeMetadata",
261
+ select: ["LogicalName", "Targets"],
262
+ });
263
+
264
+ const name = await metadataClient.getAttributeDefinition("account", "name", {
265
+ cast: "StringAttributeMetadata",
266
+ select: ["MaxLength", "Format"],
267
+ });
268
+ ```
269
+
270
+ `cast` takes any attribute metadata type name: `StringAttributeMetadata`, `MemoAttributeMetadata`,
271
+ `IntegerAttributeMetadata`, `BigIntAttributeMetadata`, `DecimalAttributeMetadata`,
272
+ `DoubleAttributeMetadata`, `MoneyAttributeMetadata`, `DateTimeAttributeMetadata`,
273
+ `BooleanAttributeMetadata`, `PicklistAttributeMetadata`, `MultiSelectPicklistAttributeMetadata`,
274
+ `StateAttributeMetadata`, `StatusAttributeMetadata`, `LookupAttributeMetadata`,
275
+ `FileAttributeMetadata`, `ImageAttributeMetadata`, `UniqueIdentifierAttributeMetadata`,
276
+ `EntityNameAttributeMetadata`, `EnumAttributeMetadata` or `ManagedPropertyAttributeMetadata`.
277
+
278
+ Key values (logical names, schema names, option set names) are escaped, so a name containing `'`
279
+ is sent as `'O''Brien'`.
280
+
281
+ Every `create*` method returns the new component's `MetadataId`. Dataverse answers a metadata
282
+ create with `204 No Content` and the id in the `OData-EntityId` header, which is where the SDK
283
+ reads it.
284
+
285
+ ### Create in a Solution
286
+
287
+ Every create and update takes an optional last argument, `{ solutionUniqueName }`, sent as the
288
+ `MSCRM.SolutionUniqueName` header. The component is added to that unmanaged solution; without it
289
+ the change goes to the default solution.
290
+
291
+ ```typescript
292
+ const metadataId = await metadataClient.createEntityDefinition(definition, {
293
+ solutionUniqueName: "ContosoProjects",
294
+ });
116
295
  ```
117
296
 
118
297
  ### Update Entity
119
298
 
299
+ Metadata updates use `PUT`, which **replaces** the whole definition: a property you leave out is
300
+ reset to its default. Either send the complete definition, with its `@odata.type`, to
301
+ `updateEntityDefinition()`, or let the SDK retrieve it, apply your changes and send it back:
302
+
120
303
  ```typescript
121
- // Update entity display name
304
+ // Change only the description; every other property keeps its current value
305
+ await metadataClient.mergeAndUpdateEntityDefinition("new_project", {
306
+ Description: { LocalizedLabels: [{ Label: "Manage projects and tasks", LanguageCode: 1033 }] },
307
+ });
308
+
309
+ // Or retrieve, change and send the whole definition yourself
310
+ const current = await metadataClient.getEntityDefinition("new_project");
122
311
  await metadataClient.updateEntityDefinition("new_project", {
312
+ ...current,
123
313
  "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
124
- DisplayName: { LocalizedLabels: [{ Label: "Project (Updated)", LanguageCode: 1033 }] },
125
- Description: { LocalizedLabels: [{ Label: "Manage projects and tasks", LanguageCode: 1033 }] },
314
+ HasNotes: true,
126
315
  });
127
316
  ```
128
317
 
318
+ Columns, relationships and global choices have the same pair: `updateAttributeDefinition()` /
319
+ `mergeAndUpdateAttributeDefinition()`, `updateOneToManyRelationship()` and
320
+ `updateManyToManyRelationship()` / `mergeAndUpdateRelationship()`, and `updateGlobalOptionSet()` /
321
+ `mergeAndUpdateGlobalOptionSet()`. See [Update Operations Use PUT](#update-operations-use-put).
322
+
129
323
  ### Delete Entity
130
324
 
131
325
  ```typescript
@@ -270,6 +464,7 @@ const relationshipId = await metadataClient.createOneToManyRelationship({
270
464
  ReferencedAttribute: "new_projectid",
271
465
  ReferencingEntity: "new_task",
272
466
  Lookup: {
467
+ "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
273
468
  SchemaName: "new_ProjectId",
274
469
  DisplayName: { LocalizedLabels: [{ Label: "Project", LanguageCode: 1033 }] },
275
470
  RequiredLevel: { Value: "ApplicationRequired" },
@@ -317,6 +512,7 @@ const relationshipId = await metadataClient.createOneToManyRelationship({
317
512
  ReferencedAttribute: "accountid",
318
513
  ReferencingEntity: "new_project", // "Many" side (child)
319
514
  Lookup: {
515
+ "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
320
516
  SchemaName: "new_AccountId",
321
517
  DisplayName: { LocalizedLabels: [{ Label: "Account", LanguageCode: 1033 }] },
322
518
  RequiredLevel: { Value: "None" },
@@ -374,30 +570,60 @@ const optionSetId = await metadataClient.createGlobalOptionSet({
374
570
 
375
571
  ### Use Global Choice in Column
376
572
 
573
+ A choice or choices (multi-select) column uses an existing global choice through
574
+ `"GlobalOptionSet@odata.bind"` instead of an `OptionSet` of its own; the create types accept
575
+ exactly one of the two. Dataverse binds by `MetadataId` only (`(Name='…')` is rejected), so keep
576
+ the id `createGlobalOptionSet` returns, or read it with `getGlobalOptionSet(name)`.
577
+
377
578
  ```typescript
378
- // Reference global choice when creating attribute
579
+ // Bind the column to the global choice created above, by its MetadataId
379
580
  await metadataClient.createAttributeDefinition("new_project", {
380
581
  "@odata.type": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata",
381
582
  SchemaName: "new_Type",
382
583
  DisplayName: { LocalizedLabels: [{ Label: "Type", LanguageCode: 1033 }] },
383
584
  RequiredLevel: { Value: "None" },
384
- GlobalOptionSet: {
385
- "@odata.bind": `/GlobalOptionSetDefinitions(Name='new_projecttype')`,
386
- },
585
+ "GlobalOptionSet@odata.bind": `/GlobalOptionSetDefinitions(${optionSetId})`,
586
+ });
587
+
588
+ // A choices (multi-select) column on a global choice found by name
589
+ const { MetadataId: projectTypeId } = await metadataClient.getGlobalOptionSet("new_projecttype", {
590
+ select: ["MetadataId"],
591
+ });
592
+ await metadataClient.createAttributeDefinition("new_project", {
593
+ "@odata.type": "Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata",
594
+ SchemaName: "new_Types",
595
+ DisplayName: { LocalizedLabels: [{ Label: "Types", LanguageCode: 1033 }] },
596
+ RequiredLevel: { Value: "None" },
597
+ "GlobalOptionSet@odata.bind": `/GlobalOptionSetDefinitions(${projectTypeId})`,
387
598
  });
388
599
  ```
389
600
 
390
601
  ### Insert Option Value
391
602
 
603
+ The option-value methods (`insertOptionValue`, `updateOptionValue`, `deleteOptionValue`,
604
+ `orderOptions`) take either `OptionSetName`, for a global choice, or `EntityLogicalName` and
605
+ `AttributeLogicalName`, for a column's own choice; passing both, or neither, is a type error and
606
+ throws `DataverseInvalidRequestError` before anything is sent. A column bound to a global choice
607
+ has no options of its own, so change the global choice. `insertStatusValue` always takes the
608
+ table and its `statuscode` column. `updateOptionValue` requires `MergeLabels`.
609
+
392
610
  ```typescript
393
- // Add new option to existing global choice
611
+ // Add a new option to a column's own (local) choice
394
612
  const result = await metadataClient.insertOptionValue({
613
+ EntityLogicalName: "new_project",
614
+ AttributeLogicalName: "new_status",
615
+ Label: { LocalizedLabels: [{ Label: "On Hold", LanguageCode: 1033 }] },
616
+ Value: 4, // Optional, auto-assigned if omitted
617
+ });
618
+ console.log("New option value:", result.NewOptionValue);
619
+
620
+ // Add a new option to a global choice
621
+ const added = await metadataClient.insertOptionValue({
395
622
  OptionSetName: "new_projecttype",
396
623
  Label: { LocalizedLabels: [{ Label: "Maintenance", LanguageCode: 1033 }] },
397
- Value: 100003, // Optional, auto-assigned if omitted
624
+ Value: 100003,
398
625
  });
399
-
400
- console.log("New option value:", result.NewOptionValue);
626
+ console.log("New option value:", added.NewOptionValue);
401
627
  ```
402
628
 
403
629
  ---
@@ -408,9 +634,9 @@ console.log("New option value:", result.NewOptionValue);
408
634
 
409
635
  ```typescript
410
636
  // Create customer lookup (targets account OR contact)
411
- const result = await metadataClient.createCustomerRelationships({
637
+ const result = await metadataClient.createCustomerRelationship({
412
638
  Lookup: {
413
- "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
639
+ "@odata.type": "Microsoft.Dynamics.CRM.ComplexLookupAttributeMetadata",
414
640
  SchemaName: "new_CustomerId",
415
641
  DisplayName: { LocalizedLabels: [{ Label: "Customer", LanguageCode: 1033 }] },
416
642
  RequiredLevel: { Value: "ApplicationRequired" },
@@ -438,7 +664,7 @@ const result = await metadataClient.createCustomerRelationships({
438
664
 
439
665
  ```typescript
440
666
  // Create lookup targeting multiple custom entities
441
- const result = await metadataClient.createPolymorphicLookupAttribute({
667
+ const result = await metadataClient.createPolymorphicLookup({
442
668
  OneToManyRelationships: [
443
669
  {
444
670
  "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
@@ -456,6 +682,7 @@ const result = await metadataClient.createPolymorphicLookupAttribute({
456
682
  },
457
683
  ],
458
684
  Lookup: {
685
+ "@odata.type": "Microsoft.Dynamics.CRM.ComplexLookupAttributeMetadata",
459
686
  SchemaName: "new_RegardingId",
460
687
  DisplayName: { LocalizedLabels: [{ Label: "Regarding", LanguageCode: 1033 }] },
461
688
  RequiredLevel: { Value: "None" },
@@ -467,89 +694,129 @@ const result = await metadataClient.createPolymorphicLookupAttribute({
467
694
 
468
695
  ## Relationship Eligibility Checks
469
696
 
697
+ `CanBeReferenced`, `CanBeReferencing` and `CanManyToMany` are Web API actions (sent as `POST`);
698
+ the `GetValid*` methods are functions (sent as `GET`). Each method returns the plain value.
699
+
470
700
  ```typescript
471
- // Check if entity can be referenced (parent in 1:N)
472
- const canBeReferenced = await metadataClient.canBeReferenced("new_project");
473
- console.log("Can be parent:", canBeReferenced.CanBeReferenced);
701
+ // Can the table be the parent (referenced) table in a 1:N relationship?
702
+ const canBeParent: boolean = await metadataClient.canBeReferenced("new_project");
474
703
 
475
- // Check if entity can reference others (child in 1:N)
476
- const canBeReferencing = await metadataClient.canBeReferencing("new_task");
477
- console.log("Can be child:", canBeReferencing.CanBeReferencing);
704
+ // Can the table be the child (referencing) table in a 1:N relationship?
705
+ const canBeChild: boolean = await metadataClient.canBeReferencing("new_task");
478
706
 
479
- // Check if entity can participate in N:N
480
- const canManyToMany = await metadataClient.canManyToMany("new_project");
481
- console.log("Can do N:N:", canManyToMany.CanManyToMany);
707
+ // Can the table take part in an N:N relationship?
708
+ const canDoManyToMany: boolean = await metadataClient.canManyToMany("new_project");
482
709
 
483
- // Get valid entities for N:N relationships
484
- const validEntities = await metadataClient.getValidManyToMany("new_project");
485
- console.log("Valid N:N partners:", validEntities.EntityNames);
710
+ // Tables that can reference account, and tables that contact can reference
711
+ const children: string[] = await metadataClient.getValidReferencingEntities("account");
712
+ const parents: string[] = await metadataClient.getValidReferencedEntities("contact");
713
+
714
+ // Every table that can take part in an N:N relationship (no argument)
715
+ const manyToManyTables: string[] = await metadataClient.getValidManyToMany();
486
716
  ```
487
717
 
488
718
  ---
489
719
 
490
720
  ## RetrieveMetadataChanges
491
721
 
492
- Track metadata changes using version stamps for incremental sync:
722
+ Track metadata changes with version stamps for incremental sync. The method is positional:
723
+ `retrieveMetadataChanges(query, clientVersionStamp?, deletedMetadataFilters?)`.
724
+ `RetrieveMetadataChanges` is a Web API function, so the SDK sends it as a `GET` with the query as
725
+ a JSON parameter alias.
493
726
 
494
727
  ```typescript
495
- // Initial query (get all + version stamp)
496
- const result = await metadataClient.retrieveMetadataChanges(
497
- {
498
- Criteria: {
499
- FilterOperator: "And",
500
- Conditions: [{ PropertyName: "IsCustomEntity", Value: true }],
501
- },
502
- Properties: {
503
- PropertyNames: ["LogicalName", "DisplayName", "Attributes"],
504
- },
728
+ import {
729
+ LogicalOperator,
730
+ MetadataConditionOperator,
731
+ type EntityQueryExpression,
732
+ } from "@mohsinonxrm/dataverse-sdk-metadata";
733
+
734
+ const query: EntityQueryExpression = {
735
+ Criteria: {
736
+ FilterOperator: LogicalOperator.And,
737
+ Conditions: [
738
+ {
739
+ PropertyName: "IsCustomEntity",
740
+ ConditionOperator: MetadataConditionOperator.Equals,
741
+ // A condition value is { Type, Value }
742
+ Value: { Type: "System.Boolean", Value: "true" },
743
+ },
744
+ ],
505
745
  },
506
- "Entity" // Track entity deletions
507
- );
746
+ Properties: { AllProperties: false, PropertyNames: ["LogicalName", "DisplayName"] },
747
+ };
508
748
 
749
+ // Initial query: the metadata, plus a version stamp
750
+ const result = await metadataClient.retrieveMetadataChanges(query);
509
751
  console.log("Entities:", result.EntityMetadata);
510
752
  console.log("Server version:", result.ServerVersionStamp);
511
753
 
512
- // Later: get only changes since last sync
754
+ // Later: only what changed since the stamp, including deleted tables
513
755
  const changes = await metadataClient.retrieveMetadataChanges(
514
- {
515
- Criteria: {
516
- FilterOperator: "And",
517
- Conditions: [{ PropertyName: "IsCustomEntity", Value: true }],
518
- },
519
- Properties: {
520
- PropertyNames: ["LogicalName", "DisplayName"],
521
- },
522
- },
523
- "Entity",
524
- result.ServerVersionStamp // Pass previous stamp
756
+ query,
757
+ result.ServerVersionStamp, // the previous stamp
758
+ "Default" // DeletedMetadataFilters: "Default" (tables), "Attribute", "All", or a comma-separated list
525
759
  );
526
-
527
760
  console.log("Changed entities:", changes.EntityMetadata);
528
- console.log("Deleted entities:", changes.DeletedMetadata);
761
+ console.log("Deleted metadata:", changes.DeletedMetadata);
529
762
  ```
530
763
 
764
+ Enum members in the query are sent by name (`"And"`, `"Equals"`), the only form Dataverse
765
+ accepts.
766
+
531
767
  ---
532
768
 
533
769
  ## Important Notes
534
770
 
535
771
  ### Update Operations Use PUT
536
772
 
537
- Per Microsoft documentation, all metadata update operations use HTTP PUT (not PATCH). This means you must send the complete definition with all properties, not just the changed ones:
773
+ Per Microsoft documentation, all metadata update operations use HTTP PUT (not PATCH). A PUT
774
+ replaces the definition, so a property you leave out is reset to its default. The `update*`
775
+ methods therefore take the complete definition, with its `@odata.type`; a body of just the
776
+ changed properties doesn't compile. Retrieve the definition, change it and send all of it back:
538
777
 
539
778
  ```typescript
540
- // Update entity - requires complete definition
541
- await metadataClient.updateEntityDefinition("new_project", {
542
- "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
543
- DisplayName: {
544
- LocalizedLabels: [{ Label: "Updated Project", LanguageCode: 1033 }],
545
- },
546
- Description: {
547
- LocalizedLabels: [{ Label: "Updated description", LanguageCode: 1033 }],
548
- },
549
- // Include all other properties you want to preserve
779
+ const note = await metadataClient.getAttributeDefinition("new_project", "new_note", {
780
+ cast: "StringAttributeMetadata", // returns the type's own properties, such as MaxLength
781
+ });
782
+ await metadataClient.updateAttributeDefinition("new_project", "new_note", {
783
+ ...note,
784
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
785
+ MaxLength: 500,
550
786
  });
551
787
  ```
552
788
 
789
+ Or let a `mergeAndUpdate*` method do that: it retrieves the current definition (with
790
+ `Consistency: Strong`, so not a cached copy that would undo a change made seconds before), applies
791
+ your changes and sends the result. Columns and relationships name their `@odata.type`, which the
792
+ retrieve casts to:
793
+
794
+ ```typescript
795
+ // Only the changed properties: the SDK fills in the rest from the current definition
796
+ await metadataClient.mergeAndUpdateEntityDefinition("new_project", {
797
+ DisplayName: { LocalizedLabels: [{ Label: "Updated Project", LanguageCode: 1033 }] },
798
+ Description: { LocalizedLabels: [{ Label: "Updated description", LanguageCode: 1033 }] },
799
+ });
800
+
801
+ await metadataClient.mergeAndUpdateAttributeDefinition("new_project", "new_note", {
802
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
803
+ DisplayName: { LocalizedLabels: [{ Label: "Comment", LanguageCode: 1033 }] },
804
+ });
805
+
806
+ await metadataClient.mergeAndUpdateRelationship("new_account_project", {
807
+ "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
808
+ IsValidForAdvancedFind: false,
809
+ });
810
+
811
+ await metadataClient.mergeAndUpdateGlobalOptionSet("new_colors", {
812
+ Description: { LocalizedLabels: [{ Label: "Paint colors", LanguageCode: 1033 }] },
813
+ });
814
+ ```
815
+
816
+ A `PUT` doesn't change a choice's options: use `insertOptionValue()`, `updateOptionValue()`,
817
+ `deleteOptionValue()` and `orderOptions()` for those. `mergeAndUpdateGlobalOptionSet()` leaves
818
+ the options out of the `PUT`.
819
+
553
820
  The SDK automatically sets the `MSCRM.MergeLabels: true` header to merge labels instead of replacing them, preserving labels for other languages.
554
821
 
555
822
  ### Strict Typing
@@ -557,10 +824,36 @@ The SDK automatically sets the `MSCRM.MergeLabels: true` header to merge labels
557
824
  The metadata package uses strict type separation:
558
825
 
559
826
  - **Read types** - include all properties returned from the server (e.g., `EntityMetadata`)
560
- - **Create types** - exclude read-only and internal properties (e.g., `EntityMetadataCreate`)
561
- - **Update types** - similar to Create but may have different required fields (e.g., `EntityMetadataUpdate`)
827
+ - **Create types** - exclude read-only and internal properties, and require what a create needs (e.g., `EntityMetadataCreate`, `StringAttributeMetadataCreate`)
828
+ - **Update types** - the complete definition with `@odata.type` required (e.g., `EntityMetadataUpdate`, `AttributeMetadataUpdateTypes`); a retrieved definition plus its `@odata.type` can be passed back
829
+ - **Changes types** - only the properties to change, for the `mergeAndUpdate*` methods (e.g., `EntityMetadataChanges`, `AttributeMetadataChanges`)
830
+
831
+ This ensures you cannot accidentally send read-only fields like `MetadataId`, `ObjectTypeCode`, or internal-use fields in create/update operations. The create types leave out every property Dataverse sets itself, such as a date column's `MinSupportedValue` and `MaxSupportedValue`, a text column's `DatabaseLength`, `AttributeType` (the type comes from `@odata.type`) and `IsValidForRead`.
832
+
833
+ Column creates take `AttributeMetadataCreateTypes`, a union discriminated on `@odata.type`
834
+ (`"Microsoft.Dynamics.CRM.StringAttributeMetadata"`, …), so the type-specific properties
835
+ (`MaxLength`, `MinValue`, …) are checked for the type you name. Column, relationship
836
+ and global choice creates require `@odata.type`; for a table it's optional. A lookup column isn't
837
+ one of them: it is created with its relationship, as the `Lookup` of
838
+ `createOneToManyRelationship()`.
839
+
840
+ A table create (`EntityMetadataCreate`) requires what Dataverse needs: `SchemaName`,
841
+ `DisplayName`, `DisplayCollectionName`, `OwnershipType`, `IsActivity`, `HasActivities`, `HasNotes`
842
+ and the primary name column. `Attributes` is a tuple whose first element is that column
843
+ (`PrimaryNameAttributeMetadataCreate`: a text column with `IsPrimaryName: true`); the other
844
+ columns follow and can't be a second primary name.
562
845
 
563
- This ensures you cannot accidentally send read-only fields like `MetadataId`, `ObjectTypeCode`, or internal-use fields in create/update operations.
846
+ The package exports the metadata types only: the definitions, their create and update payloads,
847
+ and the parameters and responses of the metadata operations `MetadataClient` calls. Other
848
+ operations' types are in the actions, functions and messages packages.
849
+
850
+ ### Enums Are Member Names
851
+
852
+ The Web API reads and writes enum members by name (`"UserOwned"`, `"ApplicationRequired"`); a
853
+ number such as `OwnershipType: 1` is rejected. The exported enums (`OwnershipTypes`,
854
+ `AttributeRequiredLevel`, `StringFormat`, …) are string-valued, so `OwnershipTypes.UserOwned` is
855
+ `"UserOwned"` and you can write either form. Read types use the same names, so
856
+ `entity.OwnershipType === OwnershipTypes.UserOwned` works on a retrieved definition.
564
857
 
565
858
  ---
566
859
 
@@ -584,17 +877,13 @@ const entityId = await metadataClient.createEntityDefinition({
584
877
 
585
878
  ---
586
879
 
587
- ## License
588
-
589
- This package is part of the `dataverse-sdk-typescript` monorepo and is licensed under **GNU AGPL v3.0**. See [LICENSE](../../LICENSE) for details.
590
-
591
- ### Publishing Changes
880
+ ## Publishing Changes
592
881
 
593
882
  Metadata changes must be published before they take effect in the application. Use PublishXml or PublishAllXml actions from the messages package.
594
883
 
595
884
  ## License
596
885
 
597
- GNU AGPL v3.0
886
+ This package is part of the `dataverse-sdk-typescript` monorepo and is licensed under **GNU AGPL v3.0**. See [LICENSE](../../LICENSE) for details.
598
887
 
599
888
  ## Related Packages
600
889