@contrail/cli 3.5.0-alpha-1 → 3.6.0-alpha-2
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/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/propertySpecifications/propertySpecifications.d.ts +9 -2
- package/dist/helpers/propertySpecifications/propertySpecifications.js +34 -6
- package/dist/helpers/propertySpecifications/propertySpecifications.spec.js +124 -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-2 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='""' data=null # Unset description, empty notes, null data
|
|
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
|
});
|
|
@@ -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=\'""\' data=null # Unset description, empty notes, null data',
|
|
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;
|
|
@@ -10,17 +10,24 @@
|
|
|
10
10
|
*
|
|
11
11
|
* It is typically used as the argv in a command.
|
|
12
12
|
*
|
|
13
|
+
* Special handling for empty values and null:
|
|
14
|
+
* - slug= (empty value) → property is set to undefined (unset)
|
|
15
|
+
* - slug='""' or slug="''" → property is set to empty string ""
|
|
16
|
+
* - slug=null → property is set to null
|
|
17
|
+
* - slug="null" → property is set to the string "null"
|
|
18
|
+
*
|
|
13
19
|
* Examples:
|
|
14
20
|
*
|
|
15
21
|
* contrail entities get color -- id=123
|
|
16
22
|
* contrail entities update color 123 -- name=obsidian hex=000000
|
|
23
|
+
* contrail entities update color 123 -- description= notes='""' data=null
|
|
17
24
|
* contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
|
|
18
25
|
*/
|
|
19
26
|
export type PropertySpecification = {
|
|
20
27
|
slug: string;
|
|
21
|
-
value: string;
|
|
28
|
+
value: string | undefined | null;
|
|
22
29
|
};
|
|
23
|
-
export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[]>;
|
|
30
|
+
export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[] | undefined | null>;
|
|
24
31
|
export type PropertySpecificationString = `${string}=${string}`;
|
|
25
32
|
export declare function parsePropertySpecification(spec: PropertySpecificationString): PropertySpecification;
|
|
26
33
|
export declare function parsePropertySpecificationsIntoMap(propertySpecifications: PropertySpecificationString[]): PropertySpecificationBySlug;
|
|
@@ -11,15 +11,25 @@
|
|
|
11
11
|
*
|
|
12
12
|
* It is typically used as the argv in a command.
|
|
13
13
|
*
|
|
14
|
+
* Special handling for empty values and null:
|
|
15
|
+
* - slug= (empty value) → property is set to undefined (unset)
|
|
16
|
+
* - slug='""' or slug="''" → property is set to empty string ""
|
|
17
|
+
* - slug=null → property is set to null
|
|
18
|
+
* - slug="null" → property is set to the string "null"
|
|
19
|
+
*
|
|
14
20
|
* Examples:
|
|
15
21
|
*
|
|
16
22
|
* contrail entities get color -- id=123
|
|
17
23
|
* contrail entities update color 123 -- name=obsidian hex=000000
|
|
24
|
+
* contrail entities update color 123 -- description= notes='""' data=null
|
|
18
25
|
* contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
|
|
19
26
|
*/
|
|
20
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
28
|
exports.isValidPropertySpecification = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
|
|
22
29
|
const dateRanges_1 = require("./dateRanges");
|
|
30
|
+
// Special tokens for empty strings
|
|
31
|
+
const DOUBLE_QUOTED_EMPTY = '""';
|
|
32
|
+
const SINGLE_QUOTED_EMPTY = "''"; // eslint-disable-line quotes
|
|
23
33
|
function parsePropertySpecification(spec) {
|
|
24
34
|
const equalsIndex = spec.indexOf('=');
|
|
25
35
|
if (equalsIndex === -1) {
|
|
@@ -27,10 +37,28 @@ function parsePropertySpecification(spec) {
|
|
|
27
37
|
}
|
|
28
38
|
const slug = spec.substring(0, equalsIndex);
|
|
29
39
|
const value = spec.substring(equalsIndex + 1);
|
|
30
|
-
if (!slug
|
|
40
|
+
if (!slug) {
|
|
31
41
|
throw new Error(`Invalid property specification: ${spec}`);
|
|
32
42
|
}
|
|
33
|
-
|
|
43
|
+
// Handle special cases for empty strings and null:
|
|
44
|
+
// - x= means undefined (unset the property)
|
|
45
|
+
// - x='""' or x="''" means empty string ""
|
|
46
|
+
// - x=null means null value
|
|
47
|
+
// - x="null" means the string "null"
|
|
48
|
+
let finalValue;
|
|
49
|
+
if (value === '') {
|
|
50
|
+
finalValue = undefined;
|
|
51
|
+
}
|
|
52
|
+
else if (value === DOUBLE_QUOTED_EMPTY || value === SINGLE_QUOTED_EMPTY) {
|
|
53
|
+
finalValue = '';
|
|
54
|
+
}
|
|
55
|
+
else if (value === 'null') {
|
|
56
|
+
finalValue = null;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
finalValue = value;
|
|
60
|
+
}
|
|
61
|
+
return { slug, value: finalValue };
|
|
34
62
|
}
|
|
35
63
|
exports.parsePropertySpecification = parsePropertySpecification;
|
|
36
64
|
function parsePropertySpecificationsIntoMap(propertySpecifications) {
|
|
@@ -43,8 +71,8 @@ function parsePropertySpecificationsIntoMap(propertySpecifications) {
|
|
|
43
71
|
continue;
|
|
44
72
|
}
|
|
45
73
|
const { slug, value } = parsePropertySpecification(spec);
|
|
46
|
-
if (slug === 'roles') {
|
|
47
|
-
// Special handling for roles, which can be an array
|
|
74
|
+
if (slug === 'roles' && value !== undefined) {
|
|
75
|
+
// Special handling for roles, which can be an array (only if value is not undefined)
|
|
48
76
|
const rolesArray = value.split(',').map((role) => role.trim());
|
|
49
77
|
propertySpecBySlug.set(slug, rolesArray);
|
|
50
78
|
continue;
|
|
@@ -60,8 +88,8 @@ function isValidPropertySpecification(maybeSpec) {
|
|
|
60
88
|
(0, dateRanges_1.parseDateRangeSpecification)(maybeSpec);
|
|
61
89
|
return true;
|
|
62
90
|
}
|
|
63
|
-
const { slug
|
|
64
|
-
return !!slug
|
|
91
|
+
const { slug } = parsePropertySpecification(maybeSpec);
|
|
92
|
+
return !!slug;
|
|
65
93
|
}
|
|
66
94
|
catch (e) {
|
|
67
95
|
return false;
|
|
@@ -39,13 +39,66 @@ 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
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('should parse property specification with null value', () => {
|
|
82
|
+
const spec = 'description=null';
|
|
83
|
+
const result = (0, propertySpecifications_1.parsePropertySpecification)(spec);
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
slug: 'description',
|
|
86
|
+
value: null,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should distinguish between null and the string "null"', () => {
|
|
90
|
+
const nullSpec = 'field=null';
|
|
91
|
+
const stringNullSpec = 'field="null"';
|
|
92
|
+
const nullResult = (0, propertySpecifications_1.parsePropertySpecification)(nullSpec);
|
|
93
|
+
const stringNullResult = (0, propertySpecifications_1.parsePropertySpecification)(stringNullSpec);
|
|
94
|
+
expect(nullResult).toEqual({
|
|
95
|
+
slug: 'field',
|
|
96
|
+
value: null,
|
|
97
|
+
});
|
|
98
|
+
expect(stringNullResult).toEqual({
|
|
99
|
+
slug: 'field',
|
|
100
|
+
value: '"null"',
|
|
101
|
+
});
|
|
49
102
|
});
|
|
50
103
|
it('should throw error for spec without equals sign', () => {
|
|
51
104
|
const spec = 'invalidspec';
|
|
@@ -118,9 +171,26 @@ describe('parsePropertySpecificationsIntoMap', () => {
|
|
|
118
171
|
expect(result.size).toBe(1);
|
|
119
172
|
});
|
|
120
173
|
it('should handle property specifications with empty values', () => {
|
|
121
|
-
const specs = ['name=john', 'description=', 'notes='];
|
|
122
|
-
|
|
123
|
-
expect(
|
|
174
|
+
const specs = ['name=john', 'description=', 'notes=""'];
|
|
175
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
176
|
+
expect(result.get('name')).toBe('john');
|
|
177
|
+
expect(result.get('description')).toBeUndefined();
|
|
178
|
+
expect(result.get('notes')).toBe('');
|
|
179
|
+
expect(result.size).toBe(3);
|
|
180
|
+
});
|
|
181
|
+
it('should handle property specifications with null values', () => {
|
|
182
|
+
const specs = [
|
|
183
|
+
'name=john',
|
|
184
|
+
'description=null',
|
|
185
|
+
'notes="null"',
|
|
186
|
+
'empty=',
|
|
187
|
+
];
|
|
188
|
+
const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
|
|
189
|
+
expect(result.get('name')).toBe('john');
|
|
190
|
+
expect(result.get('description')).toBeNull();
|
|
191
|
+
expect(result.get('notes')).toBe('"null"');
|
|
192
|
+
expect(result.get('empty')).toBeUndefined();
|
|
193
|
+
expect(result.size).toBe(4);
|
|
124
194
|
});
|
|
125
195
|
it('should throw error for invalid property specifications', () => {
|
|
126
196
|
const specs = [
|
|
@@ -141,9 +211,9 @@ describe('isValidPropertySpecification', () => {
|
|
|
141
211
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('property-name=value')).toBe(true);
|
|
142
212
|
});
|
|
143
213
|
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(
|
|
214
|
+
// Now implementation should accept empty values
|
|
215
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('description=')).toBe(true);
|
|
216
|
+
expect((0, propertySpecifications_1.isValidPropertySpecification)('notes=')).toBe(true);
|
|
147
217
|
});
|
|
148
218
|
it('should return false for invalid property specifications', () => {
|
|
149
219
|
expect((0, propertySpecifications_1.isValidPropertySpecification)('invalid')).toBe(false);
|
|
@@ -195,6 +265,35 @@ describe('integration tests', () => {
|
|
|
195
265
|
expect(map.get('id')).toBe('123');
|
|
196
266
|
expect(map.size).toBe(2);
|
|
197
267
|
});
|
|
268
|
+
it('should handle command line parsing issue with empty strings', () => {
|
|
269
|
+
// When user types: contrail entities update color 123 -- description="" notes=''
|
|
270
|
+
// The shell may convert this to: ['description=', 'notes='] or ['description=""', 'notes=\'\'']
|
|
271
|
+
// depending on the shell and escaping
|
|
272
|
+
// Test case 1: Shell strips quotes (common case)
|
|
273
|
+
const shellStrippedSpecs = ['description=', 'notes='];
|
|
274
|
+
const shellStrippedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(shellStrippedSpecs);
|
|
275
|
+
expect(shellStrippedMap.get('description')).toBeUndefined(); // Empty = undefined
|
|
276
|
+
expect(shellStrippedMap.get('notes')).toBeUndefined(); // Empty = undefined
|
|
277
|
+
// Test case 2: Shell preserves quotes (desired case for empty strings)
|
|
278
|
+
const quotedSpecs = ['description=""'];
|
|
279
|
+
const quotedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(quotedSpecs);
|
|
280
|
+
expect(quotedMap.get('description')).toBe(''); // Quoted empty = empty string
|
|
281
|
+
// Test case 3: Mixed usage with null
|
|
282
|
+
const mixedSpecs = [
|
|
283
|
+
'name=test',
|
|
284
|
+
'description=',
|
|
285
|
+
'notes=""',
|
|
286
|
+
'data=null',
|
|
287
|
+
'status="null"',
|
|
288
|
+
];
|
|
289
|
+
const mixedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(mixedSpecs);
|
|
290
|
+
expect(mixedMap.get('name')).toBe('test');
|
|
291
|
+
expect(mixedMap.get('description')).toBeUndefined(); // Unset
|
|
292
|
+
expect(mixedMap.get('notes')).toBe(''); // Empty string
|
|
293
|
+
expect(mixedMap.get('data')).toBeNull(); // null value
|
|
294
|
+
expect(mixedMap.get('status')).toBe('"null"'); // String "null"
|
|
295
|
+
expect(mixedMap.size).toBe(5);
|
|
296
|
+
});
|
|
198
297
|
});
|
|
199
298
|
describe('edge cases and limitations', () => {
|
|
200
299
|
it('should handle multiple equals signs correctly', () => {
|
|
@@ -263,4 +362,20 @@ describe('edge cases and limitations', () => {
|
|
|
263
362
|
value: 'value',
|
|
264
363
|
});
|
|
265
364
|
});
|
|
365
|
+
it('should document all special value types', () => {
|
|
366
|
+
// This test documents all the special value types and their parsing behavior
|
|
367
|
+
const specs = [
|
|
368
|
+
{ input: 'a=value', expected: 'value', description: 'regular string' },
|
|
369
|
+
{ input: 'b=', expected: undefined, description: 'undefined (unset)' },
|
|
370
|
+
{ input: 'c=""', expected: '', description: 'empty string' },
|
|
371
|
+
{ input: 'd=null', expected: null, description: 'null value' },
|
|
372
|
+
{ input: 'e="null"', expected: '"null"', description: 'string "null"' },
|
|
373
|
+
{ input: 'f=123', expected: '123', description: 'numeric string' },
|
|
374
|
+
{ input: 'g=true', expected: 'true', description: 'boolean string' },
|
|
375
|
+
];
|
|
376
|
+
for (const { input, expected } of specs) {
|
|
377
|
+
const result = (0, propertySpecifications_1.parsePropertySpecification)(input);
|
|
378
|
+
expect(result.value).toBe(expected);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
266
381
|
});
|
|
@@ -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-2",
|
|
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='\"\"' data=null # Unset description, empty notes, null data"
|
|
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-2",
|
|
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",
|