@contrail/cli 3.6.0-alpha-8-alpha.6d0c626 → 3.6.0-alpha-8-alpha.e4ae6bd

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
@@ -17,7 +17,7 @@ $ npm install -g @contrail/cli
17
17
  $ contrail COMMAND
18
18
  running command...
19
19
  $ contrail (--version)
20
- @contrail/cli/3.6.0-alpha-8-alpha.6d0c626 linux-x64 node-v24.20.0
20
+ @contrail/cli/3.6.0-alpha-8-alpha.e4ae6bd linux-x64 node-v24.21.0
21
21
  $ contrail --help [COMMAND]
22
22
  USAGE
23
23
  $ contrail COMMAND
@@ -52,9 +52,9 @@ To get instructions on how to set up autocomplete for your shell, run `contrail
52
52
  * [`contrail config set`](#contrail-config-set)
53
53
  * [`contrail config use [NAME]`](#contrail-config-use-name)
54
54
  * [`contrail entities create PATH`](#contrail-entities-create-path)
55
- * [`contrail entities delete ENTITYNAME`](#contrail-entities-delete-entityname)
56
- * [`contrail entities get TYPEPATH`](#contrail-entities-get-typepath)
57
- * [`contrail entities update ENTITYNAME [ID]`](#contrail-entities-update-entityname-id)
55
+ * [`contrail entities delete ENTITYNAME [IDS]`](#contrail-entities-delete-entityname-ids)
56
+ * [`contrail entities get TYPEPATH [IDS]`](#contrail-entities-get-typepath-ids)
57
+ * [`contrail entities update ENTITYNAME [IDS]`](#contrail-entities-update-entityname-ids)
58
58
  * [`contrail file upload FILELOCATION CONTENTTYPE`](#contrail-file-upload-filelocation-contenttype)
59
59
  * [`contrail groups export`](#contrail-groups-export)
60
60
  * [`contrail groups import GROUPSFILE`](#contrail-groups-import-groupsfile)
@@ -548,12 +548,13 @@ EXAMPLES
548
548
  ```
549
549
  USAGE
550
550
  $ contrail entities create PATH [-l error|warn|info|debug] [-e true|false] [-n <value>] [-d] [-i <value>] [-o
551
- <value>] [-g] [-r] [-s]
551
+ <value>] [-g] [-r] [-S]
552
552
 
553
553
  ARGUMENTS
554
554
  PATH Path of the entity type to create
555
555
 
556
556
  FLAGS
557
+ -S, --skipHydrateRelationships If true, does not hydrate relationships for the created entities
557
558
  -d, --dryRun If true, does not create the entity but simulates the creation
558
559
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
559
560
  <options: true|false>
@@ -567,7 +568,6 @@ FLAGS
567
568
  -n, --count=<value> Number of random entities to create
568
569
  -o, --outputFile=<value> File to save the output (optional)
569
570
  -r, --readableIds If true, uses readable IDs for the created entities
570
- -s, --skipHydrateRelationships If true, does not hydrate relationships for the created entities
571
571
 
572
572
  DESCRIPTION
573
573
  (BETA) Create entities
@@ -592,46 +592,50 @@ EXAMPLES
592
592
  $ contrail entities create color -o /tmp/colors.json -- name=Blue green=1 red=2 blue=3 # creates a color and saves the output to /tmp/colors.json
593
593
  ```
594
594
 
595
- ## `contrail entities delete ENTITYNAME`
595
+ ## `contrail entities delete ENTITYNAME [IDS]`
596
596
 
597
- (BETA) Delete an entity
597
+ (BETA) Delete entities by id
598
598
 
599
599
  ```
600
600
  USAGE
601
- $ contrail entities delete ENTITYNAME [-l error|warn|info|debug] [-e true|false] [-d] [-i <value>] [--id <value>]
601
+ $ contrail entities delete ENTITYNAME [IDS] [-l error|warn|info|debug] [-e true|false] [-d] [-i <value>]
602
602
 
603
603
  ARGUMENTS
604
604
  ENTITYNAME The name of the entity type to delete
605
+ IDS One or more entity ids to delete
605
606
 
606
607
  FLAGS
607
- -d, --dryRun If true, does not delete the entity but simulates the deletion
608
+ -d, --dryRun If true, does not delete the entities but simulates the deletion
608
609
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
609
610
  <options: true|false>
610
- -i, --input=<value> A JSON array of objects where each object is one entity to create. Cannot be used
611
- with --count. Unless --precise is specified, any missing values will be populated
612
- with random values.
611
+ -i, --inputFile=<value> A file with a JSON array of objects; only each object's id is used. Deletes every
612
+ entity in the file.
613
613
  -l, --logLevel=<option> The Log Level
614
614
  <options: error|warn|info|debug>
615
- --id=<value> The unique identifier of the entity
616
615
 
617
616
  DESCRIPTION
618
- (BETA) Delete an entity
617
+ (BETA) Delete entities by id
619
618
 
620
619
  EXAMPLES
621
620
  $ contrail entities delete color 123
621
+
622
+ $ contrail entities delete color id1 id2 id3
623
+
624
+ $ contrail entities delete item -i ids.json
622
625
  ```
623
626
 
624
- ## `contrail entities get TYPEPATH`
627
+ ## `contrail entities get TYPEPATH [IDS]`
625
628
 
626
629
  (BETA) Get an entity
627
630
 
628
631
  ```
629
632
  USAGE
630
- $ contrail entities get TYPEPATH [-l error|warn|info|debug] [-e true|false] [-o <value>] [-p <value>] [-P] [-s
631
- <value>] [-S] [-t <value>] [-r <value>]
633
+ $ contrail entities get TYPEPATH [IDS] [-l error|warn|info|debug] [-e true|false] [-o <value>] [-p <value>] [-P]
634
+ [-s <value>] [-S] [-t <value>] [-r <value>]
632
635
 
633
636
  ARGUMENTS
634
637
  TYPEPATH The name of the entity type to fetch
638
+ IDS One or more entity ids; shorthand for the id/ids criteria
635
639
 
636
640
  FLAGS
637
641
  -P, --paginated If true, fetches all entities in a paginated manner
@@ -650,6 +654,10 @@ DESCRIPTION
650
654
  (BETA) Get an entity
651
655
 
652
656
  EXAMPLES
657
+ $ contrail entities get color 123
658
+
659
+ $ contrail entities get color id1 id2 id3
660
+
653
661
  $ contrail entities get color -- id=123
654
662
 
655
663
  $ contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
@@ -659,27 +667,27 @@ EXAMPLES
659
667
  $ contrail entities get events -- timestamp=..2025-09-16T23:59:59Z
660
668
  ```
661
669
 
662
- ## `contrail entities update ENTITYNAME [ID]`
670
+ ## `contrail entities update ENTITYNAME [IDS]`
663
671
 
664
672
  (BETA) Update an entity
665
673
 
666
674
  ```
667
675
  USAGE
668
- $ contrail entities update ENTITYNAME [ID] [-l error|warn|info|debug] [-e true|false] [-s] [-d] [-i <value>] [-F]
676
+ $ contrail entities update ENTITYNAME [IDS] [-l error|warn|info|debug] [-e true|false] [-S] [-d] [-i <value>] [-F]
669
677
 
670
678
  ARGUMENTS
671
679
  ENTITYNAME The name of the entity type to update
672
- ID The unique identifier of the entity
680
+ IDS One or more entity ids to update
673
681
 
674
682
  FLAGS
675
683
  -F, --force Force update without validation. Internal use only
684
+ -S, --skipHydrateRelationships Skip hydrating relationships when updating the entity
676
685
  -d, --dryRun If true, does not create the entity but simulates the creation
677
686
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
678
687
  <options: true|false>
679
688
  -i, --inputFile=<value> File containing properties to update the entity (optional)
680
689
  -l, --logLevel=<option> The Log Level
681
690
  <options: error|warn|info|debug>
682
- -s, --skipHydrateRelationships Skip hydrating relationships when updating the entity
683
691
 
684
692
  DESCRIPTION
685
693
  (BETA) Update an entity
@@ -687,6 +695,8 @@ DESCRIPTION
687
695
  EXAMPLES
688
696
  $ contrail entities update color 123 -- name=obsidian hex=000000
689
697
 
698
+ $ contrail entities update item id1 id2 id3 -- status=archived # apply the same change to several entities
699
+
690
700
  $ contrail entities update color 123 -- description= notes='""' # Unset description, set notes to empty string
691
701
  ```
692
702
 
@@ -1,4 +1,4 @@
1
- import { PropertyLevel } from '@contrail/types';
1
+ import { PropertyLevel, PropertyType } from '@contrail/types';
2
2
  import { ColonDelimited, TypeProperty as ExtendedTypeProperty, TypeReferencesCacheByPath } from '../../api/types';
3
3
  import { TypeWithProperties } from '../../api/types';
4
4
  import { BaseCommand } from '../../basecommand';
@@ -23,6 +23,14 @@ export default class CreateEntity extends BaseCommand {
23
23
  static strict: boolean;
24
24
  run(): Promise<void>;
25
25
  }
26
+ export type CreateSummary = {
27
+ created: any[];
28
+ failed: {
29
+ input: any;
30
+ error: string;
31
+ }[];
32
+ };
33
+ export declare function summarizeSettledCreates(inputs: any[], settled: PromiseSettledResult<any>[]): CreateSummary;
26
34
  export type WithId = {
27
35
  id: string;
28
36
  [key: string]: any;
@@ -30,6 +38,7 @@ export type WithId = {
30
38
  export type ObjectReferenceCache = Map<string, WithId>;
31
39
  type ObjectReferenceCachesByTypePath = Map<string, ObjectReferenceCache>;
32
40
  export declare function pickRandomValueForProperty(property: ExtendedTypeProperty, objectReferencesCache: ObjectReferenceCachesByTypePath, typeReferencesCache: TypeReferencesCacheByPath): number | string | string[] | boolean | null;
41
+ export declare function castValueToPropertyType(value: string | number | string[] | number[], propertyType: PropertyType): any;
33
42
  export declare function getReferencedObjectPaths(type: TypeWithProperties): ColonDelimited[];
34
43
  export declare function buildObjectReferenceCache(entities: any[]): ObjectReferenceCache;
35
44
  export declare function selectRandomObject(cache: ObjectReferenceCache): WithId;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.doesPropertyLevelMatchRole = exports.selectRandomObject = exports.buildObjectReferenceCache = exports.getReferencedObjectPaths = exports.pickRandomValueForProperty = void 0;
3
+ exports.doesPropertyLevelMatchRole = exports.selectRandomObject = exports.buildObjectReferenceCache = exports.getReferencedObjectPaths = exports.castValueToPropertyType = exports.pickRandomValueForProperty = exports.summarizeSettledCreates = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_1 = tslib_1.__importDefault(require("fs"));
6
6
  const unique_names_generator_1 = require("unique-names-generator");
@@ -10,6 +10,7 @@ const types_2 = require("../../api/types");
10
10
  const core_1 = require("@oclif/core");
11
11
  const types_3 = require("../../api/types");
12
12
  const login_1 = require("../../auth/login");
13
+ const common_1 = require("../../common");
13
14
  const basecommand_1 = require("../../basecommand");
14
15
  const sdk_1 = require("@contrail/sdk");
15
16
  const entities_1 = require("../../helpers/entities");
@@ -26,7 +27,7 @@ const baseValidationMap = {
26
27
  itemId: async (_slug, itemId) => (0, entities_1.throwIfInvalidItemId)(itemId),
27
28
  projectId: async (_slug, projectId) => (0, entities_1.throwIfInvalidProjectId)(projectId),
28
29
  };
29
- const throwIfInvalidPropertyLevelFn = async (_slug, maybeRoles) => maybeRoles.forEach((maybeRole) => (0, entities_1.throwIfInvalidItemLevel)(maybeRole));
30
+ const throwIfInvalidPropertyLevelFn = async (_slug, maybeRoles) => [].concat(maybeRoles).forEach((maybeRole) => (0, entities_1.throwIfInvalidItemLevel)(maybeRole));
30
31
  const throwIfInvalidUserOrgRoleFn = async (_slug, maybeRole) => {
31
32
  if (maybeRole !== 'MEMBER' && maybeRole !== 'ADMIN') {
32
33
  throw new Error(`${maybeRole} is not a valid User Org Role`); // TODO: get these roles from a shared library
@@ -194,7 +195,7 @@ class CreateEntity extends basecommand_1.BaseCommand {
194
195
  default: false,
195
196
  }),
196
197
  skipHydrateRelationships: core_1.Flags.boolean({
197
- char: 's',
198
+ char: 'S',
198
199
  description: 'If true, does not hydrate relationships for the created entities',
199
200
  required: false,
200
201
  default: false,
@@ -209,7 +210,8 @@ class CreateEntity extends basecommand_1.BaseCommand {
209
210
  };
210
211
  static strict = false;
211
212
  async run() {
212
- this.log('🚨 This command is in BETA and may change or be removed at any time.');
213
+ (0, common_1.routeConsoleLogToStderr)();
214
+ console.error('🚨 This command is in BETA and may change or be removed at any time.');
213
215
  await (0, login_1.authUser)();
214
216
  const { flags, argv, args } = await this.parse(CreateEntity);
215
217
  const { count, dryRun: isDryRun, inputFile: inputFilename, outputFile: outputFilename, generateValues: shouldGenerateMissingValues, readableIds: shouldUseReadableIds, skipHydrateRelationships, } = flags;
@@ -221,10 +223,11 @@ class CreateEntity extends basecommand_1.BaseCommand {
221
223
  SHOULD_USE_READABLE_IDS = shouldUseReadableIds;
222
224
  SHOULD_SKIP_HYDRATE_RELATIONSHIPS = skipHydrateRelationships;
223
225
  SHOULD_GENERATE_MISSING_VALUES = shouldGenerateMissingValues;
224
- const propertySpecificationArgs = argv
225
- .slice(1, argv.length)
226
- .filter(propertySpecifications_1.isValidPropertySpecification);
227
- const propertySpecBySlug = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(propertySpecificationArgs);
226
+ const { ids: strayTokens, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
227
+ if (strayTokens.length > 0) {
228
+ this.error(`create takes no positional ids (got: ${strayTokens.join(' ')}); did you forget -- before property values?`);
229
+ }
230
+ const propertySpecBySlug = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
228
231
  // Validate args
229
232
  if (!entityTypePath) {
230
233
  this.error('You must specify an entity type path.');
@@ -257,13 +260,17 @@ class CreateEntity extends basecommand_1.BaseCommand {
257
260
  }
258
261
  try {
259
262
  const entitiesToCreate = await buildEntitiesToCreate(entityTypePath, partialEntities, propertySpecBySlug);
260
- const createdEntities = await createEntities(entityName, entitiesToCreate);
263
+ const summary = await createEntities(entityName, entitiesToCreate);
261
264
  if (outputFilename) {
262
- fs_1.default.writeFileSync(outputFilename, JSON.stringify(createdEntities, null, 2));
263
- this.log(`Output saved to ${outputFilename}`);
265
+ fs_1.default.writeFileSync(outputFilename, JSON.stringify(summary, null, 2));
266
+ console.error(`Output saved to ${outputFilename}`);
264
267
  }
265
268
  else {
266
- console.log(`Created ${entityTypePath}(s), Entity: ${JSON.stringify(createdEntities, null, 2)}`);
269
+ this.log(JSON.stringify(summary, null, 2));
270
+ }
271
+ if (summary.failed.length > 0) {
272
+ console.error(`${summary.failed.length} of ${entitiesToCreate.length} entities failed.`);
273
+ process.exitCode = 1;
267
274
  }
268
275
  }
269
276
  catch (error) {
@@ -272,15 +279,30 @@ class CreateEntity extends basecommand_1.BaseCommand {
272
279
  }
273
280
  }
274
281
  exports.default = CreateEntity;
282
+ // Every row is attempted; a failed row never stops the run. The summary pairs
283
+ // each failed input with its error so the failed rows can be retried as-is.
284
+ function summarizeSettledCreates(inputs, settled) {
285
+ const created = [];
286
+ const failed = [];
287
+ settled.forEach((result, index) => {
288
+ if (result.status === 'fulfilled') {
289
+ created.push(result.value);
290
+ }
291
+ else {
292
+ failed.push({ input: inputs[index], error: (0, common_1.sdkErrorMessage)(result.reason) });
293
+ }
294
+ });
295
+ return { created, failed };
296
+ }
297
+ exports.summarizeSettledCreates = summarizeSettledCreates;
275
298
  async function createEntities(entityName, entities) {
276
- const limit = (0, p_limit_1.default)(20);
277
- const createdEntities = IS_DRY_RUN
278
- ? entities
279
- : await Promise.allSettled(entities.map(async (e) => limit(() => createEntity(entityName, e))));
280
299
  if (IS_DRY_RUN) {
281
- console.log(`Dry run mode: not creating entities, but simulating creation for ${entityName}`);
300
+ console.error(`Dry run mode: not creating entities, but simulating creation for ${entityName}`);
301
+ return { created: entities, failed: [] };
282
302
  }
283
- return createdEntities;
303
+ const limit = (0, p_limit_1.default)(20);
304
+ const settled = await Promise.allSettled(entities.map(async (e) => limit(() => createEntity(entityName, e))));
305
+ return summarizeSettledCreates(entities, settled);
284
306
  }
285
307
  async function buildEntitiesToCreate(entityTypePath, entities, propertySpecificationBySlug) {
286
308
  let type = await (0, types_3.getTypeByPath)(entityTypePath);
@@ -303,7 +325,7 @@ async function buildEntitiesToCreate(entityTypePath, entities, propertySpecifica
303
325
  await Promise.all(populatedEntities.map(async () => limit(() => throwIfEntityIsInvalid)));
304
326
  // Skip system value generation unless -g flag is set (generateValues mode)
305
327
  if (!SHOULD_GENERATE_MISSING_VALUES) {
306
- console.log('Using only specified properties. Use --generate-values to auto-generate missing values.');
328
+ console.error('Using only specified properties. Use --generate-values to auto-generate missing values.');
307
329
  return populatedEntities;
308
330
  }
309
331
  // Add system values if needed
@@ -312,13 +334,13 @@ async function buildEntitiesToCreate(entityTypePath, entities, propertySpecifica
312
334
  for (const { slug, findValidValuesFn, applyValueToEntityFn } of valueFinderApplierForEntity) {
313
335
  // If the property is already specified in command-line args, skip it
314
336
  if (propertySpecificationBySlug?.has(slug)) {
315
- console.log(`Skipping property ${slug} because it is already specified.`);
337
+ console.error(`Skipping property ${slug} because it is already specified.`);
316
338
  continue;
317
339
  }
318
340
  // If all entities already have this property, skip finding values
319
341
  const allEntitiesHaveProperty = populatedEntities.every((entity) => entity[slug] !== undefined && entity[slug] !== null);
320
342
  if (allEntitiesHaveProperty) {
321
- console.log(`Skipping property ${slug} because all entities already have this value.`);
343
+ console.error(`Skipping property ${slug} because all entities already have this value.`);
322
344
  continue;
323
345
  }
324
346
  const values = await findValidValuesFn(populatedEntities, entities.length);
@@ -332,7 +354,8 @@ async function buildEntitiesToCreate(entityTypePath, entities, propertySpecifica
332
354
  return populatedEntities;
333
355
  }
334
356
  function createEntityWithPopulatedValues(type, typeReferencesCache, objectReferencesCache, propertySpecificationBySlug) {
335
- const roles = propertySpecificationBySlug?.get('roles');
357
+ const rolesValue = propertySpecificationBySlug?.get('roles');
358
+ const roles = rolesValue === undefined ? undefined : [].concat(rolesValue);
336
359
  const entity = SHOULD_GENERATE_MISSING_VALUES
337
360
  ? pickRandomPropertyValues(type, objectReferencesCache, typeReferencesCache, roles)
338
361
  : {};
@@ -348,6 +371,16 @@ function createEntityWithPopulatedValues(type, typeReferencesCache, objectRefere
348
371
  if (SHOULD_USE_READABLE_IDS) {
349
372
  entity.specifiedId = generateRandomReadableString();
350
373
  }
374
+ // roles is a multi-value system property with no type property to drive
375
+ // coercion; a single value still has to reach the API as an array
376
+ if (typeof entity.roles === 'string') {
377
+ if (entity.roles.includes(',')) {
378
+ console.error(`roles value "${entity.roles}" contains a comma and is treated as one literal value. To pass several roles, repeat the key: roles=${entity.roles
379
+ .split(',')
380
+ .join(' roles=')}`);
381
+ }
382
+ entity.roles = [entity.roles];
383
+ }
351
384
  // Support specifiedId
352
385
  if (propertySpecificationBySlug?.has('specifiedId')) {
353
386
  const specifiedId = propertySpecificationBySlug.get('specifiedId');
@@ -490,10 +523,24 @@ function castValueToPropertyType(value, propertyType) {
490
523
  return numValue;
491
524
  case types_1.PropertyType.Date:
492
525
  return new Date(value.toString()).toISOString();
526
+ case types_1.PropertyType.MultiSelect:
527
+ if (Array.isArray(value)) {
528
+ return value;
529
+ }
530
+ // Migration hazard: the old comma syntax parses as one string with a
531
+ // comma in it. Warn with the repeated-key equivalent instead of
532
+ // guessing at a split.
533
+ if (typeof value === 'string' && value.includes(',')) {
534
+ console.error(`Value "${value}" for a multi-value property contains a comma and is treated as one literal value. To pass several values, repeat the key: slug=${value
535
+ .split(',')
536
+ .join(' slug=')}`);
537
+ }
538
+ return [value];
493
539
  default:
494
540
  return value; // For string and other types, return as is
495
541
  }
496
542
  }
543
+ exports.castValueToPropertyType = castValueToPropertyType;
497
544
  async function createEntity(entityName, entity) {
498
545
  const client = new sdk_1.Entities();
499
546
  const skipHydrateRelationships = SHOULD_SKIP_HYDRATE_RELATIONSHIPS;
@@ -270,8 +270,8 @@ describe('Entity Creation E2E Happy Path', () => {
270
270
  expect(result.get('quantity')).toBe('10');
271
271
  expect(result.get('isActive')).toBe('true');
272
272
  });
273
- it('should handle roles as comma-separated array', () => {
274
- const specs = ['roles=option,family', 'name=Test'];
273
+ it('should build roles arrays from repeated keys', () => {
274
+ const specs = ['roles=option', 'roles=family', 'name=Test'];
275
275
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
276
276
  expect(result.get('roles')).toEqual(['option', 'family']);
277
277
  expect(result.get('name')).toBe('Test');
@@ -333,7 +333,9 @@ describe('Entity Creation E2E Happy Path', () => {
333
333
  const propertySpecs = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)([
334
334
  'roles=option',
335
335
  ]);
336
- const roles = propertySpecs.get('roles');
336
+ // a single roles value stays scalar at the parse layer and is
337
+ // normalized to an array where it is consumed
338
+ const roles = [].concat(propertySpecs.get('roles'));
337
339
  // Test that property level matching works with specified roles
338
340
  expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, roles[0])).toBe(true);
339
341
  expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, roles[0])).toBe(false);
@@ -514,3 +516,28 @@ describe('Entity Creation E2E Happy Path', () => {
514
516
  });
515
517
  });
516
518
  });
519
+ describe('castValueToPropertyType array coercion', () => {
520
+ it('should coerce a single value on a multi-value property to a one-element array', () => {
521
+ expect((0, create_1.castValueToPropertyType)('option', types_1.PropertyType.MultiSelect)).toEqual(['option']);
522
+ });
523
+ it('should pass arrays through on multi-value properties', () => {
524
+ expect((0, create_1.castValueToPropertyType)(['option', 'family'], types_1.PropertyType.MultiSelect)).toEqual([
525
+ 'option',
526
+ 'family',
527
+ ]);
528
+ });
529
+ it('should keep a comma value literal on a multi-value property and warn with the repeated-key form', () => {
530
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
531
+ expect((0, create_1.castValueToPropertyType)('option,family', types_1.PropertyType.MultiSelect)).toEqual([
532
+ 'option,family',
533
+ ]);
534
+ expect(consoleError).toHaveBeenCalledWith(expect.stringContaining('slug=option slug=family'));
535
+ consoleError.mockRestore();
536
+ });
537
+ it('should keep commas literal on string properties without warning', () => {
538
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
539
+ expect((0, create_1.castValueToPropertyType)('my,name', types_1.PropertyType.String)).toBe('my,name');
540
+ expect(consoleError).not.toHaveBeenCalled();
541
+ consoleError.mockRestore();
542
+ });
543
+ });
@@ -4,13 +4,14 @@ export default class DeleteEntity extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  dryRun: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
- input: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
8
- id: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
+ inputFile: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
8
  alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
9
  logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
10
  };
12
11
  static args: {
13
12
  entityName: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
13
+ ids: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
14
14
  };
15
+ static strict: boolean;
15
16
  run(): Promise<void>;
16
17
  }
@@ -6,63 +6,87 @@ const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
6
6
  const basecommand_1 = require("../../basecommand");
7
7
  const sdk_1 = require("@contrail/sdk");
8
8
  const login_1 = require("../../auth/login");
9
+ const common_1 = require("../../common");
9
10
  const file_helpers_1 = require("../../helpers/file-helpers");
10
11
  let IS_DRY_RUN = false;
11
12
  class DeleteEntity extends basecommand_1.BaseCommand {
12
- static description = '(BETA) Delete an entity';
13
- static examples = ['contrail entities delete color 123'];
13
+ static description = '(BETA) Delete entities by id';
14
+ static examples = [
15
+ 'contrail entities delete color 123',
16
+ 'contrail entities delete color id1 id2 id3',
17
+ 'contrail entities delete item -i ids.json',
18
+ ];
14
19
  static flags = {
15
20
  ...basecommand_1.BaseCommand.baseFlags,
16
21
  dryRun: core_1.Flags.boolean({
17
22
  char: 'd',
18
- description: 'If true, does not delete the entity but simulates the deletion',
23
+ description: 'If true, does not delete the entities but simulates the deletion',
19
24
  required: false,
20
25
  default: false,
21
26
  }),
22
- input: core_1.Flags.string({
27
+ inputFile: core_1.Flags.string({
23
28
  char: 'i',
24
- description: 'A JSON array of objects where each object is one entity to create. Cannot be used with --count. Unless --precise is specified, any missing values will be populated with random values.',
29
+ description: 'A file with a JSON array of objects; only each object\'s id is used. Deletes every entity in the file.',
25
30
  required: false,
26
31
  multiple: false,
27
32
  }),
28
- id: core_1.Flags.string({
29
- description: 'The unique identifier of the entity',
30
- required: false,
31
- }),
32
33
  };
33
34
  static args = {
34
35
  entityName: core_1.Args.string({
35
36
  description: 'The name of the entity type to delete',
36
37
  required: true,
37
38
  }),
39
+ ids: core_1.Args.string({
40
+ description: 'One or more entity ids to delete',
41
+ required: false,
42
+ }),
38
43
  };
44
+ static strict = false;
39
45
  async run() {
40
- this.log('🚨 This command is in BETA and may change or be removed at any time.');
46
+ (0, common_1.routeConsoleLogToStderr)();
47
+ console.error('🚨 This command is in BETA and may change or be removed at any time.');
41
48
  await (0, login_1.authUser)();
42
- const { args, flags } = await this.parse(DeleteEntity);
49
+ const { args, flags, argv } = await this.parse(DeleteEntity);
43
50
  const { entityName } = args;
44
- const { id, input: inputFilename } = flags;
51
+ const { inputFile: inputFilename } = flags;
45
52
  if (flags.dryRun) {
46
53
  IS_DRY_RUN = true;
47
- this.log('Running in dry run mode. No entities will be deleted.');
54
+ console.error('Running in dry run mode. No entities will be deleted.');
55
+ }
56
+ const tokens = argv.slice(1);
57
+ const valueTokens = tokens.filter((token) => token.includes('='));
58
+ if (valueTokens.length > 0) {
59
+ this.error(`delete takes no -- values (got: ${valueTokens.join(' ')}); pass ids positionally or use an input file (-i).`);
60
+ }
61
+ const positionalIds = tokens;
62
+ if (inputFilename && positionalIds.length > 0) {
63
+ this.error('You cannot use --inputFile and positional ids together. Please specify one or the other.');
64
+ }
65
+ if (!inputFilename && positionalIds.length === 0) {
66
+ this.error('You must provide one or more ids or an input file (-i) of entities to delete.');
48
67
  }
49
68
  const jsonInput = inputFilename ? (0, file_helpers_1.parseFileAsJsonArray)(inputFilename) : undefined;
50
- if (!jsonInput && !id) {
51
- this.error('You must provide either an ID or a JSON input file with entities to delete.');
69
+ const rowsWithId = jsonInput?.filter((item) => item.id);
70
+ if (jsonInput && rowsWithId.length < jsonInput.length) {
71
+ console.error(`${jsonInput.length - rowsWithId.length} row(s) without an id were skipped.`);
52
72
  }
53
- const ids = jsonInput ? jsonInput.map((item) => item.id) : [id];
73
+ const ids = rowsWithId ? rowsWithId.map((item) => item.id) : positionalIds;
54
74
  try {
55
75
  const response = await deleteEntityByIds(entityName, ids);
56
76
  if (IS_DRY_RUN) {
57
- this.log('simulated deletion of entities:');
77
+ console.error('simulated deletion of entities:');
58
78
  }
59
79
  else {
60
- this.log('deleted entities:');
80
+ console.error('deleted entities:');
61
81
  }
62
82
  this.log(JSON.stringify(response, null, 2));
83
+ if (response.idsErrored.length > 0) {
84
+ console.error(`${response.idsErrored.length} of ${ids.length} deletes failed.`);
85
+ process.exitCode = 1;
86
+ }
63
87
  }
64
88
  catch (e) {
65
- this.error(`Error fetching entity: ${e.message}`);
89
+ this.error(`Error deleting entities: ${e.message}`);
66
90
  }
67
91
  }
68
92
  }
@@ -15,6 +15,7 @@ export default class GetEntity extends BaseCommand {
15
15
  };
16
16
  static args: {
17
17
  typePath: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
18
+ ids: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
18
19
  };
19
20
  static strict: boolean;
20
21
  run(): Promise<void>;