@uipath/data-fabric-tool 1.201.0-preview.115 → 1.201.0-preview.121

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.
@@ -20,7 +20,11 @@ import type {
20
20
  import { EntityFieldDataType } from "@uipath/uipath-typescript";
21
21
  import { type Command, Option } from "commander";
22
22
  import { readJsonInput } from "../utils/input";
23
- import { fail, requireDestructiveConfirmation } from "../utils/output";
23
+ import {
24
+ fail,
25
+ failInvalidArgument,
26
+ requireDestructiveConfirmation,
27
+ } from "../utils/output";
24
28
  import { connectOrFail } from "../utils/sdk-client";
25
29
  import {
26
30
  findInvalidFieldConstraints,
@@ -110,30 +114,29 @@ const ENTITIES_GET_EXAMPLES: CommandExample[] = [
110
114
  Output: {
111
115
  Code: "EntitySchema",
112
116
  Data: {
113
- id: "a1b2c3d4-0000-0000-0000-000000000001",
114
- name: "Invoice",
115
- displayName: "Invoice",
116
- entityType: "Standard",
117
- description: "Invoice records",
118
- isRbacEnabled: false,
119
- fields: [
117
+ Id: "a1b2c3d4-0000-0000-0000-000000000001",
118
+ Name: "Invoice",
119
+ DisplayName: "Invoice",
120
+ EntityType: "Standard",
121
+ Description: "Invoice records",
122
+ IsRbacEnabled: false,
123
+ Fields: [
120
124
  {
121
- id: "f1000000-0000-0000-0000-000000000002",
122
- name: "amount",
123
- displayName: "Amount",
124
- fieldDataType: { name: "DECIMAL" },
125
- sqlType: {
126
- name: "DECIMAL",
127
- decimalPrecision: 2,
128
- minValue: 0,
129
- maxValue: 999999,
125
+ Id: "f1000000-0000-0000-0000-000000000002",
126
+ Name: "amount",
127
+ DisplayName: "Amount",
128
+ FieldDataType: {
129
+ Name: "DECIMAL",
130
+ DecimalPrecision: 2,
131
+ MinValue: 0,
132
+ MaxValue: 999999,
130
133
  },
131
- isRequired: true,
132
- isUnique: false,
133
- isEncrypted: false,
134
- isRbacEnabled: false,
135
- isPrimaryKey: false,
136
- isSystemField: false,
134
+ IsRequired: true,
135
+ IsUnique: false,
136
+ IsEncrypted: false,
137
+ IsRbacEnabled: false,
138
+ IsPrimaryKey: false,
139
+ IsSystemField: false,
137
140
  },
138
141
  ],
139
142
  },
@@ -150,7 +153,7 @@ const ENTITIES_DELETE_EXAMPLES: CommandExample[] = [
150
153
  Output: {
151
154
  Code: "EntityDeleted",
152
155
  Data: {
153
- ID: "a1b2c3d4-0000-0000-0000-000000000001",
156
+ Id: "a1b2c3d4-0000-0000-0000-000000000001",
154
157
  Reason: "test entity cleanup",
155
158
  },
156
159
  },
@@ -164,13 +167,13 @@ const ENTITIES_CREATE_EXAMPLES: CommandExample[] = [
164
167
  "CHOICE_SET_SINGLE/CHOICE_SET_MULTIPLE require 'choiceSetId' (from 'df choice-sets list'). " +
165
168
  "RELATIONSHIP requires 'referenceEntityId' (UUID of the target entity, from 'df entities list') and 'referenceFieldId' (UUID of the field on the target entity, from 'df entities get <target-id>'). " +
166
169
  "Note: a RELATIONSHIP column on a record always stores the target record's UUID 'Id' (regardless of which 'referenceFieldId' configured the join) — see 'df records insert' for how to write the value. " +
167
- "FILE fields take only 'fieldName' and 'type' — the server auto-wires the internal attachment reference; populate them later with 'uip df files upload <entity-id> <record-id> <field-name> --file <path>'. " +
170
+ "FILE fields take only 'name' and 'type' — the server auto-wires the internal attachment reference; populate them later with 'uip df files upload <entity-id> <record-id> <field-name> --file <path>'. " +
168
171
  "MULTILINE_MAX holds very large text ('lengthLimit' = UTF-16 byte budget, 1-131072; default max ~65,536 chars). Not filterable/sortable; 'records list'/'query' return a size marker — full content via 'records get'. Tenant-gated by the 'MultilineMax' feature flag.",
169
172
  Command:
170
- 'uip df entities create Expense --body \'{"displayName":"Expense","fields":[{"fieldName":"category","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000001","isRequired":true},{"fieldName":"tags","type":"CHOICE_SET_MULTIPLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000002"},{"fieldName":"submitter","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","isRequired":true},{"fieldName":"receipt","type":"FILE"},{"fieldName":"notes","type":"MULTILINE_MAX"}]}\'',
173
+ 'uip df entities create Expense --body \'{"displayName":"Expense","fields":[{"name":"category","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000001","isRequired":true},{"name":"tags","type":"CHOICE_SET_MULTIPLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000002"},{"name":"submitter","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","isRequired":true},{"name":"receipt","type":"FILE"},{"name":"notes","type":"MULTILINE_MAX"}]}\'',
171
174
  Output: {
172
175
  Code: "EntityCreated",
173
- Data: { ID: "a1b2c3d4-0000-0000-0000-000000000004" },
176
+ Data: { Id: "a1b2c3d4-0000-0000-0000-000000000004" },
174
177
  },
175
178
  },
176
179
  {
@@ -181,10 +184,47 @@ const ENTITIES_CREATE_EXAMPLES: CommandExample[] = [
181
184
  "CHOICE_SET_* fields resolve scope from 'choiceSetId' and must not pass 'referenceFolderKey'. " +
182
185
  "Get target entity/field IDs from 'df entities list --folder-key <key>' and 'df entities get <id> --folder-key <key>'; get choice-set IDs from 'df choice-sets list --folder-key <key>' (omit '--folder-key' for tenant-level choice sets).",
183
186
  Command:
184
- 'uip df entities create OrderLine --folder-key f1000000-0000-0000-0000-000000000050 --body \'{"displayName":"Order Line","fields":[{"fieldName":"purchaseOrder","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","referenceFolderKey":"f1000000-0000-0000-0000-000000000060","isRequired":true},{"fieldName":"userType","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000077"}]}\'',
187
+ 'uip df entities create OrderLine --folder-key f1000000-0000-0000-0000-000000000050 --body \'{"displayName":"Order Line","fields":[{"name":"purchaseOrder","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","referenceFolderKey":"f1000000-0000-0000-0000-000000000060","isRequired":true},{"name":"userType","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000077"}]}\'',
185
188
  Output: {
186
189
  Code: "EntityCreated",
187
- Data: { ID: "a1b2c3d4-0000-0000-0000-000000000005" },
190
+ Data: { Id: "a1b2c3d4-0000-0000-0000-000000000005" },
191
+ },
192
+ },
193
+ ];
194
+
195
+ const ENTITIES_UPDATE_EXAMPLES: CommandExample[] = [
196
+ {
197
+ Description:
198
+ "Add a field. addFields uses the flat write shape: name + type, plus optional lengthLimit, minValue, maxValue, decimalPrecision, displayName, description, isRequired, isUnique, choiceSetId, referenceEntityId, referenceFieldId, and referenceFolderKey. A field object copied from 'df entities get' is accepted in addFields and normalized, but do not replay a full 'entities get' entity object as the update body.",
199
+ Command:
200
+ 'uip df entities update a1b2c3d4-0000-0000-0000-000000000001 --body \'{"addFields":[{"name":"note","type":"STRING","lengthLimit":200}]}\'',
201
+ Output: {
202
+ Code: "EntityUpdated",
203
+ Data: { Id: "a1b2c3d4-0000-0000-0000-000000000001" },
204
+ },
205
+ },
206
+ {
207
+ Description:
208
+ "Update field metadata. updateFields uses field IDs from 'df entities get <entity-id>' and accepts metadata such as displayName, description, isRequired, isRbacEnabled, isEncrypted, isHiddenField, defaultValue, and constraints. Do not paste a full copied field object here; pass 'id' plus only the flat write-shape properties being changed. It cannot change a field's type or isUnique setting.",
209
+ Command:
210
+ 'uip df entities update a1b2c3d4-0000-0000-0000-000000000001 --body \'{"updateFields":[{"id":"f1000000-0000-0000-0000-000000000002","displayName":"Amount (USD)","isRequired":true}]}\'',
211
+ Output: {
212
+ Code: "EntityUpdated",
213
+ Data: { Id: "a1b2c3d4-0000-0000-0000-000000000001" },
214
+ },
215
+ },
216
+ {
217
+ Description:
218
+ "Remove fields. removeFields uses field names, not field IDs, and requires --yes with --reason because the field values are deleted.",
219
+ Command:
220
+ 'uip df entities update a1b2c3d4-0000-0000-0000-000000000001 --body \'{"removeFields":[{"name":"oldColumn"}]}\' --yes --reason "quarterly cleanup"',
221
+ Output: {
222
+ Code: "EntityUpdated",
223
+ Data: {
224
+ Id: "a1b2c3d4-0000-0000-0000-000000000001",
225
+ RemovedFields: ["oldColumn"],
226
+ Reason: "quarterly cleanup",
227
+ },
188
228
  },
189
229
  },
190
230
  ];
@@ -194,6 +234,7 @@ const VALID_FIELD_TYPES_LIST = [...VALID_FIELD_TYPES].join(", ");
194
234
 
195
235
  function hasInvalidFieldType(fields: unknown[]): boolean {
196
236
  return fields.some((f) => {
237
+ if (typeof f !== "object" || f === null) return false;
197
238
  const field = f as Record<string, unknown>;
198
239
  return (
199
240
  field.type !== undefined &&
@@ -209,6 +250,7 @@ const KNOWN_UPDATE_KEYS = [
209
250
  "displayName",
210
251
  "description",
211
252
  "isRbacEnabled",
253
+ "isAnalyticsEnabled",
212
254
  ] as const;
213
255
 
214
256
  function pickKnownUpdateKeys(
@@ -221,6 +263,187 @@ function pickKnownUpdateKeys(
221
263
  return out as EntityUpdateByIdOptions;
222
264
  }
223
265
 
266
+ const ADD_FIELDS_ARRAY_INSTRUCTIONS =
267
+ 'Example: {"addFields":[{"name":"title","type":"STRING"}]}';
268
+ const UPDATE_FIELDS_ARRAY_INSTRUCTIONS =
269
+ 'Example: {"updateFields":[{"id":"<fieldId>","displayName":"New Name"}]}';
270
+ const REMOVE_FIELDS_ARRAY_INSTRUCTIONS =
271
+ 'Example: {"removeFields":[{"name":"oldField"}]}';
272
+ const ADD_FIELDS_FIELD_NAME_INSTRUCTIONS =
273
+ 'Use the flat write shape, for example {"name":"title","type":"STRING"}. A field copied from "df entities get" is also accepted inside addFields and normalized.';
274
+ const UPDATE_FIELDS_ID_INSTRUCTIONS =
275
+ 'Use "df entities get <entityId>" to find field IDs. Example: {"id":"<fieldId>","displayName":"Total Amount","isRequired":true}';
276
+ const REMOVE_FIELDS_FIELD_NAME_INSTRUCTIONS =
277
+ 'Example: {"removeFields":[{"name":"oldField"}]}';
278
+
279
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
280
+ return typeof value === "object" && value !== null && !Array.isArray(value);
281
+ }
282
+
283
+ function readKey(
284
+ record: Record<string, unknown>,
285
+ camelKey: string,
286
+ pascalKey: string,
287
+ ): unknown {
288
+ return record[camelKey] ?? record[pascalKey];
289
+ }
290
+
291
+ function readObjectKey(
292
+ record: Record<string, unknown>,
293
+ camelKey: string,
294
+ pascalKey: string,
295
+ ): Record<string, unknown> | undefined {
296
+ const value = readKey(record, camelKey, pascalKey);
297
+ return isObjectRecord(value) ? value : undefined;
298
+ }
299
+
300
+ function setIfDefined(
301
+ target: Record<string, unknown>,
302
+ key: string,
303
+ value: unknown,
304
+ ): void {
305
+ if (value !== undefined) target[key] = value;
306
+ }
307
+
308
+ function readId(value: unknown): unknown {
309
+ if (!isObjectRecord(value)) return undefined;
310
+ return value.id ?? value.Id ?? value.ID;
311
+ }
312
+
313
+ function readFieldTypeMetadata(
314
+ field: Record<string, unknown>,
315
+ ): Record<string, unknown> | undefined {
316
+ return readObjectKey(field, "fieldDataType", "FieldDataType");
317
+ }
318
+
319
+ function readConstraint(
320
+ field: Record<string, unknown>,
321
+ fieldTypeMetadata: Record<string, unknown> | undefined,
322
+ camelKey: string,
323
+ pascalKey: string,
324
+ ): unknown {
325
+ return (
326
+ readKey(field, camelKey, pascalKey) ??
327
+ (fieldTypeMetadata === undefined
328
+ ? undefined
329
+ : readKey(fieldTypeMetadata, camelKey, pascalKey))
330
+ );
331
+ }
332
+
333
+ function normalizeAddFieldShape(field: unknown): unknown {
334
+ if (!isObjectRecord(field)) return field;
335
+ if (field.name !== undefined || field.type !== undefined) return field;
336
+
337
+ const readName = readKey(field, "name", "Name");
338
+ const fieldTypeMetadata = readFieldTypeMetadata(field);
339
+ if (typeof readName !== "string" || fieldTypeMetadata === undefined) {
340
+ return field;
341
+ }
342
+
343
+ const normalized: Record<string, unknown> = { name: readName };
344
+ setIfDefined(
345
+ normalized,
346
+ "type",
347
+ readKey(fieldTypeMetadata, "name", "Name"),
348
+ );
349
+
350
+ for (const [writeKey, pascalKey] of [
351
+ ["displayName", "DisplayName"],
352
+ ["description", "Description"],
353
+ ["isRequired", "IsRequired"],
354
+ ["isUnique", "IsUnique"],
355
+ ["isRbacEnabled", "IsRbacEnabled"],
356
+ ["isEncrypted", "IsEncrypted"],
357
+ ["isHiddenField", "IsHiddenField"],
358
+ ["defaultValue", "DefaultValue"],
359
+ ] as const) {
360
+ setIfDefined(normalized, writeKey, readKey(field, writeKey, pascalKey));
361
+ }
362
+
363
+ for (const [writeKey, pascalKey] of [
364
+ ["lengthLimit", "LengthLimit"],
365
+ ["maxValue", "MaxValue"],
366
+ ["minValue", "MinValue"],
367
+ ["decimalPrecision", "DecimalPrecision"],
368
+ ] as const) {
369
+ setIfDefined(
370
+ normalized,
371
+ writeKey,
372
+ readConstraint(field, fieldTypeMetadata, writeKey, pascalKey),
373
+ );
374
+ }
375
+
376
+ setIfDefined(
377
+ normalized,
378
+ "choiceSetId",
379
+ readKey(field, "choiceSetId", "ChoiceSetId") ??
380
+ readId(readKey(field, "referenceChoiceSet", "ReferenceChoiceSet")),
381
+ );
382
+ setIfDefined(
383
+ normalized,
384
+ "referenceEntityId",
385
+ readKey(field, "referenceEntityId", "ReferenceEntityId") ??
386
+ readId(readKey(field, "referenceEntity", "ReferenceEntity")),
387
+ );
388
+ setIfDefined(
389
+ normalized,
390
+ "referenceFieldId",
391
+ readKey(field, "referenceFieldId", "ReferenceFieldId") ??
392
+ readId(readKey(field, "referenceField", "ReferenceField")),
393
+ );
394
+ setIfDefined(
395
+ normalized,
396
+ "referenceFolderKey",
397
+ readKey(field, "referenceFolderKey", "ReferenceFolderKey"),
398
+ );
399
+
400
+ return normalized;
401
+ }
402
+
403
+ function normalizeAddFields(addFields: unknown[]): unknown[] {
404
+ return addFields.map((field) => normalizeAddFieldShape(field));
405
+ }
406
+
407
+ function hasMissingStringField(fields: unknown[], key: string): boolean {
408
+ return fields.some((field) => {
409
+ if (!isObjectRecord(field)) return true;
410
+ const value = field[key];
411
+ return typeof value !== "string" || value.trim() === "";
412
+ });
413
+ }
414
+
415
+ function getUpdateArray(
416
+ input: Record<string, unknown>,
417
+ key: "addFields" | "updateFields" | "removeFields",
418
+ instructions: string,
419
+ issues: ValidationError[],
420
+ ): unknown[] | undefined {
421
+ const value = input[key];
422
+ if (value === undefined) return undefined;
423
+ if (Array.isArray(value)) return value;
424
+ issues.push({
425
+ message: `'${key}' must be an array`,
426
+ instructions,
427
+ });
428
+ return undefined;
429
+ }
430
+
431
+ function combineValidationIssues(
432
+ issues: ValidationError[],
433
+ ): ValidationError | null {
434
+ if (issues.length === 0) return null;
435
+ if (issues.length === 1) return issues[0];
436
+
437
+ return {
438
+ message: `Update body validation failed: ${issues
439
+ .map((issue) => issue.message)
440
+ .join("; ")}`,
441
+ instructions: issues
442
+ .map((issue, index) => `${index + 1}. ${issue.instructions}`)
443
+ .join("\n"),
444
+ };
445
+ }
446
+
224
447
  // Run every check that applies to a `fields` / `addFields` payload:
225
448
  // name rules and UI-broken-type substitution. The SDK-enum type check
226
449
  // stays inline at the call site because it needs `hasInvalidFieldType`
@@ -380,9 +603,12 @@ export const registerEntitiesCommand = (program: Command) => {
380
603
  )
381
604
  .option(
382
605
  "-f, --file <path>",
383
- "Path to JSON file with entity definition (fields array required; displayName, description, isRbacEnabled optional)",
606
+ "Path to JSON file with entity definition (fields array required; displayName, description, isRbacEnabled, isAnalyticsEnabled optional). A field object copied from 'df entities get' is also accepted and normalized to the write shape.",
607
+ )
608
+ .option(
609
+ "--body <json>",
610
+ "Inline JSON entity definition. A field object copied from 'df entities get' is also accepted and normalized to the write shape.",
384
611
  )
385
- .option("--body <json>", "Inline JSON entity definition")
386
612
  .option(
387
613
  "--folder-key <key>",
388
614
  "Folder key (GUID) of the target folder (for folder-scoped entities)",
@@ -413,7 +639,7 @@ export const registerEntitiesCommand = (program: Command) => {
413
639
  ) {
414
640
  return fail(
415
641
  "Entity definition must be a JSON object",
416
- "Provide a JSON object with a 'fields' array and optional displayName, description, isRbacEnabled.",
642
+ "Provide a JSON object with a 'fields' array and optional displayName, description, isRbacEnabled, isAnalyticsEnabled.",
417
643
  );
418
644
  }
419
645
 
@@ -425,17 +651,20 @@ export const registerEntitiesCommand = (program: Command) => {
425
651
  );
426
652
  }
427
653
 
654
+ definition.fields = normalizeAddFields(
655
+ definition.fields as unknown[],
656
+ );
657
+
428
658
  const hasInvalidField = (definition.fields as unknown[]).some(
429
659
  (f) =>
430
660
  typeof f !== "object" ||
431
661
  f === null ||
432
- typeof (f as Record<string, unknown>).fieldName !==
433
- "string",
662
+ typeof (f as Record<string, unknown>).name !== "string",
434
663
  );
435
664
  if (hasInvalidField) {
436
665
  return fail(
437
- "Each field must include a 'fieldName' string",
438
- 'Example: {"fieldName":"title","type":"STRING"}',
666
+ "Each field must include a 'name' string",
667
+ 'Example: {"name":"title","type":"STRING"}',
439
668
  );
440
669
  }
441
670
 
@@ -471,6 +700,10 @@ export const registerEntitiesCommand = (program: Command) => {
471
700
  ...(definition.isRbacEnabled !== undefined && {
472
701
  isRbacEnabled: definition.isRbacEnabled as boolean,
473
702
  }),
703
+ ...(definition.isAnalyticsEnabled !== undefined && {
704
+ isAnalyticsEnabled:
705
+ definition.isAnalyticsEnabled as boolean,
706
+ }),
474
707
  ...(options.folderKey !== undefined && {
475
708
  folderKey: options.folderKey,
476
709
  }),
@@ -511,11 +744,11 @@ export const registerEntitiesCommand = (program: Command) => {
511
744
  )
512
745
  .option(
513
746
  "-f, --file <path>",
514
- "Path to JSON file with update options (addFields, updateFields, removeFields, displayName, description, isRbacEnabled)",
747
+ "Path to JSON file with update options. Field shapes: addFields [{name,type,...}], updateFields [{id,...}], removeFields [{name}]. Do not paste a full copied field object into updateFields; pass id plus only changed flat properties. Also accepts the entity metadata keys displayName, description, isRbacEnabled and isAnalyticsEnabled.",
515
748
  )
516
749
  .option(
517
750
  "--body <json>",
518
- "Inline JSON update options (use `-` to read from stdin)",
751
+ "Inline JSON update options (use `-` to read from stdin). Field shapes: addFields [{name,type,...}], updateFields [{id,...}], removeFields [{name}]. Do not paste a full copied field object into updateFields; pass id plus only changed flat properties. Also accepts the entity metadata keys displayName, description, isRbacEnabled and isAnalyticsEnabled.",
519
752
  )
520
753
  .option(
521
754
  "-y, --yes",
@@ -532,6 +765,7 @@ export const registerEntitiesCommand = (program: Command) => {
532
765
  "--folder-key <key>",
533
766
  "Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
534
767
  )
768
+ .examples(ENTITIES_UPDATE_EXAMPLES)
535
769
  .trackedAction(
536
770
  processContext,
537
771
  async (id: string, options: UpdateEntityOptions) => {
@@ -544,7 +778,7 @@ export const registerEntitiesCommand = (program: Command) => {
544
778
  );
545
779
 
546
780
  if (parseError) {
547
- return fail(
781
+ return failInvalidArgument(
548
782
  "Error parsing update options",
549
783
  parseError.message,
550
784
  );
@@ -555,157 +789,165 @@ export const registerEntitiesCommand = (program: Command) => {
555
789
  parsed === null ||
556
790
  Array.isArray(parsed)
557
791
  ) {
558
- return fail(
792
+ return failInvalidArgument(
559
793
  "Update options must be a JSON object",
560
- "Provide a JSON object with addFields, updateFields, removeFields, displayName, description, or isRbacEnabled.",
794
+ "Provide a JSON object with addFields, updateFields, removeFields, displayName, description, isRbacEnabled, or isAnalyticsEnabled.",
561
795
  );
562
796
  }
563
797
 
564
798
  const input = parsed as Record<string, unknown>;
565
- const metadataErr = validateSchemaMetadata(input, "Entity");
566
- if (metadataErr)
567
- return fail(metadataErr.message, metadataErr.instructions);
568
-
569
- if (
570
- input.addFields !== undefined &&
571
- !Array.isArray(input.addFields)
572
- ) {
573
- return fail(
574
- "'addFields' must be an array",
575
- 'Example: {"addFields":[{"fieldName":"title","type":"STRING"}]}',
576
- );
799
+ if (Array.isArray(input.addFields)) {
800
+ input.addFields = normalizeAddFields(input.addFields);
577
801
  }
578
802
 
579
- if (
580
- input.updateFields !== undefined &&
581
- !Array.isArray(input.updateFields)
582
- ) {
583
- return fail(
584
- "'updateFields' must be an array",
585
- 'Example: {"updateFields":[{"id":"<fieldId>","displayName":"New Name"}]}',
586
- );
587
- }
803
+ const validationIssues: ValidationError[] = [];
588
804
 
589
- if (
590
- input.removeFields !== undefined &&
591
- !Array.isArray(input.removeFields)
592
- ) {
593
- return fail(
594
- "'removeFields' must be an array",
595
- 'Example: {"removeFields":[{"fieldName":"oldField"}]}',
596
- );
805
+ if (input.name !== undefined) {
806
+ validationIssues.push({
807
+ message: "Entity name cannot be changed after creation",
808
+ instructions:
809
+ "Remove 'name' from the update body. The entity name is immutable — only 'displayName' and 'description' can be changed. To use a different name, create a new entity.",
810
+ });
597
811
  }
598
812
 
599
- let trimmedReason: string | undefined;
600
- let removedFieldNames: string[] = [];
601
- const hasRemove =
602
- Array.isArray(input.removeFields) &&
603
- input.removeFields.length > 0;
604
-
605
- if (hasRemove) {
606
- const removeFields = input.removeFields as unknown[];
607
- const hasInvalidRemove = removeFields.some((f) => {
608
- if (typeof f !== "object" || f === null) return true;
609
- const fn = (f as Record<string, unknown>).fieldName;
610
- return typeof fn !== "string" || fn.trim() === "";
813
+ const hasMetadataUpdate = [
814
+ "description",
815
+ "isRbacEnabled",
816
+ "isAnalyticsEnabled",
817
+ ].some((key) => input[key] !== undefined);
818
+ if (hasMetadataUpdate && input.displayName === undefined) {
819
+ validationIssues.push({
820
+ message:
821
+ "'displayName' is required when updating entity metadata",
822
+ instructions:
823
+ 'Include the current displayName in the body alongside description/isRbacEnabled/isAnalyticsEnabled — get it with \'df entities get <entityId>\'. Example: {"displayName":"<current-display-name>","description":"<new-description>"}.',
611
824
  });
612
- if (hasInvalidRemove) {
613
- return fail(
614
- "Each field in removeFields must include a non-empty 'fieldName' string",
615
- 'Example: {"removeFields":[{"fieldName":"oldField"}]}',
616
- );
617
- }
825
+ }
618
826
 
619
- const reason = requireDestructiveConfirmation(
620
- options,
621
- `remove fields from entity '${id}'`,
622
- 'Pass --reason "<text>" to record why fields are being removed.',
623
- );
624
- if (reason === null) return;
625
- trimmedReason = reason;
827
+ const metadataErr = validateSchemaMetadata(input, "Entity");
828
+ if (metadataErr) validationIssues.push(metadataErr);
626
829
 
627
- removedFieldNames = (
628
- removeFields as { fieldName: string }[]
629
- ).map((f) => f.fieldName);
630
- }
830
+ const addFields = getUpdateArray(
831
+ input,
832
+ "addFields",
833
+ ADD_FIELDS_ARRAY_INSTRUCTIONS,
834
+ validationIssues,
835
+ );
836
+ const updateFields = getUpdateArray(
837
+ input,
838
+ "updateFields",
839
+ UPDATE_FIELDS_ARRAY_INSTRUCTIONS,
840
+ validationIssues,
841
+ );
842
+ const removeFields = getUpdateArray(
843
+ input,
844
+ "removeFields",
845
+ REMOVE_FIELDS_ARRAY_INSTRUCTIONS,
846
+ validationIssues,
847
+ );
631
848
 
632
- if (Array.isArray(input.addFields)) {
633
- const hasInvalidField = (input.addFields as unknown[]).some(
634
- (f) => {
635
- if (typeof f !== "object" || f === null)
636
- return true;
637
- const fn = (f as Record<string, unknown>).fieldName;
638
- return typeof fn !== "string" || fn.trim() === "";
639
- },
849
+ let addFieldNames: string[] = [];
850
+ if (addFields !== undefined) {
851
+ const hasInvalidAddFieldsShape = hasMissingStringField(
852
+ addFields,
853
+ "name",
640
854
  );
641
- if (hasInvalidField) {
642
- return fail(
643
- "Each field in addFields must include a non-empty 'fieldName' string",
644
- 'Example: {"fieldName":"title","type":"STRING"}',
855
+ if (hasInvalidAddFieldsShape) {
856
+ validationIssues.push({
857
+ message:
858
+ "Each field in addFields must include a non-empty 'name' string",
859
+ instructions: ADD_FIELDS_FIELD_NAME_INSTRUCTIONS,
860
+ });
861
+ } else {
862
+ addFieldNames = (addFields as { name: string }[]).map(
863
+ (field) => field.name,
645
864
  );
865
+
866
+ if (hasInvalidFieldType(addFields)) {
867
+ validationIssues.push({
868
+ message: "Invalid field type in addFields",
869
+ instructions: `Valid types: ${VALID_FIELD_TYPES_LIST}`,
870
+ });
871
+ }
872
+
873
+ const addErr = validateFieldsPayload(addFields);
874
+ if (addErr) validationIssues.push(addErr);
646
875
  }
876
+ }
647
877
 
648
- if (hasInvalidFieldType(input.addFields as unknown[])) {
649
- return fail(
650
- "Invalid field type in addFields",
651
- `Valid types: ${VALID_FIELD_TYPES_LIST}`,
652
- );
878
+ let removedFieldNames: string[] = [];
879
+ if (removeFields !== undefined) {
880
+ if (hasMissingStringField(removeFields, "name")) {
881
+ validationIssues.push({
882
+ message:
883
+ "Each field in removeFields must include a non-empty 'name' string",
884
+ instructions: REMOVE_FIELDS_FIELD_NAME_INSTRUCTIONS,
885
+ });
886
+ } else {
887
+ removedFieldNames = (
888
+ removeFields as { name: string }[]
889
+ ).map((field) => field.name);
653
890
  }
891
+ }
654
892
 
655
- const addErr = validateFieldsPayload(
656
- input.addFields as unknown[],
893
+ if (addFieldNames.length > 0 && removedFieldNames.length > 0) {
894
+ const removedNamesLower = new Set(
895
+ removedFieldNames.map((name) => name.toLowerCase()),
657
896
  );
658
- if (addErr)
659
- return fail(addErr.message, addErr.instructions);
660
-
661
- const addNames = (
662
- input.addFields as { fieldName: string }[]
663
- ).map((f) => f.fieldName);
664
- if (removedFieldNames.length > 0) {
665
- const removedNamesLower = new Set(
666
- removedFieldNames.map((name) => name.toLowerCase()),
667
- );
668
- const conflict = addNames.find((name) =>
669
- removedNamesLower.has(name.toLowerCase()),
670
- );
671
- if (conflict !== undefined) {
672
- return fail(
673
- `Field '${conflict}' appears in both addFields and removeFields`,
897
+ const conflict = addFieldNames.find((name) =>
898
+ removedNamesLower.has(name.toLowerCase()),
899
+ );
900
+ if (conflict !== undefined) {
901
+ validationIssues.push({
902
+ message: `Field '${conflict}' appears in both addFields and removeFields`,
903
+ instructions:
674
904
  "A single update cannot add and remove the same field. Split into two calls if you need to recreate it.",
675
- );
676
- }
905
+ });
677
906
  }
678
907
  }
679
908
 
680
- if (Array.isArray(input.updateFields)) {
681
- const hasInvalidField = (
682
- input.updateFields as unknown[]
683
- ).some((f) => {
684
- if (typeof f !== "object" || f === null) return true;
685
- const fid = (f as Record<string, unknown>).id;
686
- return typeof fid !== "string" || fid.trim() === "";
687
- });
688
- if (hasInvalidField) {
689
- return fail(
690
- "Each field in updateFields must include a non-empty 'id' string",
691
- 'Use \'df entities get <entityId>\' to find field IDs. Example: {"id":"<fieldId>","displayName":"Total Amount","isRequired":true}',
692
- );
909
+ if (updateFields !== undefined) {
910
+ const hasInvalidUpdateFieldsShape = hasMissingStringField(
911
+ updateFields,
912
+ "id",
913
+ );
914
+ if (hasInvalidUpdateFieldsShape) {
915
+ validationIssues.push({
916
+ message:
917
+ "Each field in updateFields must include a non-empty 'id' string",
918
+ instructions: UPDATE_FIELDS_ID_INSTRUCTIONS,
919
+ });
920
+ } else {
921
+ const updateErr =
922
+ findIsUniqueInUpdateFields(updateFields);
923
+ if (updateErr) validationIssues.push(updateErr);
924
+
925
+ const updateMetadataErr =
926
+ findInvalidFieldMetadata(updateFields);
927
+ if (updateMetadataErr)
928
+ validationIssues.push(updateMetadataErr);
693
929
  }
930
+ }
694
931
 
695
- const updateErr = findIsUniqueInUpdateFields(
696
- input.updateFields as unknown[],
932
+ const validationErr = combineValidationIssues(validationIssues);
933
+ if (validationErr) {
934
+ return failInvalidArgument(
935
+ validationErr.message,
936
+ validationErr.instructions,
697
937
  );
698
- if (updateErr)
699
- return fail(updateErr.message, updateErr.instructions);
938
+ }
939
+
940
+ let trimmedReason: string | undefined;
941
+ const hasRemove = removedFieldNames.length > 0;
700
942
 
701
- const updateMetadataErr = findInvalidFieldMetadata(
702
- input.updateFields as unknown[],
943
+ if (hasRemove) {
944
+ const reason = requireDestructiveConfirmation(
945
+ options,
946
+ `remove fields from entity '${id}'`,
947
+ 'Pass --reason "<text>" to record why fields are being removed.',
703
948
  );
704
- if (updateMetadataErr)
705
- return fail(
706
- updateMetadataErr.message,
707
- updateMetadataErr.instructions,
708
- );
949
+ if (reason === null) return;
950
+ trimmedReason = reason;
709
951
  }
710
952
 
711
953
  const sdk = await connectOrFail(options.tenant);