@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.
- package/dist/index.js +1 -1
- package/dist/{tool-cyfkvxhw.js → tool-4g47skgm.js} +1059 -373
- package/dist/tool.js +1 -1
- package/package.json +2 -2
- package/src/commands/directory.ts +21 -22
- package/src/commands/entities.spec.ts +513 -112
- package/src/commands/entities.ts +402 -160
- package/src/commands/roles.ts +8 -6
- package/src/utils/output.spec.ts +21 -1
- package/src/utils/output.ts +22 -1
- package/src/utils/validate.spec.ts +57 -62
- package/src/utils/validate.ts +12 -16
- package/tests/entities-lifecycle.e2e.test.ts +92 -17
- package/tests/files-management.e2e.test.ts +2 -2
- package/tests/records-management.e2e.test.ts +1 -1
package/src/commands/entities.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 '
|
|
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":[{"
|
|
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: {
|
|
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":[{"
|
|
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: {
|
|
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>).
|
|
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 '
|
|
438
|
-
'Example: {"
|
|
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
|
|
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
|
|
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
|
|
792
|
+
return failInvalidArgument(
|
|
559
793
|
"Update options must be a JSON object",
|
|
560
|
-
"Provide a JSON object with addFields, updateFields, removeFields, displayName, description, or
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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 (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
656
|
-
|
|
893
|
+
if (addFieldNames.length > 0 && removedFieldNames.length > 0) {
|
|
894
|
+
const removedNamesLower = new Set(
|
|
895
|
+
removedFieldNames.map((name) => name.toLowerCase()),
|
|
657
896
|
);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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 (
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
696
|
-
|
|
932
|
+
const validationErr = combineValidationIssues(validationIssues);
|
|
933
|
+
if (validationErr) {
|
|
934
|
+
return failInvalidArgument(
|
|
935
|
+
validationErr.message,
|
|
936
|
+
validationErr.instructions,
|
|
697
937
|
);
|
|
698
|
-
|
|
699
|
-
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let trimmedReason: string | undefined;
|
|
941
|
+
const hasRemove = removedFieldNames.length > 0;
|
|
700
942
|
|
|
701
|
-
|
|
702
|
-
|
|
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 (
|
|
705
|
-
|
|
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);
|