@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 +32 -22
- package/dist/commands/entities/create.d.ts +10 -1
- package/dist/commands/entities/create.js +69 -22
- package/dist/commands/entities/create.spec.js +30 -3
- package/dist/commands/entities/delete.d.ts +3 -2
- package/dist/commands/entities/delete.js +43 -19
- package/dist/commands/entities/get.d.ts +1 -0
- package/dist/commands/entities/get.js +29 -9
- package/dist/commands/entities/grammar.spec.d.ts +1 -0
- package/dist/commands/entities/grammar.spec.js +69 -0
- package/dist/commands/entities/output.spec.d.ts +1 -0
- package/dist/commands/entities/output.spec.js +72 -0
- package/dist/commands/entities/update.d.ts +12 -1
- package/dist/commands/entities/update.js +55 -34
- package/dist/common.d.ts +2 -0
- package/dist/common.js +13 -1
- package/dist/helpers/propertySpecifications/propertySpecifications.d.ts +9 -0
- package/dist/helpers/propertySpecifications/propertySpecifications.js +22 -8
- package/dist/helpers/propertySpecifications/propertySpecifications.spec.js +15 -10
- package/dist/logger.js +4 -1
- package/dist/logger.spec.d.ts +1 -0
- package/dist/logger.spec.js +12 -0
- package/oclif.manifest.json +28 -20
- package/package.json +1 -1
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.
|
|
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 [
|
|
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] [-
|
|
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
|
|
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>]
|
|
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
|
|
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, --
|
|
611
|
-
|
|
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
|
|
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]
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
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
|
|
263
|
+
const summary = await createEntities(entityName, entitiesToCreate);
|
|
261
264
|
if (outputFilename) {
|
|
262
|
-
fs_1.default.writeFileSync(outputFilename, JSON.stringify(
|
|
263
|
-
|
|
265
|
+
fs_1.default.writeFileSync(outputFilename, JSON.stringify(summary, null, 2));
|
|
266
|
+
console.error(`Output saved to ${outputFilename}`);
|
|
264
267
|
}
|
|
265
268
|
else {
|
|
266
|
-
|
|
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.
|
|
300
|
+
console.error(`Dry run mode: not creating entities, but simulating creation for ${entityName}`);
|
|
301
|
+
return { created: entities, failed: [] };
|
|
282
302
|
}
|
|
283
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13
|
-
static examples = [
|
|
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
|
|
23
|
+
description: 'If true, does not delete the entities but simulates the deletion',
|
|
19
24
|
required: false,
|
|
20
25
|
default: false,
|
|
21
26
|
}),
|
|
22
|
-
|
|
27
|
+
inputFile: core_1.Flags.string({
|
|
23
28
|
char: 'i',
|
|
24
|
-
description: 'A JSON array of objects
|
|
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
|
-
|
|
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 {
|
|
51
|
+
const { inputFile: inputFilename } = flags;
|
|
45
52
|
if (flags.dryRun) {
|
|
46
53
|
IS_DRY_RUN = true;
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
77
|
+
console.error('simulated deletion of entities:');
|
|
58
78
|
}
|
|
59
79
|
else {
|
|
60
|
-
|
|
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
|
|
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>;
|