@contrail/cli 3.3.3 → 3.5.0-alpha
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 +150 -1
- package/dist/api/types.d.ts +35 -0
- package/dist/api/types.js +47 -1
- package/dist/auth/login.js +0 -8
- package/dist/basecommand.d.ts +3 -1
- package/dist/basecommand.js +1 -1
- package/dist/commands/app/extension/deploy.js +2 -4
- package/dist/commands/entities/create.d.ts +37 -0
- package/dist/commands/entities/create.js +555 -0
- package/dist/commands/entities/create.spec.d.ts +1 -0
- package/dist/commands/entities/create.spec.js +514 -0
- package/dist/commands/entities/delete.d.ts +16 -0
- package/dist/commands/entities/delete.js +118 -0
- package/dist/commands/entities/get.d.ts +20 -0
- package/dist/commands/entities/get.js +204 -0
- package/dist/commands/entities/update.d.ts +18 -0
- package/dist/commands/entities/update.js +116 -0
- package/dist/commands/types/uploadUsageInfo.js +2 -14
- package/dist/common.d.ts +1 -1
- package/dist/helpers/app/app-deployer.js +4 -8
- package/dist/helpers/app/extension-package-deployer.js +1 -1
- package/dist/helpers/config/config.js +3 -3
- package/dist/helpers/entities/assortment-items.d.ts +10 -0
- package/dist/helpers/entities/assortment-items.js +84 -0
- package/dist/helpers/entities/batchFetch.d.ts +8 -0
- package/dist/helpers/entities/batchFetch.js +41 -0
- package/dist/helpers/entities/index.d.ts +3 -0
- package/dist/helpers/entities/index.js +6 -0
- package/dist/helpers/entities/items.d.ts +10 -0
- package/dist/helpers/entities/items.js +102 -0
- package/dist/helpers/entities/project-items.d.ts +3 -0
- package/dist/helpers/entities/project-items.js +65 -0
- package/dist/helpers/file-helpers.d.ts +1 -0
- package/dist/helpers/file-helpers.js +19 -1
- package/dist/helpers/propertySpecifications/dateRanges.d.ts +38 -0
- package/dist/helpers/propertySpecifications/dateRanges.js +89 -0
- package/dist/helpers/propertySpecifications/dateRanges.spec.d.ts +1 -0
- package/dist/helpers/propertySpecifications/dateRanges.spec.js +95 -0
- package/dist/helpers/propertySpecifications/propertySpecifications.d.ts +27 -0
- package/dist/helpers/propertySpecifications/propertySpecifications.js +70 -0
- package/dist/helpers/propertySpecifications/propertySpecifications.spec.d.ts +1 -0
- package/dist/helpers/propertySpecifications/propertySpecifications.spec.js +266 -0
- package/dist/helpers/types/type-uploader.js +0 -7
- package/dist/interfaces.d.ts +0 -1
- package/dist/templates/typescript/README.md +1 -3
- package/dist/templates/typescript/assets/sample.json +1 -1
- package/dist/templates/typescript/eslintrc.json +4 -1
- package/dist/templates/typescript/tsconfig.json +21 -5
- package/oclif.manifest.json +361 -1
- package/package.json +6 -5
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const types_1 = require("@contrail/types");
|
|
4
|
+
const create_1 = require("./create");
|
|
5
|
+
const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
|
|
6
|
+
describe('doesPropertyLevelMatchRole', () => {
|
|
7
|
+
it('should return true for exact matches', () => {
|
|
8
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, 'option')).toBe(true);
|
|
9
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, 'family')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it('should return false when propertyLevel does not match role', () => {
|
|
12
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, 'family')).toBe(false);
|
|
13
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, 'option')).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it('should return true for ALL property level with any role', () => {
|
|
16
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.ALL, 'option')).toBe(true);
|
|
17
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.ALL, 'family')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it('should return true for OVERRIDABLE property level with any role', () => {
|
|
20
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OVERRIDABLE, 'option')).toBe(true);
|
|
21
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OVERRIDABLE, 'family')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('Entity Creation E2E Happy Path', () => {
|
|
25
|
+
describe('pickRandomValueForProperty', () => {
|
|
26
|
+
const mockObjectReferencesCache = new Map();
|
|
27
|
+
const mockTypeReferencesCache = new Map();
|
|
28
|
+
it('should generate random string values for String property type', () => {
|
|
29
|
+
const property = {
|
|
30
|
+
slug: 'name',
|
|
31
|
+
propertyType: types_1.PropertyType.String,
|
|
32
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
33
|
+
};
|
|
34
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
35
|
+
expect(typeof result).toBe('string');
|
|
36
|
+
expect(result.length).toBeGreaterThan(0);
|
|
37
|
+
});
|
|
38
|
+
it('should generate random boolean values for Boolean property type', () => {
|
|
39
|
+
const property = {
|
|
40
|
+
slug: 'isActive',
|
|
41
|
+
propertyType: types_1.PropertyType.Boolean,
|
|
42
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
43
|
+
};
|
|
44
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
45
|
+
expect(typeof result).toBe('boolean');
|
|
46
|
+
});
|
|
47
|
+
it('should generate random number values for Number property type', () => {
|
|
48
|
+
const property = {
|
|
49
|
+
slug: 'quantity',
|
|
50
|
+
propertyType: types_1.PropertyType.Number,
|
|
51
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
52
|
+
};
|
|
53
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
54
|
+
expect(typeof result).toBe('number');
|
|
55
|
+
});
|
|
56
|
+
it('should generate random date string for Date property type', () => {
|
|
57
|
+
const property = {
|
|
58
|
+
slug: 'createdAt',
|
|
59
|
+
propertyType: types_1.PropertyType.Date,
|
|
60
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
61
|
+
};
|
|
62
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
63
|
+
expect(typeof result).toBe('string');
|
|
64
|
+
expect(new Date(result).toString()).not.toBe('Invalid Date');
|
|
65
|
+
});
|
|
66
|
+
it('should pick random option for SingleSelect property type', () => {
|
|
67
|
+
const options = [
|
|
68
|
+
{ value: 'red', display: 'Red' },
|
|
69
|
+
{ value: 'blue', display: 'Blue' },
|
|
70
|
+
{ value: 'green', display: 'Green' },
|
|
71
|
+
];
|
|
72
|
+
const property = {
|
|
73
|
+
slug: 'color',
|
|
74
|
+
propertyType: types_1.PropertyType.SingleSelect,
|
|
75
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
76
|
+
options,
|
|
77
|
+
};
|
|
78
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
79
|
+
expect(options.some((option) => option.value === result)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('should pick random options for MultiSelect property type', () => {
|
|
82
|
+
const options = [
|
|
83
|
+
{ value: 'feature1', display: 'Feature 1' },
|
|
84
|
+
{ value: 'feature2', display: 'Feature 2' },
|
|
85
|
+
{ value: 'feature3', display: 'Feature 3' },
|
|
86
|
+
];
|
|
87
|
+
const property = {
|
|
88
|
+
slug: 'features',
|
|
89
|
+
propertyType: types_1.PropertyType.MultiSelect,
|
|
90
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
91
|
+
options,
|
|
92
|
+
};
|
|
93
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
94
|
+
expect(Array.isArray(result)).toBe(true);
|
|
95
|
+
expect(result.length).toBeGreaterThan(0);
|
|
96
|
+
result.forEach((value) => {
|
|
97
|
+
expect(options.some((option) => option.value === value)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it('should return object reference ID for ObjectReference property type', () => {
|
|
101
|
+
const mockEntity = { id: 'test-id-123', name: 'Test Entity' };
|
|
102
|
+
const objectCache = new Map([['test-id-123', mockEntity]]);
|
|
103
|
+
mockObjectReferencesCache.set('custom-entity:test', objectCache);
|
|
104
|
+
const property = {
|
|
105
|
+
slug: 'parentId',
|
|
106
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
107
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
108
|
+
referencedTypePath: 'custom-entity:test',
|
|
109
|
+
};
|
|
110
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
111
|
+
expect(result).toBe('test-id-123');
|
|
112
|
+
});
|
|
113
|
+
it('should return type reference ID for TypeReference property type', () => {
|
|
114
|
+
const mockType = { id: 'type-id-456', typePath: 'custom-entity:category' };
|
|
115
|
+
mockTypeReferencesCache.set('custom-entity:category', mockType);
|
|
116
|
+
const property = {
|
|
117
|
+
slug: 'categoryType',
|
|
118
|
+
propertyType: types_1.PropertyType.TypeReference,
|
|
119
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
120
|
+
referencedTypePath: 'custom-entity:category',
|
|
121
|
+
};
|
|
122
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
123
|
+
expect(result).toBe('type-id-456');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('buildObjectReferenceCache', () => {
|
|
127
|
+
it('should build a cache from entities with IDs', () => {
|
|
128
|
+
const entities = [
|
|
129
|
+
{ id: 'entity-1', name: 'First Entity', type: 'test' },
|
|
130
|
+
{ id: 'entity-2', name: 'Second Entity', type: 'test' },
|
|
131
|
+
{ id: 'entity-3', name: 'Third Entity', type: 'test' },
|
|
132
|
+
];
|
|
133
|
+
const cache = (0, create_1.buildObjectReferenceCache)(entities);
|
|
134
|
+
expect(cache.size).toBe(3);
|
|
135
|
+
expect(cache.get('entity-1')).toEqual(entities[0]);
|
|
136
|
+
expect(cache.get('entity-2')).toEqual(entities[1]);
|
|
137
|
+
expect(cache.get('entity-3')).toEqual(entities[2]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('getReferencedObjectPaths', () => {
|
|
141
|
+
it('should extract object reference paths from type properties', () => {
|
|
142
|
+
const mockType = {
|
|
143
|
+
id: 'test-type',
|
|
144
|
+
typePath: 'item',
|
|
145
|
+
typeProperties: [
|
|
146
|
+
{
|
|
147
|
+
slug: 'name',
|
|
148
|
+
propertyType: types_1.PropertyType.String,
|
|
149
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
slug: 'categoryId',
|
|
153
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
154
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
155
|
+
referencedTypePath: 'custom-entity:category',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
slug: 'supplierId',
|
|
159
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
160
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
161
|
+
referencedTypePath: 'custom-entity:supplier',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
slug: 'description',
|
|
165
|
+
propertyType: types_1.PropertyType.Text,
|
|
166
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
const referencedPaths = (0, create_1.getReferencedObjectPaths)(mockType);
|
|
171
|
+
expect(referencedPaths).toEqual(['custom-entity:category', 'custom-entity:supplier']);
|
|
172
|
+
});
|
|
173
|
+
it('should return empty array when no object references exist', () => {
|
|
174
|
+
const mockType = {
|
|
175
|
+
id: 'test-type',
|
|
176
|
+
typePath: 'item',
|
|
177
|
+
typeProperties: [
|
|
178
|
+
{
|
|
179
|
+
slug: 'name',
|
|
180
|
+
propertyType: types_1.PropertyType.String,
|
|
181
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
slug: 'isActive',
|
|
185
|
+
propertyType: types_1.PropertyType.Boolean,
|
|
186
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
expect((0, create_1.getReferencedObjectPaths)(mockType)).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('Integration: Complete entity creation flow', () => {
|
|
194
|
+
it('should create a complete entity with all property types populated', () => {
|
|
195
|
+
// Mock a comprehensive type with various property types
|
|
196
|
+
const mockType = {
|
|
197
|
+
id: 'comprehensive-type',
|
|
198
|
+
typePath: 'item',
|
|
199
|
+
typeProperties: [
|
|
200
|
+
{
|
|
201
|
+
slug: 'name',
|
|
202
|
+
propertyType: types_1.PropertyType.String,
|
|
203
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
slug: 'isActive',
|
|
207
|
+
propertyType: types_1.PropertyType.Boolean,
|
|
208
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
slug: 'quantity',
|
|
212
|
+
propertyType: types_1.PropertyType.Number,
|
|
213
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
slug: 'color',
|
|
217
|
+
propertyType: types_1.PropertyType.SingleSelect,
|
|
218
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
219
|
+
options: [
|
|
220
|
+
{ value: 'red', display: 'Red' },
|
|
221
|
+
{ value: 'blue', display: 'Blue' },
|
|
222
|
+
{ value: 'green', display: 'Green' },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
slug: 'features',
|
|
227
|
+
propertyType: types_1.PropertyType.MultiSelect,
|
|
228
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
229
|
+
options: [
|
|
230
|
+
{ value: 'waterproof', display: 'Waterproof' },
|
|
231
|
+
{ value: 'durable', display: 'Durable' },
|
|
232
|
+
{ value: 'lightweight', display: 'Lightweight' },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
// Create mock caches
|
|
238
|
+
const objectReferencesCache = new Map();
|
|
239
|
+
const typeReferencesCache = new Map();
|
|
240
|
+
// Generate random values for each property
|
|
241
|
+
const generatedEntity = {};
|
|
242
|
+
for (const property of mockType.typeProperties) {
|
|
243
|
+
const value = (0, create_1.pickRandomValueForProperty)(property, objectReferencesCache, typeReferencesCache);
|
|
244
|
+
generatedEntity[property.slug] = value;
|
|
245
|
+
}
|
|
246
|
+
// Verify all properties are populated with correct types
|
|
247
|
+
expect(typeof generatedEntity.name).toBe('string');
|
|
248
|
+
expect(generatedEntity.name.length).toBeGreaterThan(0);
|
|
249
|
+
expect(typeof generatedEntity.isActive).toBe('boolean');
|
|
250
|
+
expect(typeof generatedEntity.quantity).toBe('number');
|
|
251
|
+
expect(generatedEntity.quantity).toBeGreaterThanOrEqual(0);
|
|
252
|
+
expect(['red', 'blue', 'green']).toContain(generatedEntity.color);
|
|
253
|
+
expect(Array.isArray(generatedEntity.features)).toBe(true);
|
|
254
|
+
expect(generatedEntity.features.length).toBeGreaterThan(0);
|
|
255
|
+
generatedEntity.features.forEach((feature) => {
|
|
256
|
+
expect(['waterproof', 'durable', 'lightweight']).toContain(feature);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('Property Specifications Integration', () => {
|
|
261
|
+
describe('parsePropertySpecificationsIntoMap', () => {
|
|
262
|
+
it('should parse simple property specifications', () => {
|
|
263
|
+
const specs = [
|
|
264
|
+
'name=Test Item',
|
|
265
|
+
'quantity=10',
|
|
266
|
+
'isActive=true',
|
|
267
|
+
];
|
|
268
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
269
|
+
expect(result.get('name')).toBe('Test Item');
|
|
270
|
+
expect(result.get('quantity')).toBe('10');
|
|
271
|
+
expect(result.get('isActive')).toBe('true');
|
|
272
|
+
});
|
|
273
|
+
it('should handle roles as comma-separated array', () => {
|
|
274
|
+
const specs = ['roles=option,family', 'name=Test'];
|
|
275
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
276
|
+
expect(result.get('roles')).toEqual(['option', 'family']);
|
|
277
|
+
expect(result.get('name')).toBe('Test');
|
|
278
|
+
});
|
|
279
|
+
it('should handle complex values with spaces and special characters', () => {
|
|
280
|
+
const specs = [
|
|
281
|
+
'description=This is a long description with spaces',
|
|
282
|
+
'hex=#FF0000',
|
|
283
|
+
];
|
|
284
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
285
|
+
expect(result.get('description')).toBe('This is a long description with spaces');
|
|
286
|
+
expect(result.get('hex')).toBe('#FF0000');
|
|
287
|
+
});
|
|
288
|
+
it('should throw error for invalid specifications', () => {
|
|
289
|
+
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['invalid-spec'])).toThrow('Invalid property specification: invalid-spec');
|
|
290
|
+
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['=value'])).toThrow('Invalid property specification: =value');
|
|
291
|
+
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['key='])).toThrow('Invalid property specification: key=');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
describe('isValidPropertySpecification', () => {
|
|
295
|
+
it('should validate correct property specifications', () => {
|
|
296
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('name=value')).toBe(true);
|
|
297
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('id=123')).toBe(true);
|
|
298
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('description=Long description with spaces')).toBe(true);
|
|
299
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('roles=option,family')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it('should reject invalid property specifications', () => {
|
|
302
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('invalid')).toBe(false);
|
|
303
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('=')).toBe(false);
|
|
304
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('key=')).toBe(false);
|
|
305
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('=value')).toBe(false);
|
|
306
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('')).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('Property specifications with entity creation', () => {
|
|
310
|
+
it('should apply property specifications to override random values', () => {
|
|
311
|
+
const propertySpecs = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)([
|
|
312
|
+
'name=Custom Name',
|
|
313
|
+
'quantity=42',
|
|
314
|
+
'isActive=true',
|
|
315
|
+
]);
|
|
316
|
+
// Simulate the property specification override logic
|
|
317
|
+
const entity = {};
|
|
318
|
+
// First generate random values (simulated)
|
|
319
|
+
entity.name = 'random-name';
|
|
320
|
+
entity.quantity = 99;
|
|
321
|
+
entity.isActive = false;
|
|
322
|
+
// Then apply property specifications
|
|
323
|
+
for (const [slug, value] of propertySpecs.entries()) {
|
|
324
|
+
entity[slug] = value;
|
|
325
|
+
}
|
|
326
|
+
expect(entity.name).toBe('Custom Name');
|
|
327
|
+
expect(entity.quantity).toBe('42'); // Note: type casting would happen in real implementation
|
|
328
|
+
expect(entity.isActive).toBe('true'); // Note: type casting would happen in real implementation
|
|
329
|
+
});
|
|
330
|
+
it('should handle roles property specification for filtering properties', () => {
|
|
331
|
+
const propertySpecs = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)([
|
|
332
|
+
'roles=option',
|
|
333
|
+
]);
|
|
334
|
+
const roles = propertySpecs.get('roles');
|
|
335
|
+
// Test that property level matching works with specified roles
|
|
336
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, roles[0])).toBe(true);
|
|
337
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, roles[0])).toBe(false);
|
|
338
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.ALL, roles[0])).toBe(true);
|
|
339
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OVERRIDABLE, roles[0])).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
describe('Error Handling and Edge Cases', () => {
|
|
344
|
+
describe('pickRandomValueForProperty error scenarios', () => {
|
|
345
|
+
const mockObjectReferencesCache = new Map();
|
|
346
|
+
const mockTypeReferencesCache = new Map();
|
|
347
|
+
it('should throw error for SingleSelect without options', () => {
|
|
348
|
+
const property = {
|
|
349
|
+
slug: 'color',
|
|
350
|
+
propertyType: types_1.PropertyType.SingleSelect,
|
|
351
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
352
|
+
// Missing options property
|
|
353
|
+
};
|
|
354
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache)).toThrow('Option set is required for SingleSelect property');
|
|
355
|
+
});
|
|
356
|
+
it('should throw error for MultiSelect without options', () => {
|
|
357
|
+
const property = {
|
|
358
|
+
slug: 'features',
|
|
359
|
+
propertyType: types_1.PropertyType.MultiSelect,
|
|
360
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
361
|
+
// Missing options property
|
|
362
|
+
};
|
|
363
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache)).toThrow('Option set is required for MultiSelect property');
|
|
364
|
+
});
|
|
365
|
+
it('should throw error for ObjectReference with missing cache', () => {
|
|
366
|
+
const property = {
|
|
367
|
+
slug: 'categoryId',
|
|
368
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
369
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
370
|
+
referencedTypePath: 'custom-entity:nonexistent',
|
|
371
|
+
};
|
|
372
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache)).toThrow('No object references found for referencedTypePath: custom-entity:nonexistent');
|
|
373
|
+
});
|
|
374
|
+
it('should throw error for ObjectReference with empty cache', () => {
|
|
375
|
+
const emptyCache = new Map();
|
|
376
|
+
emptyCache.set('custom-entity:empty', new Map());
|
|
377
|
+
const property = {
|
|
378
|
+
slug: 'categoryId',
|
|
379
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
380
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
381
|
+
referencedTypePath: 'custom-entity:empty',
|
|
382
|
+
};
|
|
383
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, emptyCache, mockTypeReferencesCache)).toThrow('No object references found for referencedTypePath: custom-entity:empty');
|
|
384
|
+
});
|
|
385
|
+
it('should throw error for TypeReference with missing type', () => {
|
|
386
|
+
const property = {
|
|
387
|
+
slug: 'categoryType',
|
|
388
|
+
propertyType: types_1.PropertyType.TypeReference,
|
|
389
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
390
|
+
referencedTypePath: 'custom-entity:nonexistent',
|
|
391
|
+
};
|
|
392
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache)).toThrow('Referenced type not found for path: custom-entity:nonexistent');
|
|
393
|
+
});
|
|
394
|
+
it('should throw error for unsupported property type', () => {
|
|
395
|
+
const property = {
|
|
396
|
+
slug: 'unsupported',
|
|
397
|
+
propertyType: 'UnsupportedType',
|
|
398
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
399
|
+
};
|
|
400
|
+
expect(() => (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache)).toThrow('Cannot handle property type: UnsupportedType');
|
|
401
|
+
});
|
|
402
|
+
it('should handle Formula property type gracefully', () => {
|
|
403
|
+
const property = {
|
|
404
|
+
slug: 'calculatedValue',
|
|
405
|
+
propertyType: types_1.PropertyType.Formula,
|
|
406
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
407
|
+
};
|
|
408
|
+
const result = (0, create_1.pickRandomValueForProperty)(property, mockObjectReferencesCache, mockTypeReferencesCache);
|
|
409
|
+
expect(result).toBeUndefined();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
describe('Cache building edge cases', () => {
|
|
413
|
+
it('should handle empty entities array', () => {
|
|
414
|
+
const cache = (0, create_1.buildObjectReferenceCache)([]);
|
|
415
|
+
expect(cache.size).toBe(0);
|
|
416
|
+
});
|
|
417
|
+
it('should handle entities without IDs gracefully', () => {
|
|
418
|
+
const entitiesWithoutIds = [
|
|
419
|
+
{ name: 'Entity 1' },
|
|
420
|
+
{ id: 'entity-2', name: 'Entity 2' },
|
|
421
|
+
undefined, // Undefined entity
|
|
422
|
+
].filter(Boolean); // Remove undefined
|
|
423
|
+
const cache = (0, create_1.buildObjectReferenceCache)(entitiesWithoutIds);
|
|
424
|
+
expect(cache.size).toBe(1); // Only the valid entity with ID
|
|
425
|
+
expect(cache.get('entity-2')).toEqual({ id: 'entity-2', name: 'Entity 2' });
|
|
426
|
+
});
|
|
427
|
+
it('should handle duplicate entity IDs', () => {
|
|
428
|
+
const entitiesWithDuplicates = [
|
|
429
|
+
{ id: 'duplicate-id', name: 'First Entity' },
|
|
430
|
+
{ id: 'duplicate-id', name: 'Second Entity' },
|
|
431
|
+
{ id: 'unique-id', name: 'Unique Entity' },
|
|
432
|
+
];
|
|
433
|
+
const cache = (0, create_1.buildObjectReferenceCache)(entitiesWithDuplicates);
|
|
434
|
+
expect(cache.size).toBe(2);
|
|
435
|
+
// Should keep the first occurrence
|
|
436
|
+
expect(cache.get('duplicate-id')).toEqual({ id: 'duplicate-id', name: 'First Entity' });
|
|
437
|
+
expect(cache.get('unique-id')).toEqual({ id: 'unique-id', name: 'Unique Entity' });
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('Reference path extraction edge cases', () => {
|
|
441
|
+
it('should handle type with no properties', () => {
|
|
442
|
+
const typeWithNoProperties = {
|
|
443
|
+
id: 'empty-type',
|
|
444
|
+
typePath: 'item',
|
|
445
|
+
typeProperties: [],
|
|
446
|
+
};
|
|
447
|
+
const referencedPaths = (0, create_1.getReferencedObjectPaths)(typeWithNoProperties);
|
|
448
|
+
expect(referencedPaths).toEqual([]);
|
|
449
|
+
});
|
|
450
|
+
it('should handle type with undefined typeProperties', () => {
|
|
451
|
+
const typeWithUndefinedProperties = {
|
|
452
|
+
id: 'undefined-props-type',
|
|
453
|
+
typePath: 'item',
|
|
454
|
+
typeProperties: undefined,
|
|
455
|
+
};
|
|
456
|
+
const referencedPaths = (0, create_1.getReferencedObjectPaths)(typeWithUndefinedProperties);
|
|
457
|
+
expect(referencedPaths).toEqual([]);
|
|
458
|
+
});
|
|
459
|
+
it('should filter out non-ObjectReference properties correctly', () => {
|
|
460
|
+
const mixedType = {
|
|
461
|
+
id: 'mixed-type',
|
|
462
|
+
typePath: 'item',
|
|
463
|
+
typeProperties: [
|
|
464
|
+
{
|
|
465
|
+
slug: 'name',
|
|
466
|
+
propertyType: types_1.PropertyType.String,
|
|
467
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
slug: 'categoryId',
|
|
471
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
472
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
473
|
+
referencedTypePath: 'custom-entity:category',
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
slug: 'isActive',
|
|
477
|
+
propertyType: types_1.PropertyType.Boolean,
|
|
478
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
slug: 'supplierId',
|
|
482
|
+
propertyType: types_1.PropertyType.ObjectReference,
|
|
483
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
484
|
+
referencedTypePath: 'custom-entity:supplier',
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
slug: 'typeRef',
|
|
488
|
+
propertyType: types_1.PropertyType.TypeReference,
|
|
489
|
+
propertyLevel: types_1.PropertyLevel.ALL,
|
|
490
|
+
referencedTypePath: 'custom-entity:type',
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
const referencedPaths = (0, create_1.getReferencedObjectPaths)(mixedType);
|
|
495
|
+
expect(referencedPaths).toEqual(['custom-entity:category', 'custom-entity:supplier']);
|
|
496
|
+
expect(referencedPaths).not.toContain('custom-entity:type'); // TypeReference should not be included
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
describe('Property level and role matching edge cases', () => {
|
|
500
|
+
it('should handle valid roles correctly', () => {
|
|
501
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, 'option')).toBe(true);
|
|
502
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, 'family')).toBe(true);
|
|
503
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.ALL, 'option')).toBe(true);
|
|
504
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.ALL, 'family')).toBe(true);
|
|
505
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OVERRIDABLE, 'option')).toBe(true);
|
|
506
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OVERRIDABLE, 'family')).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
it('should reject mismatched property levels and roles', () => {
|
|
509
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, 'family')).toBe(false);
|
|
510
|
+
expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.FAMILY, 'option')).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../basecommand';
|
|
2
|
+
export default class DeleteEntity extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
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>;
|
|
9
|
+
alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
10
|
+
logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
static args: {
|
|
13
|
+
entityName: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
|
|
6
|
+
const basecommand_1 = require("../../basecommand");
|
|
7
|
+
const sdk_1 = require("@contrail/sdk");
|
|
8
|
+
const login_1 = require("../../auth/login");
|
|
9
|
+
const file_helpers_1 = require("../../helpers/file-helpers");
|
|
10
|
+
let IS_DRY_RUN = false;
|
|
11
|
+
class DeleteEntity extends basecommand_1.BaseCommand {
|
|
12
|
+
static description = '(BETA) Delete an entity';
|
|
13
|
+
static examples = ['contrail entities delete color 123'];
|
|
14
|
+
static flags = {
|
|
15
|
+
...basecommand_1.BaseCommand.baseFlags,
|
|
16
|
+
dryRun: core_1.Flags.boolean({
|
|
17
|
+
char: 'd',
|
|
18
|
+
description: 'If true, does not delete the entity but simulates the deletion',
|
|
19
|
+
required: false,
|
|
20
|
+
default: false,
|
|
21
|
+
}),
|
|
22
|
+
input: core_1.Flags.string({
|
|
23
|
+
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.',
|
|
25
|
+
required: false,
|
|
26
|
+
multiple: false,
|
|
27
|
+
}),
|
|
28
|
+
id: core_1.Flags.string({
|
|
29
|
+
description: 'The unique identifier of the entity',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
static args = {
|
|
34
|
+
entityName: core_1.Args.string({
|
|
35
|
+
description: 'The name of the entity type to delete',
|
|
36
|
+
required: true,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
this.log('🚨 This command is in BETA and may change or be removed at any time.');
|
|
41
|
+
await (0, login_1.authUser)();
|
|
42
|
+
const { args, flags } = await this.parse(DeleteEntity);
|
|
43
|
+
const { entityName } = args;
|
|
44
|
+
const { id, input: inputFilename } = flags;
|
|
45
|
+
if (flags.dryRun) {
|
|
46
|
+
IS_DRY_RUN = true;
|
|
47
|
+
this.log('Running in dry run mode. No entities will be deleted.');
|
|
48
|
+
}
|
|
49
|
+
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.');
|
|
52
|
+
}
|
|
53
|
+
const ids = jsonInput ? jsonInput.map((item) => item.id) : [id];
|
|
54
|
+
try {
|
|
55
|
+
const response = await deleteEntityByIds(entityName, ids);
|
|
56
|
+
if (IS_DRY_RUN) {
|
|
57
|
+
this.log('simulated deletion of entities:');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.log('deleted entities:');
|
|
61
|
+
}
|
|
62
|
+
this.log(JSON.stringify(response, null, 2));
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
this.error(`Error fetching entity: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.default = DeleteEntity;
|
|
70
|
+
async function deleteEntityByIds(entityName, ids) {
|
|
71
|
+
const idsDeleted = new Set();
|
|
72
|
+
const idsNotFound = new Set();
|
|
73
|
+
const idsErrored = new Set();
|
|
74
|
+
const limit = (0, p_limit_1.default)(10);
|
|
75
|
+
const deletePromises = ids.map((id) => limit(() => deleteEntityById(entityName, id).then((entity) => {
|
|
76
|
+
if (!entity) {
|
|
77
|
+
idsNotFound.add(id);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
idsDeleted.add(id);
|
|
81
|
+
return entity;
|
|
82
|
+
}, (error) => {
|
|
83
|
+
if (error.message.includes('not found')) {
|
|
84
|
+
idsNotFound.add(id);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
idsErrored.add(id);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
})));
|
|
91
|
+
await Promise.all(deletePromises);
|
|
92
|
+
return {
|
|
93
|
+
idsDeleted: Array.from(idsDeleted),
|
|
94
|
+
idsNotFound: Array.from(idsNotFound),
|
|
95
|
+
idsErrored: Array.from(idsErrored),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function deleteEntityById(entityName, id) {
|
|
99
|
+
const client = new sdk_1.Entities();
|
|
100
|
+
if (IS_DRY_RUN) {
|
|
101
|
+
return client.get({ entityName, id, ignore404: true }).catch(throwIfNot404);
|
|
102
|
+
}
|
|
103
|
+
return client.delete({ entityName, id, ignore404: true }).catch(throwIfNot404);
|
|
104
|
+
}
|
|
105
|
+
function throwIfNot404(e) {
|
|
106
|
+
const { statusCode } = parseSDKErrorMessage(e);
|
|
107
|
+
if (statusCode === 404) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
function parseSDKErrorMessage(e) {
|
|
113
|
+
const parsed = JSON.parse(e.details);
|
|
114
|
+
return {
|
|
115
|
+
statusCode: parsed.statusCode,
|
|
116
|
+
message: parsed.message,
|
|
117
|
+
};
|
|
118
|
+
}
|