@contrail/cli 3.5.0-alpha-1 → 3.6.0-alpha-3
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 +3 -1
- package/dist/auth/login.js +22 -9
- package/dist/commands/app/extension/deploy.js +4 -2
- package/dist/commands/entities/create.spec.js +5 -3
- package/dist/commands/entities/get.js +8 -1
- package/dist/commands/entities/update.js +17 -14
- package/dist/commands/types/uploadUsageInfo.js +14 -2
- package/dist/helpers/app/app-deployer.js +8 -4
- package/dist/helpers/app/extension-package-deployer.js +1 -1
- package/dist/helpers/config/config.js +13 -7
- package/dist/helpers/entities/batchFetch.d.ts +2 -2
- package/dist/helpers/entities/batchFetch.js +9 -1
- package/dist/helpers/propertySpecifications/propertySpecifications.d.ts +7 -2
- package/dist/helpers/propertySpecifications/propertySpecifications.js +27 -6
- package/dist/helpers/propertySpecifications/propertySpecifications.spec.js +64 -9
- package/dist/helpers/types/type-uploader.js +7 -0
- package/dist/interfaces.d.ts +1 -0
- package/dist/templates/typescript/README.md +3 -1
- package/dist/templates/typescript/assets/sample.json +1 -1
- package/dist/templates/typescript/eslintrc.json +1 -4
- package/dist/templates/typescript/tsconfig.json +5 -21
- package/oclif.manifest.json +3 -2
- package/package.json +2 -2
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.
|
|
20
|
+
@contrail/cli/3.6.0-alpha-3 darwin-arm64 node-v22.19.0
|
|
21
21
|
$ contrail --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ contrail COMMAND
|
|
@@ -682,6 +682,8 @@ DESCRIPTION
|
|
|
682
682
|
|
|
683
683
|
EXAMPLES
|
|
684
684
|
$ contrail entities update color 123 -- name=obsidian hex=000000
|
|
685
|
+
|
|
686
|
+
$ contrail entities update color 123 -- description= notes='""' # Unset description, set notes to empty string
|
|
685
687
|
```
|
|
686
688
|
|
|
687
689
|
## `contrail file upload FILELOCATION CONTENTTYPE`
|
package/dist/auth/login.js
CHANGED
|
@@ -9,6 +9,7 @@ const config_1 = require("../helpers/config/config");
|
|
|
9
9
|
const logger_1 = tslib_1.__importDefault(require("../logger"));
|
|
10
10
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
11
11
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
12
|
+
const common_1 = require("../common");
|
|
12
13
|
const CONFIGURATION = {
|
|
13
14
|
userPoolId: 'us-east-1_qIaNZdUfm',
|
|
14
15
|
clientId: process.env.CONTRAIL_COGNITO_CLIENT_ID || '7knkt34gfacbgd0ghnuspefdfg',
|
|
@@ -37,10 +38,21 @@ async function authUser() {
|
|
|
37
38
|
};
|
|
38
39
|
(0, sdk_1.setConfig)(sdk_1.Auth.config);
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
// hacky retry policy
|
|
42
|
+
return await refreshCurrentUserOrg().catch(async () => {
|
|
43
|
+
await (0, common_1.delay)(100);
|
|
44
|
+
return await refreshCurrentUserOrg().catch(async () => {
|
|
45
|
+
await (0, common_1.delay)(1000);
|
|
46
|
+
return await refreshCurrentUserOrg();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
41
49
|
}
|
|
42
50
|
catch (e) {
|
|
43
|
-
|
|
51
|
+
if (isNetworkConnectionError(e)) {
|
|
52
|
+
logger_1.default.error("Encountered a network connection issue. Please ensure you're connected to the internet and try again");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
logger_1.default.info(`Failed to refresh token: ${e}. Requesting auth credentials...`);
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
return await authenticateWithUsernamePassword(config);
|
|
@@ -71,7 +83,8 @@ async function authenticateWithApiKey(config, apiKey, orgSlug) {
|
|
|
71
83
|
};
|
|
72
84
|
(0, sdk_1.setConfig)(sdk_1.Auth.config);
|
|
73
85
|
await (0, sdk_1.getCurrentUser)();
|
|
74
|
-
|
|
86
|
+
await (0, config_1.storeConfig)(sdk_1.Auth.config);
|
|
87
|
+
return Object.assign({}, sdk_1.Auth.config);
|
|
75
88
|
}
|
|
76
89
|
async function authenticateWithUsernamePassword(config) {
|
|
77
90
|
const questions = [];
|
|
@@ -159,13 +172,14 @@ async function refreshCurrentUserOrg() {
|
|
|
159
172
|
console.log('fetched new access token from refresh token', newVibeAccessToken);
|
|
160
173
|
}
|
|
161
174
|
catch (e) {
|
|
162
|
-
throw Error(
|
|
175
|
+
throw Error(`Could not get refresh token: ${e}`);
|
|
163
176
|
}
|
|
177
|
+
await (0, config_1.storeConfig)(sdk_1.Auth.config);
|
|
164
178
|
}
|
|
165
179
|
// TODO: To uncomment when telemetry backend is developed.
|
|
166
180
|
// const current_Version = getVersionFromPackageJson();
|
|
167
181
|
// await checkTelemetry(current_Version);
|
|
168
|
-
return
|
|
182
|
+
return Object.assign({}, sdk_1.Auth.config);
|
|
169
183
|
}
|
|
170
184
|
async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiGateway) {
|
|
171
185
|
const url = `${apiGateway}/auth/cognito/`;
|
|
@@ -206,8 +220,7 @@ function getVersionFromPackageJson() {
|
|
|
206
220
|
return packageJson.version;
|
|
207
221
|
}
|
|
208
222
|
exports.getVersionFromPackageJson = getVersionFromPackageJson;
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
return configCopy;
|
|
223
|
+
function isNetworkConnectionError(e) {
|
|
224
|
+
const cause = e?.cause;
|
|
225
|
+
return cause?.code === 'ENOTFOUND' || cause?.code === 'ECONNREFUSED';
|
|
213
226
|
}
|
|
@@ -40,10 +40,12 @@ class DeployExtension extends basecommand_1.BaseCommand {
|
|
|
40
40
|
await this.validateExtensionDeclaration(appDeclaration, extensionIdentifier);
|
|
41
41
|
const extensionDir = this.getExtensionDirectory(extensionIdentifier);
|
|
42
42
|
// 2. Package and upload extension
|
|
43
|
-
const createdPackage = await (0, extension_package_deployer_1.createExtensionPackage)(app.id, extensionDir, {
|
|
43
|
+
const createdPackage = await (0, extension_package_deployer_1.createExtensionPackage)(app.id, extensionDir, {
|
|
44
|
+
debug: this.flags.debug,
|
|
45
|
+
});
|
|
44
46
|
// 3. Poll until package is ready (timeout after 1 minute)
|
|
45
47
|
const readyPackage = await this.pollUntilPackageIsReady(createdPackage);
|
|
46
|
-
logger_1.default.info(
|
|
48
|
+
logger_1.default.info('Extension package is ready...Checking for extension deployment');
|
|
47
49
|
logger_1.default.debug(JSON.stringify(readyPackage, null, 2));
|
|
48
50
|
const deployment = await this.pollUntilDeploymentIsReady(readyPackage.deploymentId);
|
|
49
51
|
logger_1.default.info(`${chalk_1.default.green.bold('SUCCESS')} Extension package is ready.`);
|
|
@@ -288,7 +288,9 @@ describe('Entity Creation E2E Happy Path', () => {
|
|
|
288
288
|
it('should throw error for invalid specifications', () => {
|
|
289
289
|
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['invalid-spec'])).toThrow('Invalid property specification: invalid-spec');
|
|
290
290
|
expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['=value'])).toThrow('Invalid property specification: =value');
|
|
291
|
-
|
|
291
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(['key=']);
|
|
292
|
+
expect(result.get('key')).toBeUndefined();
|
|
293
|
+
expect(result.size).toBe(1);
|
|
292
294
|
});
|
|
293
295
|
});
|
|
294
296
|
describe('isValidPropertySpecification', () => {
|
|
@@ -297,11 +299,11 @@ describe('Entity Creation E2E Happy Path', () => {
|
|
|
297
299
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('id=123')).toBe(true);
|
|
298
300
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('description=Long description with spaces')).toBe(true);
|
|
299
301
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('roles=option,family')).toBe(true);
|
|
302
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('key=')).toBe(true);
|
|
300
303
|
});
|
|
301
|
-
it('should reject invalid property specifications', () => {
|
|
304
|
+
it('should reject most invalid property specifications', () => {
|
|
302
305
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('invalid')).toBe(false);
|
|
303
306
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('=')).toBe(false);
|
|
304
|
-
expect((0, propertySpecifications_1.isValidPropertySpecification)('key=')).toBe(false);
|
|
305
307
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('=value')).toBe(false);
|
|
306
308
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('')).toBe(false);
|
|
307
309
|
});
|
|
@@ -8,6 +8,7 @@ const sdk_1 = require("@contrail/sdk");
|
|
|
8
8
|
const login_1 = require("../../auth/login");
|
|
9
9
|
const types_1 = require("../../api/types");
|
|
10
10
|
const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
|
|
11
|
+
const batchFetch_1 = require("../../helpers/entities/batchFetch");
|
|
11
12
|
let SHOULD_USE_PAGINATED_FIND = false;
|
|
12
13
|
let MAX_NUMBER_OF_ENTITIES_TO_FETCH = undefined;
|
|
13
14
|
class GetEntity extends basecommand_1.BaseCommand {
|
|
@@ -162,7 +163,13 @@ async function find(entityName, options = {}) {
|
|
|
162
163
|
if (propertySlugs && propertySlugs.length > 0) {
|
|
163
164
|
queryOptions.customParams = { propertySlugs: propertySlugs.join(',') };
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
+
const payload = id ? { id, ...queryOptions } : { criteria, ...queryOptions };
|
|
167
|
+
if ((0, batchFetch_1.canBatchFetchEntity)(entityName)) {
|
|
168
|
+
return (0, batchFetch_1.batchFetch)(entityName, payload);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
return client.get(payload);
|
|
172
|
+
}
|
|
166
173
|
}
|
|
167
174
|
async function* streamPaginatedFind(typePath, options = {}) {
|
|
168
175
|
const client = new sdk_1.Entities();
|
|
@@ -10,7 +10,10 @@ const propertySpecifications_1 = require("../../helpers/propertySpecifications/p
|
|
|
10
10
|
const file_helpers_1 = require("../../helpers/file-helpers");
|
|
11
11
|
class UpdateEntity extends basecommand_1.BaseCommand {
|
|
12
12
|
static description = '(BETA) Update an entity';
|
|
13
|
-
static examples = [
|
|
13
|
+
static examples = [
|
|
14
|
+
'contrail entities update color 123 -- name=obsidian hex=000000',
|
|
15
|
+
'contrail entities update color 123 -- description= notes=\'""\' # Unset description, set notes to empty string',
|
|
16
|
+
];
|
|
14
17
|
static flags = {
|
|
15
18
|
...basecommand_1.BaseCommand.baseFlags,
|
|
16
19
|
skipHydrateRelationships: core_1.Flags.boolean({
|
|
@@ -70,20 +73,20 @@ class UpdateEntity extends basecommand_1.BaseCommand {
|
|
|
70
73
|
acc[key] = value;
|
|
71
74
|
return acc;
|
|
72
75
|
}, {});
|
|
76
|
+
async function updateEntity(id) {
|
|
77
|
+
const promise = isDryRun
|
|
78
|
+
? mockUpdateById(entityName, id, changes, { skipHydrateRelationships })
|
|
79
|
+
: updateEntityById(entityName, id, changes, { skipHydrateRelationships });
|
|
80
|
+
promise
|
|
81
|
+
.catch(() => {
|
|
82
|
+
results.idsFailedToUpdate.add(id);
|
|
83
|
+
})
|
|
84
|
+
.then(() => {
|
|
85
|
+
results.idsSuccessfullyUpdated.add(id);
|
|
86
|
+
});
|
|
87
|
+
return promise;
|
|
88
|
+
}
|
|
73
89
|
try {
|
|
74
|
-
async function updateEntity(id) {
|
|
75
|
-
const promise = isDryRun
|
|
76
|
-
? mockUpdateById(entityName, id, changes, { skipHydrateRelationships })
|
|
77
|
-
: updateEntityById(entityName, id, changes, { skipHydrateRelationships });
|
|
78
|
-
promise
|
|
79
|
-
.catch(() => {
|
|
80
|
-
results.idsFailedToUpdate.add(id);
|
|
81
|
-
})
|
|
82
|
-
.then(() => {
|
|
83
|
-
results.idsSuccessfullyUpdated.add(id);
|
|
84
|
-
});
|
|
85
|
-
return promise;
|
|
86
|
-
}
|
|
87
90
|
const limit = (0, p_limit_1.default)(5); // Limit concurrency to 5
|
|
88
91
|
const responses = await Promise.all(idsToUpdate.map((id) => limit(() => updateEntity(id))));
|
|
89
92
|
const messagePrefix = isDryRun
|
|
@@ -115,8 +115,20 @@ async function uploadUsageInfoObjects(loadedUsageInfo) {
|
|
|
115
115
|
entityName: 'type-property-usage-info',
|
|
116
116
|
id: found.id,
|
|
117
117
|
object: {
|
|
118
|
-
editable: loadedUI
|
|
119
|
-
used: loadedUI
|
|
118
|
+
editable: loadedUI?.editable,
|
|
119
|
+
used: loadedUI?.used,
|
|
120
|
+
formulaFunction: loadedUI?.formulaFunction,
|
|
121
|
+
validationFunction: loadedUI?.validationFunction,
|
|
122
|
+
numberFormat: loadedUI?.numberFormat,
|
|
123
|
+
defaultValue: loadedUI?.defaultValue,
|
|
124
|
+
label: loadedUI?.label,
|
|
125
|
+
required: loadedUI?.required,
|
|
126
|
+
searchable: loadedUI?.searchable,
|
|
127
|
+
isUniqueProperty: loadedUI?.isUniqueProperty,
|
|
128
|
+
isUniquePropertyInFamily: loadedUI?.isUniquePropertyInFamily,
|
|
129
|
+
copyBehavior: loadedUI?.copyBehavior,
|
|
130
|
+
copyDefault: loadedUI?.copyDefault,
|
|
131
|
+
propertyLevel: loadedUI?.propertyLevel,
|
|
120
132
|
},
|
|
121
133
|
});
|
|
122
134
|
}
|
|
@@ -107,6 +107,7 @@ class AppDeployer {
|
|
|
107
107
|
if (appDeclaration?.accessGrants) {
|
|
108
108
|
accessGrants = await app_template_parsers_1.AccessGrantsParser.buildAccessGrants(appDeclaration);
|
|
109
109
|
}
|
|
110
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
110
111
|
if (appDeclaration.hasOwnProperty('dockerImageRef')) {
|
|
111
112
|
throw new Error('Docker apps are no longer supported. Please use a node app.');
|
|
112
113
|
}
|
|
@@ -201,7 +202,11 @@ async function createNodePackage(appIdentifier, debug, dryRun) {
|
|
|
201
202
|
logger_1.default.info('Installing production dependencies done.');
|
|
202
203
|
logger_1.default.info('Building package...');
|
|
203
204
|
try {
|
|
204
|
-
|
|
205
|
+
// Check for tsconfig.build.json first, fallback to tsconfig.json
|
|
206
|
+
const buildConfigPath = fs_1.default.existsSync('tsconfig.build.json')
|
|
207
|
+
? 'tsconfig.build.json'
|
|
208
|
+
: 'tsconfig.json';
|
|
209
|
+
child_process_1.default.execSync(`npx tsc --project ${buildConfigPath} --outDir tmp/dist`);
|
|
205
210
|
}
|
|
206
211
|
catch (e) {
|
|
207
212
|
logger_1.default.error(`${chalk_1.default.red.bold('APP')} Failed to build package.`);
|
|
@@ -228,7 +233,7 @@ async function createNodePackage(appIdentifier, debug, dryRun) {
|
|
|
228
233
|
return {};
|
|
229
234
|
}
|
|
230
235
|
logger_1.default.info('Uploading package to S3...');
|
|
231
|
-
|
|
236
|
+
const apps = await entitiesClient.get({ entityName: 'app' });
|
|
232
237
|
const thisApp = apps.find((x) => x.identifier === appIdentifier);
|
|
233
238
|
if (!thisApp) {
|
|
234
239
|
throw new Error('App not found');
|
|
@@ -238,8 +243,7 @@ async function createNodePackage(appIdentifier, debug, dryRun) {
|
|
|
238
243
|
object: { appId: thisApp.id },
|
|
239
244
|
});
|
|
240
245
|
// upload an s3 object using a signed url with an https put request
|
|
241
|
-
const
|
|
242
|
-
const postBody = Buffer.from(readStream);
|
|
246
|
+
const postBody = fs_1.default.readFileSync('tmp.zip');
|
|
243
247
|
// await axios.put(uploadUrlObject.signedUrl, postBody.toString());
|
|
244
248
|
await axios_1.default.request({
|
|
245
249
|
method: 'PUT',
|
|
@@ -119,7 +119,7 @@ async function createExtensionPackage(appId, extensionDir, options) {
|
|
|
119
119
|
await uploadExtensionPackage(appExtensionPackage, tmpDir, options);
|
|
120
120
|
logger_1.default.info(`${chalk_1.default.green.bold('COMPLETE')} Extension packaged and uploaded.`);
|
|
121
121
|
if (options.debug) {
|
|
122
|
-
logger_1.default.debug(
|
|
122
|
+
logger_1.default.debug('debug mode enabled. Not cleaning up tmp dir.');
|
|
123
123
|
}
|
|
124
124
|
else {
|
|
125
125
|
cleanUp(tmpDir);
|
|
@@ -41,13 +41,13 @@ exports.initializeVibeIQConfig = initializeVibeIQConfig;
|
|
|
41
41
|
async function readVibeIQConfig() {
|
|
42
42
|
let fileContent;
|
|
43
43
|
try {
|
|
44
|
-
fileContent =
|
|
44
|
+
fileContent = (await node_fs_1.promises.readFile(configFileLocation)).toString();
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
47
|
logger_1.default.debug(`failed to open config file (${configFileLocation}) for reading.`);
|
|
48
48
|
logger_1.default.debug('trying to initialize config file');
|
|
49
49
|
await initializeVibeIQConfig();
|
|
50
|
-
fileContent =
|
|
50
|
+
fileContent = (await node_fs_1.promises.readFile(configFileLocation)).toString();
|
|
51
51
|
}
|
|
52
52
|
try {
|
|
53
53
|
return JSON.parse(fileContent);
|
|
@@ -92,7 +92,7 @@ async function writeConfig(config, configName = null) {
|
|
|
92
92
|
const fileContent = await readVibeIQConfig();
|
|
93
93
|
configFileName = fileContent.configInUse;
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
atomicFileWriteSync((0, node_path_1.join)(configDirectoryLocation, `${configFileName}.json`), JSON.stringify(config, null, 2));
|
|
96
96
|
}
|
|
97
97
|
catch (error) {
|
|
98
98
|
logger_1.default.error('Could not open config file for writing.');
|
|
@@ -100,16 +100,22 @@ async function writeConfig(config, configName = null) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
exports.writeConfig = writeConfig;
|
|
103
|
+
function atomicFileWriteSync(filePath, data) {
|
|
104
|
+
const randomId = Math.random().toString(36).substring(2, 15);
|
|
105
|
+
const tmpFilepath = `${filePath}.tmp-${randomId}`;
|
|
106
|
+
(0, node_fs_1.writeFileSync)(tmpFilepath, data);
|
|
107
|
+
(0, node_fs_1.renameSync)(tmpFilepath, filePath);
|
|
108
|
+
}
|
|
103
109
|
async function getConfig() {
|
|
104
110
|
let fileContent;
|
|
105
111
|
try {
|
|
106
|
-
fileContent =
|
|
112
|
+
fileContent = (await node_fs_1.promises.readFile(configFileLocation)).toString();
|
|
107
113
|
}
|
|
108
114
|
catch {
|
|
109
115
|
logger_1.default.debug(`failed to open config file (${configFileLocation}) for reading.`);
|
|
110
116
|
logger_1.default.debug('trying to initialize config file');
|
|
111
117
|
await initializeVibeIQConfig();
|
|
112
|
-
fileContent =
|
|
118
|
+
fileContent = (await node_fs_1.promises.readFile(configFileLocation)).toString();
|
|
113
119
|
}
|
|
114
120
|
try {
|
|
115
121
|
const configFileJSON = JSON.parse(fileContent);
|
|
@@ -156,11 +162,11 @@ async function getConfigContent(fileName) {
|
|
|
156
162
|
const configFilePath = (0, node_path_1.join)(configDirectoryLocation, `${fileName}.json`);
|
|
157
163
|
let fileContent;
|
|
158
164
|
try {
|
|
159
|
-
fileContent =
|
|
165
|
+
fileContent = (await node_fs_1.promises.readFile(configFilePath)).toString();
|
|
160
166
|
return JSON.parse(fileContent);
|
|
161
167
|
}
|
|
162
168
|
catch (error) {
|
|
163
|
-
logger_1.default.error(`Failed to open/parse config file (${fileName}) might be malformed or not found
|
|
169
|
+
logger_1.default.error(`Failed to open/parse config file (${fileName}) might be malformed or not found. Path: ${configFilePath}, error: ${error}`);
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
exports.getConfigContent = getConfigContent;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AssortmentItem,
|
|
1
|
+
import { AssortmentItem, ProjectItem } from '@contrail/entity-types';
|
|
2
2
|
import { EntitiesClientGetOptions } from '@contrail/sdk';
|
|
3
|
+
export declare function canBatchFetchEntity(entityName: string): entityName is keyof BatchFetchableEntityMap;
|
|
3
4
|
export interface BatchFetchableEntityMap {
|
|
4
|
-
item: Item;
|
|
5
5
|
'project-item': ProjectItem;
|
|
6
6
|
'assortment-item': AssortmentItem;
|
|
7
7
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.batchFetch = void 0;
|
|
3
|
+
exports.batchFetch = exports.canBatchFetchEntity = void 0;
|
|
4
4
|
const sdk_1 = require("@contrail/sdk");
|
|
5
5
|
const util_1 = require("@contrail/util");
|
|
6
|
+
// Maps entity names to their types for batch fetching.
|
|
7
|
+
// This allows for type-safe batch fetching of entities
|
|
8
|
+
// such that the Type returned always matches the entity name.
|
|
9
|
+
const BATCH_FETCHABLE_ENTITIES = ['assortment-item', 'project-item'];
|
|
10
|
+
function canBatchFetchEntity(entityName) {
|
|
11
|
+
return BATCH_FETCHABLE_ENTITIES.includes(entityName);
|
|
12
|
+
}
|
|
13
|
+
exports.canBatchFetchEntity = canBatchFetchEntity;
|
|
6
14
|
async function batchFetch(entityName, options) {
|
|
7
15
|
const response = await requestBatchFetch(entityName, options);
|
|
8
16
|
return parseBatchFetchResponse(response);
|
|
@@ -10,17 +10,22 @@
|
|
|
10
10
|
*
|
|
11
11
|
* It is typically used as the argv in a command.
|
|
12
12
|
*
|
|
13
|
+
* Special handling for empty values:
|
|
14
|
+
* - slug= (empty value) → property is set to undefined (unset)
|
|
15
|
+
* - slug='""' or slug="''" → property is set to empty string ""
|
|
16
|
+
*
|
|
13
17
|
* Examples:
|
|
14
18
|
*
|
|
15
19
|
* contrail entities get color -- id=123
|
|
16
20
|
* contrail entities update color 123 -- name=obsidian hex=000000
|
|
21
|
+
* contrail entities update color 123 -- description= notes='""'
|
|
17
22
|
* contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
|
|
18
23
|
*/
|
|
19
24
|
export type PropertySpecification = {
|
|
20
25
|
slug: string;
|
|
21
|
-
value: string;
|
|
26
|
+
value: string | undefined;
|
|
22
27
|
};
|
|
23
|
-
export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[]>;
|
|
28
|
+
export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[] | undefined>;
|
|
24
29
|
export type PropertySpecificationString = `${string}=${string}`;
|
|
25
30
|
export declare function parsePropertySpecification(spec: PropertySpecificationString): PropertySpecification;
|
|
26
31
|
export declare function parsePropertySpecificationsIntoMap(propertySpecifications: PropertySpecificationString[]): PropertySpecificationBySlug;
|
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
*
|
|
12
12
|
* It is typically used as the argv in a command.
|
|
13
13
|
*
|
|
14
|
+
* Special handling for empty values:
|
|
15
|
+
* - slug= (empty value) → property is set to undefined (unset)
|
|
16
|
+
* - slug='""' or slug="''" → property is set to empty string ""
|
|
17
|
+
*
|
|
14
18
|
* Examples:
|
|
15
19
|
*
|
|
16
20
|
* contrail entities get color -- id=123
|
|
17
21
|
* contrail entities update color 123 -- name=obsidian hex=000000
|
|
22
|
+
* contrail entities update color 123 -- description= notes='""'
|
|
18
23
|
* contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
|
|
19
24
|
*/
|
|
20
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
26
|
exports.isValidPropertySpecification = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
|
|
22
27
|
const dateRanges_1 = require("./dateRanges");
|
|
28
|
+
// Special tokens for empty strings
|
|
29
|
+
const DOUBLE_QUOTED_EMPTY = '""';
|
|
30
|
+
const SINGLE_QUOTED_EMPTY = "''"; // eslint-disable-line quotes
|
|
23
31
|
function parsePropertySpecification(spec) {
|
|
24
32
|
const equalsIndex = spec.indexOf('=');
|
|
25
33
|
if (equalsIndex === -1) {
|
|
@@ -27,10 +35,23 @@ function parsePropertySpecification(spec) {
|
|
|
27
35
|
}
|
|
28
36
|
const slug = spec.substring(0, equalsIndex);
|
|
29
37
|
const value = spec.substring(equalsIndex + 1);
|
|
30
|
-
if (!slug
|
|
38
|
+
if (!slug) {
|
|
31
39
|
throw new Error(`Invalid property specification: ${spec}`);
|
|
32
40
|
}
|
|
33
|
-
|
|
41
|
+
// Handle special cases for empty strings:
|
|
42
|
+
// - x= means undefined (unset the property)
|
|
43
|
+
// - x='""' or x="''" means empty string ""
|
|
44
|
+
let finalValue;
|
|
45
|
+
if (value === '') {
|
|
46
|
+
finalValue = undefined;
|
|
47
|
+
}
|
|
48
|
+
else if (value === DOUBLE_QUOTED_EMPTY || value === SINGLE_QUOTED_EMPTY) {
|
|
49
|
+
finalValue = '';
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
finalValue = value;
|
|
53
|
+
}
|
|
54
|
+
return { slug, value: finalValue };
|
|
34
55
|
}
|
|
35
56
|
exports.parsePropertySpecification = parsePropertySpecification;
|
|
36
57
|
function parsePropertySpecificationsIntoMap(propertySpecifications) {
|
|
@@ -43,8 +64,8 @@ function parsePropertySpecificationsIntoMap(propertySpecifications) {
|
|
|
43
64
|
continue;
|
|
44
65
|
}
|
|
45
66
|
const { slug, value } = parsePropertySpecification(spec);
|
|
46
|
-
if (slug === 'roles') {
|
|
47
|
-
// Special handling for roles, which can be an array
|
|
67
|
+
if (slug === 'roles' && value !== undefined) {
|
|
68
|
+
// Special handling for roles, which can be an array (only if value is not undefined)
|
|
48
69
|
const rolesArray = value.split(',').map((role) => role.trim());
|
|
49
70
|
propertySpecBySlug.set(slug, rolesArray);
|
|
50
71
|
continue;
|
|
@@ -60,8 +81,8 @@ function isValidPropertySpecification(maybeSpec) {
|
|
|
60
81
|
(0, dateRanges_1.parseDateRangeSpecification)(maybeSpec);
|
|
61
82
|
return true;
|
|
62
83
|
}
|
|
63
|
-
const { slug
|
|
64
|
-
return !!slug
|
|
84
|
+
const { slug } = parsePropertySpecification(maybeSpec);
|
|
85
|
+
return !!slug;
|
|
65
86
|
}
|
|
66
87
|
catch (e) {
|
|
67
88
|
return false;
|
|
@@ -39,13 +39,44 @@ describe('parsePropertySpecification', () => {
|
|
|
39
39
|
const result = (0, propertySpecifications_1.parsePropertySpecification)(spec);
|
|
40
40
|
expect(result).toEqual({
|
|
41
41
|
slug: 'formula',
|
|
42
|
-
value: 'x=y+z',
|
|
42
|
+
value: 'x=y+z',
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
it('should parse property specification with empty value', () => {
|
|
46
46
|
const spec = 'description=';
|
|
47
|
-
|
|
48
|
-
expect(
|
|
47
|
+
const result = (0, propertySpecifications_1.parsePropertySpecification)(spec);
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
slug: 'description',
|
|
50
|
+
value: undefined,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('should distinguish between empty value and empty string', () => {
|
|
54
|
+
const emptySpec = 'description=';
|
|
55
|
+
const emptyStringSpec = 'description=""';
|
|
56
|
+
const emptyResult = (0, propertySpecifications_1.parsePropertySpecification)(emptySpec);
|
|
57
|
+
const emptyStringResult = (0, propertySpecifications_1.parsePropertySpecification)(emptyStringSpec);
|
|
58
|
+
expect(emptyResult).toEqual({
|
|
59
|
+
slug: 'description',
|
|
60
|
+
value: undefined,
|
|
61
|
+
});
|
|
62
|
+
expect(emptyStringResult).toEqual({
|
|
63
|
+
slug: 'description',
|
|
64
|
+
value: '',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('should handle both double and single quoted empty strings', () => {
|
|
68
|
+
const doubleQuotedSpec = 'field=""';
|
|
69
|
+
const singleQuotedSpec = "field=''"; // eslint-disable-line quotes
|
|
70
|
+
const doubleQuotedResult = (0, propertySpecifications_1.parsePropertySpecification)(doubleQuotedSpec);
|
|
71
|
+
const singleQuotedResult = (0, propertySpecifications_1.parsePropertySpecification)(singleQuotedSpec);
|
|
72
|
+
expect(doubleQuotedResult).toEqual({
|
|
73
|
+
slug: 'field',
|
|
74
|
+
value: '',
|
|
75
|
+
});
|
|
76
|
+
expect(singleQuotedResult).toEqual({
|
|
77
|
+
slug: 'field',
|
|
78
|
+
value: '',
|
|
79
|
+
});
|
|
49
80
|
});
|
|
50
81
|
it('should throw error for spec without equals sign', () => {
|
|
51
82
|
const spec = 'invalidspec';
|
|
@@ -118,9 +149,12 @@ describe('parsePropertySpecificationsIntoMap', () => {
|
|
|
118
149
|
expect(result.size).toBe(1);
|
|
119
150
|
});
|
|
120
151
|
it('should handle property specifications with empty values', () => {
|
|
121
|
-
const specs = ['name=john', 'description=', 'notes='];
|
|
122
|
-
|
|
123
|
-
expect(
|
|
152
|
+
const specs = ['name=john', 'description=', 'notes=""'];
|
|
153
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
154
|
+
expect(result.get('name')).toBe('john');
|
|
155
|
+
expect(result.get('description')).toBeUndefined();
|
|
156
|
+
expect(result.get('notes')).toBe(''); // Empty quotes are converted to empty string
|
|
157
|
+
expect(result.size).toBe(3);
|
|
124
158
|
});
|
|
125
159
|
it('should throw error for invalid property specifications', () => {
|
|
126
160
|
const specs = [
|
|
@@ -141,9 +175,9 @@ describe('isValidPropertySpecification', () => {
|
|
|
141
175
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('property-name=value')).toBe(true);
|
|
142
176
|
});
|
|
143
177
|
it('should return true for property specifications with empty values', () => {
|
|
144
|
-
//
|
|
145
|
-
expect((0, propertySpecifications_1.isValidPropertySpecification)('description=')).toBe(
|
|
146
|
-
expect((0, propertySpecifications_1.isValidPropertySpecification)('notes=')).toBe(
|
|
178
|
+
// Now implementation should accept empty values
|
|
179
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('description=')).toBe(true);
|
|
180
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('notes=')).toBe(true);
|
|
147
181
|
});
|
|
148
182
|
it('should return false for invalid property specifications', () => {
|
|
149
183
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('invalid')).toBe(false);
|
|
@@ -195,6 +229,27 @@ describe('integration tests', () => {
|
|
|
195
229
|
expect(map.get('id')).toBe('123');
|
|
196
230
|
expect(map.size).toBe(2);
|
|
197
231
|
});
|
|
232
|
+
it('should handle command line parsing issue with empty strings', () => {
|
|
233
|
+
// When user types: contrail entities update color 123 -- description="" notes=''
|
|
234
|
+
// The shell may convert this to: ['description=', 'notes='] or ['description=""', 'notes=\'\'']
|
|
235
|
+
// depending on the shell and escaping
|
|
236
|
+
// Test case 1: Shell strips quotes (common case)
|
|
237
|
+
const shellStrippedSpecs = ['description=', 'notes='];
|
|
238
|
+
const shellStrippedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(shellStrippedSpecs);
|
|
239
|
+
expect(shellStrippedMap.get('description')).toBeUndefined(); // Empty = undefined
|
|
240
|
+
expect(shellStrippedMap.get('notes')).toBeUndefined(); // Empty = undefined
|
|
241
|
+
// Test case 2: Shell preserves quotes (desired case for empty strings)
|
|
242
|
+
const quotedSpecs = ['description=""'];
|
|
243
|
+
const quotedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(quotedSpecs);
|
|
244
|
+
expect(quotedMap.get('description')).toBe(''); // Quoted empty = empty string
|
|
245
|
+
// Test case 3: Mixed usage
|
|
246
|
+
const mixedSpecs = ['name=test', 'description=', 'notes=""'];
|
|
247
|
+
const mixedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(mixedSpecs);
|
|
248
|
+
expect(mixedMap.get('name')).toBe('test');
|
|
249
|
+
expect(mixedMap.get('description')).toBeUndefined(); // Unset
|
|
250
|
+
expect(mixedMap.get('notes')).toBe(''); // Empty string
|
|
251
|
+
expect(mixedMap.size).toBe(3);
|
|
252
|
+
});
|
|
198
253
|
});
|
|
199
254
|
describe('edge cases and limitations', () => {
|
|
200
255
|
it('should handle multiple equals signs correctly', () => {
|
|
@@ -188,6 +188,7 @@ async function uploadReducedTypeProperties(type, currentPropertyTypes, newProper
|
|
|
188
188
|
validationFunction: newPropertyType.validationFunction,
|
|
189
189
|
numberFormat: newPropertyType.numberFormat,
|
|
190
190
|
sequenceSeedValue: newPropertyType.sequenceSeedValue,
|
|
191
|
+
copyBehavior: newPropertyType?.copyBehavior,
|
|
191
192
|
}),
|
|
192
193
|
headers: {
|
|
193
194
|
Accept: 'application/json',
|
|
@@ -199,6 +200,11 @@ async function uploadReducedTypeProperties(type, currentPropertyTypes, newProper
|
|
|
199
200
|
// !newPropertyType.inherited &&
|
|
200
201
|
// !typePropertiesEqual(found, newPropertyType, optionSetIdBySlug)
|
|
201
202
|
) {
|
|
203
|
+
// Skip platform-managed entities
|
|
204
|
+
if (newPropertyType.core) {
|
|
205
|
+
logger_1.default.debug(`Skipping platform-managed property: ${newPropertyType.slug}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
202
208
|
const proposedTP = {
|
|
203
209
|
isArchived: newPropertyType.isArchived ?? null,
|
|
204
210
|
propertyType: newPropertyType.propertyType ?? null,
|
|
@@ -223,6 +229,7 @@ async function uploadReducedTypeProperties(type, currentPropertyTypes, newProper
|
|
|
223
229
|
validationFunction: newPropertyType.validationFunction ?? null,
|
|
224
230
|
numberFormat: newPropertyType.numberFormat ?? null,
|
|
225
231
|
sequenceSeedValue: newPropertyType.sequenceSeedValue ?? null,
|
|
232
|
+
copyBehavior: newPropertyType.copyBehavior ?? null,
|
|
226
233
|
};
|
|
227
234
|
// logger.debug(
|
|
228
235
|
// `found type property option set: ${newPropertyType.typePropertyOptionSetSlug}, ${proposedTP.typePropertyOptionSetId}, `,
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
### Running your app
|
|
2
|
+
|
|
2
3
|
Run `npm run start` at the root directory of your app (where the `package.json` file is located).
|
|
3
4
|
|
|
4
5
|
### Publishing your app
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
Run `contrail app publish` to publish a new version of the app.
|
|
@@ -25,10 +25,7 @@
|
|
|
25
25
|
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
|
|
26
26
|
"no-trailing-spaces": "error",
|
|
27
27
|
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
|
28
|
-
"@typescript-eslint/no-empty-function": [
|
|
29
|
-
"error",
|
|
30
|
-
{ "allow": ["arrowFunctions"] }
|
|
31
|
-
],
|
|
28
|
+
"@typescript-eslint/no-empty-function": ["error", { "allow": ["arrowFunctions"] }],
|
|
32
29
|
"@typescript-eslint/no-explicit-any": "off",
|
|
33
30
|
"@typescript-eslint/no-var-requires": "warn",
|
|
34
31
|
"no-unused-vars": "off",
|
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
"emitDecoratorMetadata": true,
|
|
5
5
|
"esModuleInterop": true,
|
|
6
6
|
"experimentalDecorators": true,
|
|
7
|
-
"lib": [
|
|
8
|
-
"es5",
|
|
9
|
-
"ES2015",
|
|
10
|
-
"ES2016",
|
|
11
|
-
"ES2017",
|
|
12
|
-
"ES2020",
|
|
13
|
-
"dom",
|
|
14
|
-
"esnext.asynciterable"
|
|
15
|
-
],
|
|
7
|
+
"lib": ["es5", "ES2015", "ES2016", "ES2017", "ES2020", "dom", "esnext.asynciterable"],
|
|
16
8
|
"module": "CommonJS",
|
|
17
9
|
// "moduleResolution": "node",
|
|
18
10
|
"noImplicitAny": false,
|
|
@@ -22,16 +14,8 @@
|
|
|
22
14
|
"strict": false,
|
|
23
15
|
"strictPropertyInitialization": false,
|
|
24
16
|
"target": "ES2020",
|
|
25
|
-
"types": [
|
|
26
|
-
"node", "jest"
|
|
27
|
-
]
|
|
17
|
+
"types": ["node", "jest"]
|
|
28
18
|
},
|
|
29
|
-
"exclude": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
],
|
|
33
|
-
"include": [
|
|
34
|
-
"src",
|
|
35
|
-
"package.json"
|
|
36
|
-
]
|
|
37
|
-
}
|
|
19
|
+
"exclude": ["node_modules", "**/__tests__/*"],
|
|
20
|
+
"include": ["src", "package.json"]
|
|
21
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.6.0-alpha-3",
|
|
3
3
|
"commands": {
|
|
4
4
|
"app:create": {
|
|
5
5
|
"id": "app:create",
|
|
@@ -963,7 +963,8 @@
|
|
|
963
963
|
"pluginType": "core",
|
|
964
964
|
"aliases": [],
|
|
965
965
|
"examples": [
|
|
966
|
-
"contrail entities update color 123 -- name=obsidian hex=000000"
|
|
966
|
+
"contrail entities update color 123 -- name=obsidian hex=000000",
|
|
967
|
+
"contrail entities update color 123 -- description= notes='\"\"' # Unset description, set notes to empty string"
|
|
967
968
|
],
|
|
968
969
|
"flags": {
|
|
969
970
|
"logLevel": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0-alpha-3",
|
|
4
4
|
"description": "VibeIQ's Contrail Platform CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"contrail-cli": "./bin/run",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@contrail/entity-types": "^1.4.0-alpha",
|
|
26
|
-
"@contrail/org-config-migration": "^1.1.
|
|
26
|
+
"@contrail/org-config-migration": "^1.1.6",
|
|
27
27
|
"@contrail/sdk": "^1.6.0-alpha",
|
|
28
28
|
"@contrail/types": "^3.0.48",
|
|
29
29
|
"@contrail/util": "^1.1.15",
|