@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
|
@@ -6,6 +6,7 @@ const core_1 = require("@oclif/core");
|
|
|
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 types_1 = require("../../api/types");
|
|
10
11
|
const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
|
|
11
12
|
const batchFetch_1 = require("../../helpers/entities/batchFetch");
|
|
@@ -14,6 +15,8 @@ let MAX_NUMBER_OF_ENTITIES_TO_FETCH = undefined;
|
|
|
14
15
|
class GetEntity extends basecommand_1.BaseCommand {
|
|
15
16
|
static description = '(BETA) Get an entity';
|
|
16
17
|
static examples = [
|
|
18
|
+
'contrail entities get color 123',
|
|
19
|
+
'contrail entities get color id1 id2 id3',
|
|
17
20
|
'contrail entities get color -- id=123',
|
|
18
21
|
'contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16',
|
|
19
22
|
'contrail entities get orders -- updatedAt=2025-09-15T04:00:00Z..',
|
|
@@ -65,10 +68,15 @@ class GetEntity extends basecommand_1.BaseCommand {
|
|
|
65
68
|
description: 'The name of the entity type to fetch',
|
|
66
69
|
required: true,
|
|
67
70
|
}),
|
|
71
|
+
ids: core_1.Args.string({
|
|
72
|
+
description: 'One or more entity ids; shorthand for the id/ids criteria',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
68
75
|
};
|
|
69
76
|
static strict = false;
|
|
70
77
|
async run() {
|
|
71
|
-
|
|
78
|
+
(0, common_1.routeConsoleLogToStderr)();
|
|
79
|
+
console.error('🚨 This command is in BETA and may change or be removed at any time.');
|
|
72
80
|
await (0, login_1.authUser)();
|
|
73
81
|
const { args, flags, argv } = await this.parse(GetEntity);
|
|
74
82
|
const { typePath } = args;
|
|
@@ -76,11 +84,23 @@ class GetEntity extends basecommand_1.BaseCommand {
|
|
|
76
84
|
SHOULD_USE_PAGINATED_FIND = shouldUsePaginatedFind;
|
|
77
85
|
MAX_NUMBER_OF_ENTITIES_TO_FETCH = take;
|
|
78
86
|
if (MAX_NUMBER_OF_ENTITIES_TO_FETCH)
|
|
79
|
-
console.
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
console.error(`Will limit output to ${MAX_NUMBER_OF_ENTITIES_TO_FETCH} entities.`);
|
|
88
|
+
const { ids: positionalIds, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
|
|
89
|
+
const propertySpecBySlug = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
90
|
+
if (positionalIds.length > 0) {
|
|
91
|
+
if (propertySpecBySlug.has('id') || propertySpecBySlug.has('ids')) {
|
|
92
|
+
this.error('Pass ids positionally or as -- id=/ids= criteria, not both.');
|
|
93
|
+
}
|
|
94
|
+
if (propertySpecBySlug.size > 0) {
|
|
95
|
+
this.error('Positional ids cannot be combined with other -- criteria. Use -- ids= alongside the other criteria instead.');
|
|
96
|
+
}
|
|
97
|
+
if (positionalIds.length === 1) {
|
|
98
|
+
propertySpecBySlug.set('id', positionalIds[0]);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
propertySpecBySlug.set('ids', positionalIds);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
84
104
|
const relations = relationString
|
|
85
105
|
? relationString
|
|
86
106
|
.split(',')
|
|
@@ -115,7 +135,7 @@ class GetEntity extends basecommand_1.BaseCommand {
|
|
|
115
135
|
}
|
|
116
136
|
writer.write('\n]\n');
|
|
117
137
|
if (outputFile)
|
|
118
|
-
|
|
138
|
+
console.error(`Output saved to ${outputFile}`);
|
|
119
139
|
}
|
|
120
140
|
else {
|
|
121
141
|
const response = await getEntityByTypePath(typePath, propertySpecBySlug, {
|
|
@@ -127,7 +147,7 @@ class GetEntity extends basecommand_1.BaseCommand {
|
|
|
127
147
|
});
|
|
128
148
|
if (outputFile) {
|
|
129
149
|
fs_1.default.writeFileSync(outputFile, JSON.stringify(response, null, 2));
|
|
130
|
-
|
|
150
|
+
console.error(`Output saved to ${outputFile}`);
|
|
131
151
|
}
|
|
132
152
|
else {
|
|
133
153
|
this.log(JSON.stringify(response, null, 2));
|
|
@@ -212,7 +232,7 @@ async function* streamPaginatedFind(typePath, options = {}) {
|
|
|
212
232
|
if (MAX_NUMBER_OF_ENTITIES_TO_FETCH && count >= MAX_NUMBER_OF_ENTITIES_TO_FETCH)
|
|
213
233
|
return;
|
|
214
234
|
}
|
|
215
|
-
console.
|
|
235
|
+
console.error(`Fetched ${count} entities`);
|
|
216
236
|
if (!nextPageKey || results.length === 0)
|
|
217
237
|
return;
|
|
218
238
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const create_1 = tslib_1.__importDefault(require("./create"));
|
|
5
|
+
const get_1 = tslib_1.__importDefault(require("./get"));
|
|
6
|
+
const update_1 = tslib_1.__importDefault(require("./update"));
|
|
7
|
+
const delete_1 = tslib_1.__importDefault(require("./delete"));
|
|
8
|
+
const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
|
|
9
|
+
describe('entities command grammar: entities <verb> <typePath> [ID...]', () => {
|
|
10
|
+
describe('partitionIdsAndSpecs', () => {
|
|
11
|
+
it('should treat tokens without an equals sign as ids', () => {
|
|
12
|
+
const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['id1', 'id2', 'id3']);
|
|
13
|
+
expect(ids).toEqual(['id1', 'id2', 'id3']);
|
|
14
|
+
expect(specs).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
it('should treat slug=value tokens as property specifications', () => {
|
|
17
|
+
const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['name=jet', 'hex=000000']);
|
|
18
|
+
expect(ids).toEqual([]);
|
|
19
|
+
expect(specs).toEqual(['name=jet', 'hex=000000']);
|
|
20
|
+
});
|
|
21
|
+
it('should split mixed tokens preserving order within each group', () => {
|
|
22
|
+
const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['id1', 'name=jet', 'id2', 'status=archived']);
|
|
23
|
+
expect(ids).toEqual(['id1', 'id2']);
|
|
24
|
+
expect(specs).toEqual(['name=jet', 'status=archived']);
|
|
25
|
+
});
|
|
26
|
+
it('should not classify malformed specs as ids', () => {
|
|
27
|
+
const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['=value']);
|
|
28
|
+
expect(ids).toEqual([]);
|
|
29
|
+
expect(specs).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
it('should handle empty input', () => {
|
|
32
|
+
const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)([]);
|
|
33
|
+
expect(ids).toEqual([]);
|
|
34
|
+
expect(specs).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('positional id arguments', () => {
|
|
38
|
+
it('get accepts variadic positional ids', () => {
|
|
39
|
+
expect(get_1.default.strict).toBe(false);
|
|
40
|
+
expect(get_1.default.args.ids).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
it('update accepts variadic positional ids and has no single id arg', () => {
|
|
43
|
+
expect(update_1.default.strict).toBe(false);
|
|
44
|
+
expect(update_1.default.args.ids).toBeDefined();
|
|
45
|
+
expect(update_1.default.args.id).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
it('delete accepts variadic positional ids and has no --id flag', () => {
|
|
48
|
+
expect(delete_1.default.strict).toBe(false);
|
|
49
|
+
expect(delete_1.default.args.ids).toBeDefined();
|
|
50
|
+
expect(delete_1.default.flags.id).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('flag alignment across verbs', () => {
|
|
54
|
+
it('the input file is -i, --inputFile on every verb that accepts one', () => {
|
|
55
|
+
expect(create_1.default.flags.inputFile.char).toBe('i');
|
|
56
|
+
expect(update_1.default.flags.inputFile.char).toBe('i');
|
|
57
|
+
expect(delete_1.default.flags.inputFile.char).toBe('i');
|
|
58
|
+
expect(delete_1.default.flags.input).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
it('skipHydrateRelationships is -S on every verb', () => {
|
|
61
|
+
expect(create_1.default.flags.skipHydrateRelationships.char).toBe('S');
|
|
62
|
+
expect(get_1.default.flags.skipHydrateRelationships.char).toBe('S');
|
|
63
|
+
expect(update_1.default.flags.skipHydrateRelationships.char).toBe('S');
|
|
64
|
+
});
|
|
65
|
+
it('get keeps -s for --skip', () => {
|
|
66
|
+
expect(get_1.default.flags.skip.char).toBe('s');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const create_1 = require("./create");
|
|
4
|
+
const update_1 = require("./update");
|
|
5
|
+
const fulfilled = (value) => ({ status: 'fulfilled', value });
|
|
6
|
+
const rejected = (message) => ({
|
|
7
|
+
status: 'rejected',
|
|
8
|
+
reason: new Error(message),
|
|
9
|
+
});
|
|
10
|
+
describe('mutation output summaries', () => {
|
|
11
|
+
describe('summarizeSettledCreates', () => {
|
|
12
|
+
it('should put every fulfilled result under created', () => {
|
|
13
|
+
const inputs = [{ name: 'a' }, { name: 'b' }];
|
|
14
|
+
const summary = (0, create_1.summarizeSettledCreates)(inputs, [
|
|
15
|
+
fulfilled({ id: '1', name: 'a' }),
|
|
16
|
+
fulfilled({ id: '2', name: 'b' }),
|
|
17
|
+
]);
|
|
18
|
+
expect(summary.created).toEqual([
|
|
19
|
+
{ id: '1', name: 'a' },
|
|
20
|
+
{ id: '2', name: 'b' },
|
|
21
|
+
]);
|
|
22
|
+
expect(summary.failed).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
it('should pair each failed input with its error so it can be retried as-is', () => {
|
|
25
|
+
const inputs = [{ itemId: 'itm-1' }, { itemId: 'itm-41x' }, { itemId: 'itm-3' }];
|
|
26
|
+
const summary = (0, create_1.summarizeSettledCreates)(inputs, [
|
|
27
|
+
fulfilled({ id: 'ai-1' }),
|
|
28
|
+
rejected('Item with ID itm-41x does not exist'),
|
|
29
|
+
fulfilled({ id: 'ai-3' }),
|
|
30
|
+
]);
|
|
31
|
+
expect(summary.created).toEqual([{ id: 'ai-1' }, { id: 'ai-3' }]);
|
|
32
|
+
expect(summary.failed).toEqual([
|
|
33
|
+
{ input: { itemId: 'itm-41x' }, error: 'Item with ID itm-41x does not exist' },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
it('should fall back to details when the SDK error message is empty', () => {
|
|
37
|
+
const sdkError = Object.assign(new Error(''), {
|
|
38
|
+
details: '{"message":"Assortment not found.","statusCode":404}',
|
|
39
|
+
});
|
|
40
|
+
const summary = (0, create_1.summarizeSettledCreates)([{ assortmentId: 'bogus' }], [{ status: 'rejected', reason: sdkError }]);
|
|
41
|
+
expect(summary.failed[0].error).toBe('{"message":"Assortment not found.","statusCode":404}');
|
|
42
|
+
});
|
|
43
|
+
it('should stringify reasons that are not Errors', () => {
|
|
44
|
+
const summary = (0, create_1.summarizeSettledCreates)([{ name: 'a' }], [{ status: 'rejected', reason: 'boom' }]);
|
|
45
|
+
expect(summary.failed).toEqual([{ input: { name: 'a' }, error: 'boom' }]);
|
|
46
|
+
});
|
|
47
|
+
it('should handle empty input', () => {
|
|
48
|
+
expect((0, create_1.summarizeSettledCreates)([], [])).toEqual({ created: [], failed: [] });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('summarizeSettledUpdates', () => {
|
|
52
|
+
it('should report updated, failed, and skipped without stopping on failures', () => {
|
|
53
|
+
const inputs = [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }];
|
|
54
|
+
const summary = (0, update_1.summarizeSettledUpdates)(inputs, [
|
|
55
|
+
fulfilled({ id: 'id1', status: 'archived' }),
|
|
56
|
+
rejected('Entity not found.'),
|
|
57
|
+
fulfilled({ id: 'id3', status: 'archived' }),
|
|
58
|
+
], 2);
|
|
59
|
+
expect(summary.updated).toEqual([
|
|
60
|
+
{ id: 'id1', status: 'archived' },
|
|
61
|
+
{ id: 'id3', status: 'archived' },
|
|
62
|
+
]);
|
|
63
|
+
expect(summary.failed).toEqual([{ id: 'id2', error: 'Entity not found.' }]);
|
|
64
|
+
expect(summary.skipped).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
it('should not report a failed id as updated', () => {
|
|
67
|
+
const summary = (0, update_1.summarizeSettledUpdates)([{ id: 'id1' }], [rejected('boom')], 0);
|
|
68
|
+
expect(summary.updated).toEqual([]);
|
|
69
|
+
expect(summary.failed).toEqual([{ id: 'id1', error: 'boom' }]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -12,8 +12,19 @@ export default class UpdateEntity extends BaseCommand {
|
|
|
12
12
|
};
|
|
13
13
|
static args: {
|
|
14
14
|
entityName: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
|
|
15
|
-
|
|
15
|
+
ids: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
|
|
16
16
|
};
|
|
17
17
|
static strict: boolean;
|
|
18
18
|
run(): Promise<void>;
|
|
19
19
|
}
|
|
20
|
+
export type UpdateSummary = {
|
|
21
|
+
updated: any[];
|
|
22
|
+
failed: {
|
|
23
|
+
id: string;
|
|
24
|
+
error: string;
|
|
25
|
+
}[];
|
|
26
|
+
skipped: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function summarizeSettledUpdates(inputs: {
|
|
29
|
+
id: string;
|
|
30
|
+
}[], settled: PromiseSettledResult<any>[], skipped: number): UpdateSummary;
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeSettledUpdates = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
|
|
5
6
|
const core_1 = require("@oclif/core");
|
|
6
7
|
const basecommand_1 = require("../../basecommand");
|
|
7
8
|
const sdk_1 = require("@contrail/sdk");
|
|
8
9
|
const login_1 = require("../../auth/login");
|
|
10
|
+
const common_1 = require("../../common");
|
|
9
11
|
const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
|
|
10
12
|
const file_helpers_1 = require("../../helpers/file-helpers");
|
|
11
13
|
class UpdateEntity extends basecommand_1.BaseCommand {
|
|
12
14
|
static description = '(BETA) Update an entity';
|
|
13
15
|
static examples = [
|
|
14
16
|
'contrail entities update color 123 -- name=obsidian hex=000000',
|
|
17
|
+
'contrail entities update item id1 id2 id3 -- status=archived # apply the same change to several entities',
|
|
15
18
|
'contrail entities update color 123 -- description= notes=\'""\' # Unset description, set notes to empty string',
|
|
16
19
|
];
|
|
17
20
|
static flags = {
|
|
18
21
|
...basecommand_1.BaseCommand.baseFlags,
|
|
19
22
|
skipHydrateRelationships: core_1.Flags.boolean({
|
|
20
|
-
char: '
|
|
23
|
+
char: 'S',
|
|
21
24
|
description: 'Skip hydrating relationships when updating the entity',
|
|
22
25
|
required: false,
|
|
23
26
|
default: false,
|
|
@@ -44,33 +47,38 @@ class UpdateEntity extends basecommand_1.BaseCommand {
|
|
|
44
47
|
description: 'The name of the entity type to update',
|
|
45
48
|
required: true,
|
|
46
49
|
}),
|
|
47
|
-
|
|
48
|
-
description: '
|
|
50
|
+
ids: core_1.Args.string({
|
|
51
|
+
description: 'One or more entity ids to update',
|
|
49
52
|
required: false,
|
|
50
53
|
}),
|
|
51
54
|
};
|
|
52
55
|
static strict = false;
|
|
53
56
|
async run() {
|
|
54
|
-
|
|
57
|
+
(0, common_1.routeConsoleLogToStderr)();
|
|
58
|
+
console.error('🚨 This command is in BETA and may change or be removed at any time.');
|
|
55
59
|
await (0, login_1.authUser)();
|
|
56
60
|
const { args, argv } = await this.parse(UpdateEntity);
|
|
57
|
-
const {
|
|
61
|
+
const { entityName } = args;
|
|
58
62
|
const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships, force, } = this.flags;
|
|
59
|
-
const
|
|
60
|
-
const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
+
const { ids: positionalIds, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
|
|
64
|
+
const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
65
|
+
if (propertySpec.has('id') || propertySpec.has('ids')) {
|
|
66
|
+
this.error('The -- payload is values to write. Pass ids positionally: contrail entities update <typePath> <ID...> -- slug=value');
|
|
67
|
+
}
|
|
68
|
+
if (inputFilename && positionalIds.length > 0) {
|
|
69
|
+
this.error('You cannot use --inputFile and positional ids together. Please specify one or the other.');
|
|
70
|
+
}
|
|
71
|
+
if (!inputFilename && positionalIds.length === 0) {
|
|
72
|
+
this.error('You must provide one or more ids or an input file (-i) of entities to update.');
|
|
73
|
+
}
|
|
74
|
+
if (positionalIds.length > 0 && propertySpec.size === 0) {
|
|
75
|
+
this.error('Nothing to update: pass values to write after --, e.g. -- name=jet');
|
|
63
76
|
}
|
|
64
77
|
const entitiesToUpdate = inputFilename
|
|
65
78
|
? (0, file_helpers_1.parseFileAsJsonArray)(inputFilename)
|
|
66
|
-
:
|
|
67
|
-
const
|
|
68
|
-
idsSuccessfullyUpdated: new Set(),
|
|
69
|
-
idsFailedToUpdate: new Set(),
|
|
70
|
-
numEntriesSkipped: entitiesToUpdate.filter(({ id }) => !id).length,
|
|
71
|
-
};
|
|
79
|
+
: positionalIds.map((id) => ({ id }));
|
|
80
|
+
const skipped = entitiesToUpdate.filter(({ id }) => !id).length;
|
|
72
81
|
const entitiesWithId = entitiesToUpdate.filter(({ id }) => id);
|
|
73
|
-
propertySpec.delete('id'); // Remove 'id' from the property spec if it exists
|
|
74
82
|
// will be applied to all entities
|
|
75
83
|
const fixedValues = [...propertySpec.entries()].reduce((acc, [key, value]) => {
|
|
76
84
|
acc[key] = value;
|
|
@@ -79,35 +87,48 @@ class UpdateEntity extends basecommand_1.BaseCommand {
|
|
|
79
87
|
async function updateEntity(entity) {
|
|
80
88
|
const newEntityState = { ...entity, ...fixedValues };
|
|
81
89
|
const id = entity.id;
|
|
82
|
-
|
|
90
|
+
return isDryRun
|
|
83
91
|
? mockUpdateById(entityName, id, newEntityState, { skipHydrateRelationships })
|
|
84
92
|
: updateEntityById(entityName, id, newEntityState, { skipHydrateRelationships, force });
|
|
85
|
-
promise
|
|
86
|
-
.catch(() => {
|
|
87
|
-
results.idsFailedToUpdate.add(id);
|
|
88
|
-
})
|
|
89
|
-
.then(() => {
|
|
90
|
-
results.idsSuccessfullyUpdated.add(id);
|
|
91
|
-
});
|
|
92
|
-
return promise;
|
|
93
93
|
}
|
|
94
94
|
try {
|
|
95
95
|
const limit = (0, p_limit_1.default)(20);
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.log(
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
const settled = await Promise.allSettled(entitiesWithId.map((entity) => limit(() => updateEntity(entity))));
|
|
97
|
+
const summary = summarizeSettledUpdates(entitiesWithId, settled, skipped);
|
|
98
|
+
if (isDryRun) {
|
|
99
|
+
console.error('Dry run. Update skipped. Changes would be:');
|
|
100
|
+
}
|
|
101
|
+
this.log(JSON.stringify(summary, null, 2));
|
|
102
|
+
if (summary.failed.length > 0) {
|
|
103
|
+
console.error(`${summary.failed.length} of ${entitiesWithId.length} updates failed.`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
104
106
|
}
|
|
105
107
|
catch (e) {
|
|
106
|
-
this.error(`Error
|
|
108
|
+
this.error(`Error updating entities: ${e.message}`);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
exports.default = UpdateEntity;
|
|
113
|
+
// Every row is attempted; a failed row never stops the run. The summary pairs
|
|
114
|
+
// each failed id with its error so the failed rows can be retried.
|
|
115
|
+
function summarizeSettledUpdates(inputs, settled, skipped) {
|
|
116
|
+
const updated = [];
|
|
117
|
+
const failed = [];
|
|
118
|
+
settled.forEach((result, index) => {
|
|
119
|
+
if (result.status === 'fulfilled') {
|
|
120
|
+
updated.push(result.value);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
failed.push({
|
|
124
|
+
id: inputs[index].id,
|
|
125
|
+
error: (0, common_1.sdkErrorMessage)(result.reason),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return { updated, failed, skipped };
|
|
130
|
+
}
|
|
131
|
+
exports.summarizeSettledUpdates = summarizeSettledUpdates;
|
|
111
132
|
async function updateEntityById(entityName, id, changes, options = {}) {
|
|
112
133
|
const client = new sdk_1.Entities();
|
|
113
134
|
const { skipHydrateRelationships, force } = options;
|
package/dist/common.d.ts
CHANGED
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
export declare function getSeparatorByPlatform(): string;
|
|
3
3
|
export declare function delay(ms: number): Promise<NodeJS.Timeout>;
|
|
4
4
|
export declare function removeSlash(string: string): string;
|
|
5
|
+
export declare function routeConsoleLogToStderr(): void;
|
|
6
|
+
export declare function sdkErrorMessage(reason: any): string;
|
package/dist/common.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.removeSlash = exports.delay = exports.getSeparatorByPlatform = void 0;
|
|
3
|
+
exports.sdkErrorMessage = exports.routeConsoleLogToStderr = exports.removeSlash = exports.delay = exports.getSeparatorByPlatform = void 0;
|
|
4
4
|
function getSeparatorByPlatform() {
|
|
5
5
|
const isWin = process.platform === 'win32';
|
|
6
6
|
const separator = isWin ? '\\' : '/';
|
|
@@ -16,3 +16,15 @@ function removeSlash(string) {
|
|
|
16
16
|
return string.replace(re, '-');
|
|
17
17
|
}
|
|
18
18
|
exports.removeSlash = removeSlash;
|
|
19
|
+
// The SDK and auth flow print request logs and token messages via bare
|
|
20
|
+
// console.log. A command whose stdout is a data contract must route those to
|
|
21
|
+
// stderr before any of that code runs.
|
|
22
|
+
function routeConsoleLogToStderr() {
|
|
23
|
+
console.log = (...args) => console.error(...args);
|
|
24
|
+
}
|
|
25
|
+
exports.routeConsoleLogToStderr = routeConsoleLogToStderr;
|
|
26
|
+
// SDK errors can carry an empty message with the response body in details
|
|
27
|
+
function sdkErrorMessage(reason) {
|
|
28
|
+
return reason?.message || reason?.details || String(reason);
|
|
29
|
+
}
|
|
30
|
+
exports.sdkErrorMessage = sdkErrorMessage;
|
|
@@ -29,4 +29,13 @@ export type PropertySpecificationBySlug = Map<string, string | number | string[]
|
|
|
29
29
|
export type PropertySpecificationString = `${string}=${string}`;
|
|
30
30
|
export declare function parsePropertySpecification(spec: PropertySpecificationString): PropertySpecification;
|
|
31
31
|
export declare function parsePropertySpecificationsIntoMap(propertySpecifications: PropertySpecificationString[]): PropertySpecificationBySlug;
|
|
32
|
+
/**
|
|
33
|
+
* Splits command argv tokens (after the typePath) into positional ids and
|
|
34
|
+
* property specifications. A token containing '=' is a property specification
|
|
35
|
+
* candidate; every other token is an id.
|
|
36
|
+
*/
|
|
37
|
+
export declare function partitionIdsAndSpecs(tokens: string[]): {
|
|
38
|
+
ids: string[];
|
|
39
|
+
specs: PropertySpecificationString[];
|
|
40
|
+
};
|
|
32
41
|
export declare function isValidPropertySpecification(maybeSpec: string): maybeSpec is PropertySpecificationString;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.isValidPropertySpecification = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
|
|
26
|
+
exports.isValidPropertySpecification = exports.partitionIdsAndSpecs = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
|
|
27
27
|
const dateRanges_1 = require("./dateRanges");
|
|
28
28
|
// Special tokens for empty strings
|
|
29
29
|
const DOUBLE_QUOTED_EMPTY = '""';
|
|
@@ -64,23 +64,37 @@ function parsePropertySpecificationsIntoMap(propertySpecifications) {
|
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
const { slug, value } = parsePropertySpecification(spec);
|
|
67
|
-
if (slug === 'roles' && value !== undefined) {
|
|
68
|
-
// Special handling for roles, which can be an array (only if value is not undefined)
|
|
69
|
-
const rolesArray = value.split(',').map((role) => role.trim());
|
|
70
|
-
propertySpecBySlug.set(slug, rolesArray);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
67
|
if (slug === 'ids' && value !== undefined) {
|
|
74
|
-
//
|
|
68
|
+
// ids is the one comma-split exception: it is a list of ids by definition
|
|
69
|
+
// and ids never contain commas.
|
|
75
70
|
const idsArray = value.split(',').map((id) => id.trim());
|
|
76
71
|
propertySpecBySlug.set(slug, idsArray);
|
|
77
72
|
continue;
|
|
78
73
|
}
|
|
74
|
+
// Repeating a key accumulates values into an array; commas in every other
|
|
75
|
+
// value are literal.
|
|
76
|
+
const existing = propertySpecBySlug.get(slug);
|
|
77
|
+
if (propertySpecBySlug.has(slug) && existing !== undefined && value !== undefined) {
|
|
78
|
+
propertySpecBySlug.set(slug, Array.isArray(existing) ? [...existing, value] : [existing, value]);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
79
81
|
propertySpecBySlug.set(slug, value);
|
|
80
82
|
}
|
|
81
83
|
return propertySpecBySlug;
|
|
82
84
|
}
|
|
83
85
|
exports.parsePropertySpecificationsIntoMap = parsePropertySpecificationsIntoMap;
|
|
86
|
+
/**
|
|
87
|
+
* Splits command argv tokens (after the typePath) into positional ids and
|
|
88
|
+
* property specifications. A token containing '=' is a property specification
|
|
89
|
+
* candidate; every other token is an id.
|
|
90
|
+
*/
|
|
91
|
+
function partitionIdsAndSpecs(tokens) {
|
|
92
|
+
return {
|
|
93
|
+
ids: tokens.filter((token) => !token.includes('=')),
|
|
94
|
+
specs: tokens.filter(isValidPropertySpecification),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
exports.partitionIdsAndSpecs = partitionIdsAndSpecs;
|
|
84
98
|
function isValidPropertySpecification(maybeSpec) {
|
|
85
99
|
try {
|
|
86
100
|
if ((0, dateRanges_1.isDateRangeSpecification)(maybeSpec)) {
|
|
@@ -142,12 +142,17 @@ describe('parsePropertySpecificationsIntoMap', () => {
|
|
|
142
142
|
expect(result.get('url')).toBe('https://example.com');
|
|
143
143
|
expect(result.size).toBe(3);
|
|
144
144
|
});
|
|
145
|
-
it('should
|
|
145
|
+
it('should accumulate repeated keys into an array', () => {
|
|
146
146
|
const specs = ['name=first', 'name=second', 'name=third'];
|
|
147
147
|
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
148
|
-
expect(result.get('name')).
|
|
148
|
+
expect(result.get('name')).toEqual(['first', 'second', 'third']);
|
|
149
149
|
expect(result.size).toBe(1);
|
|
150
150
|
});
|
|
151
|
+
it('should keep a single value scalar', () => {
|
|
152
|
+
const specs = ['status=archived'];
|
|
153
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
154
|
+
expect(result.get('status')).toBe('archived');
|
|
155
|
+
});
|
|
151
156
|
it('should handle property specifications with empty values', () => {
|
|
152
157
|
const specs = ['name=john', 'description=', 'notes=""'];
|
|
153
158
|
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
@@ -163,21 +168,21 @@ describe('parsePropertySpecificationsIntoMap', () => {
|
|
|
163
168
|
];
|
|
164
169
|
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs)).toThrow('Invalid property specification: invalid');
|
|
165
170
|
});
|
|
166
|
-
it('should
|
|
167
|
-
const specs = ['roles=option,family', 'name=Test'];
|
|
171
|
+
it('should build roles arrays from repeated keys, with commas literal', () => {
|
|
172
|
+
const specs = ['roles=option', 'roles=family', 'name=Test'];
|
|
168
173
|
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
169
174
|
expect(result.get('roles')).toEqual(['option', 'family']);
|
|
170
175
|
expect(result.get('name')).toBe('Test');
|
|
171
176
|
});
|
|
172
|
-
it('should
|
|
173
|
-
const specs = ['roles=option'];
|
|
177
|
+
it('should treat a comma in a roles value as literal', () => {
|
|
178
|
+
const specs = ['roles=option,family'];
|
|
174
179
|
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
175
|
-
expect(result.get('roles')).
|
|
180
|
+
expect(result.get('roles')).toBe('option,family');
|
|
176
181
|
});
|
|
177
|
-
it('should
|
|
178
|
-
const specs = ['
|
|
182
|
+
it('should keep commas literal in ordinary values', () => {
|
|
183
|
+
const specs = ['name=my,name'];
|
|
179
184
|
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
180
|
-
expect(result.get('
|
|
185
|
+
expect(result.get('name')).toBe('my,name');
|
|
181
186
|
});
|
|
182
187
|
it('should handle ids as comma-separated array', () => {
|
|
183
188
|
const specs = ['ids=1,2,3', 'name=Test'];
|
package/dist/logger.js
CHANGED
|
@@ -12,7 +12,10 @@ class Logger {
|
|
|
12
12
|
debug: 'blue',
|
|
13
13
|
};
|
|
14
14
|
constructor() {
|
|
15
|
-
|
|
15
|
+
// All log output goes to stderr so stdout carries only command output (JSON)
|
|
16
|
+
const transport = new winston_1.transports.Console({
|
|
17
|
+
stderrLevels: ['error', 'warn', 'info', 'debug'],
|
|
18
|
+
});
|
|
16
19
|
winston_1.default.addColors(this.colors);
|
|
17
20
|
this.logger = (0, winston_1.createLogger)({
|
|
18
21
|
level: 'info',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const logger_1 = tslib_1.__importDefault(require("./logger"));
|
|
5
|
+
describe('logger', () => {
|
|
6
|
+
it('writes every level to stderr so stdout carries only command output', () => {
|
|
7
|
+
const consoleTransport = logger_1.default.transports[0];
|
|
8
|
+
for (const level of ['error', 'warn', 'info', 'debug']) {
|
|
9
|
+
expect(consoleTransport.stderrLevels[level]).toBe(true);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|