@contrail/cli 3.5.5-alpha.270bb0b → 3.5.5-alpha.500c168
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 +1 -1
- package/dist/auth/login.js +41 -15
- package/dist/auth/login.spec.js +97 -0
- package/dist/helpers/app/app-config-validators.d.ts +0 -5
- package/dist/helpers/app/app-config-validators.js +3 -76
- package/dist/helpers/app/app-config-validators.spec.js +0 -35
- package/dist/helpers/app/extensions-deployer.d.ts +2 -10
- package/dist/helpers/app/extensions-deployer.js +37 -31
- package/dist/interfaces.d.ts +0 -26
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/helpers/app/extensions-deployer.spec.d.ts +0 -1
- package/dist/helpers/app/extensions-deployer.spec.js +0 -29
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.5.5-alpha.
|
|
20
|
+
@contrail/cli/3.5.5-alpha.500c168 linux-x64 node-v24.21.0
|
|
21
21
|
$ contrail --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ contrail COMMAND
|
package/dist/auth/login.js
CHANGED
|
@@ -9,10 +9,10 @@ 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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const DEFAULT_API_GATEWAY = config_1.ENVIRONMENTS.prod;
|
|
13
|
+
const COGNITO_CONFIGS = {
|
|
14
|
+
dev: { userPoolId: 'us-east-1_e2cA0nJWu', clientId: '209s9c62o165cqab0fu5ihjtu9' },
|
|
15
|
+
prod: { userPoolId: 'us-east-1_qIaNZdUfm', clientId: '7knkt34gfacbgd0ghnuspefdfg' },
|
|
16
16
|
};
|
|
17
17
|
async function authUser() {
|
|
18
18
|
logger_1.default.debug('authorizing user...');
|
|
@@ -66,7 +66,7 @@ async function getOrgSlug(config) {
|
|
|
66
66
|
return orgSlug;
|
|
67
67
|
}
|
|
68
68
|
async function authenticateWithApiKey(config, apiKey, orgSlug) {
|
|
69
|
-
const apiGateway = config?.apiGateway ??
|
|
69
|
+
const apiGateway = config?.apiGateway ?? DEFAULT_API_GATEWAY;
|
|
70
70
|
sdk_1.Auth.config = {
|
|
71
71
|
apiUserToken: apiKey,
|
|
72
72
|
apiRefreshToken: null,
|
|
@@ -78,6 +78,10 @@ async function authenticateWithApiKey(config, apiKey, orgSlug) {
|
|
|
78
78
|
return returnAndStoreConfig(sdk_1.Auth.config);
|
|
79
79
|
}
|
|
80
80
|
async function authenticateWithUsernamePassword(config) {
|
|
81
|
+
const apiGateway = config?.apiGateway ?? DEFAULT_API_GATEWAY;
|
|
82
|
+
const cognitoConfig = getCognitoConfig(apiGateway);
|
|
83
|
+
const environment = describeEnvironment(apiGateway);
|
|
84
|
+
logger_1.default.info(`Signing in to ${environment} — use your ${environment} VibeIQ credentials.`);
|
|
81
85
|
const questions = [];
|
|
82
86
|
if (!process.env.CONTRAIL_CLI_EMAIL) {
|
|
83
87
|
questions.push({
|
|
@@ -100,14 +104,14 @@ async function authenticateWithUsernamePassword(config) {
|
|
|
100
104
|
email: process.env.CONTRAIL_CLI_EMAIL || answers.email,
|
|
101
105
|
password: process.env.CONTRAIL_CLI_PASSWORD || answers.password,
|
|
102
106
|
orgSlug: await getOrgSlug(config),
|
|
103
|
-
|
|
107
|
+
apiGateway,
|
|
104
108
|
};
|
|
105
109
|
if (!params.email || !params.password) {
|
|
106
110
|
throw new Error('Email and Password required');
|
|
107
111
|
}
|
|
108
112
|
const poolData = {
|
|
109
|
-
UserPoolId:
|
|
110
|
-
ClientId:
|
|
113
|
+
UserPoolId: cognitoConfig.userPoolId,
|
|
114
|
+
ClientId: cognitoConfig.clientId,
|
|
111
115
|
};
|
|
112
116
|
const userPool = new amazon_cognito_identity_js_1.CognitoUserPool(poolData);
|
|
113
117
|
const authData = {
|
|
@@ -123,18 +127,25 @@ async function authenticateWithUsernamePassword(config) {
|
|
|
123
127
|
return new Promise((resolve, reject) => {
|
|
124
128
|
cognitoUser.authenticateUser(authDetails, {
|
|
125
129
|
onSuccess: async (result) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
try {
|
|
131
|
+
resolve(await setAuthContext(result, params));
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
reject(e);
|
|
135
|
+
}
|
|
129
136
|
},
|
|
130
137
|
onFailure: (err) => {
|
|
138
|
+
if (err?.code === 'ResourceNotFoundException') {
|
|
139
|
+
reject(new Error(`This version of the CLI signs in to ${environment} with a Cognito client that no longer exists. Update the CLI: npm i -g @contrail/cli`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
131
142
|
reject(err);
|
|
132
143
|
},
|
|
133
144
|
});
|
|
134
145
|
});
|
|
135
146
|
}
|
|
136
147
|
async function setAuthContext(user, params) {
|
|
137
|
-
const apiGateway = params
|
|
148
|
+
const apiGateway = params.apiGateway;
|
|
138
149
|
const { vibeAccessToken, vibeRefreshToken } = await getVibeAccessTokenFromCognitoAccessToken(user.idToken.jwtToken, apiGateway);
|
|
139
150
|
// Set Auth Config
|
|
140
151
|
sdk_1.Auth.config = {
|
|
@@ -171,6 +182,19 @@ async function refreshCurrentUserOrg() {
|
|
|
171
182
|
// await checkTelemetry(current_Version);
|
|
172
183
|
return returnAndStoreConfig(sdk_1.Auth.config);
|
|
173
184
|
}
|
|
185
|
+
function getCognitoConfig(apiGateway) {
|
|
186
|
+
const environment = findEnvironmentName(apiGateway);
|
|
187
|
+
if (!environment) {
|
|
188
|
+
throw new Error(`No Cognito user pool is known for ${new URL(apiGateway).host}. Email and password sign-in works for ${Object.keys(COGNITO_CONFIGS).join(' and ')}; use CONTRAIL_CLI_API_KEY for other environments.`);
|
|
189
|
+
}
|
|
190
|
+
return COGNITO_CONFIGS[environment];
|
|
191
|
+
}
|
|
192
|
+
function findEnvironmentName(apiGateway) {
|
|
193
|
+
return Object.keys(config_1.ENVIRONMENTS).find((name) => config_1.ENVIRONMENTS[name] === apiGateway);
|
|
194
|
+
}
|
|
195
|
+
function describeEnvironment(apiGateway) {
|
|
196
|
+
return findEnvironmentName(apiGateway) ?? new URL(apiGateway).host;
|
|
197
|
+
}
|
|
174
198
|
async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiGateway) {
|
|
175
199
|
const url = `${apiGateway}/auth/cognito/`;
|
|
176
200
|
const response = await fetch(url, {
|
|
@@ -181,9 +205,11 @@ async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiG
|
|
|
181
205
|
},
|
|
182
206
|
body: JSON.stringify({ accessToken: cognitoAccessToken }),
|
|
183
207
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
throw new Error(`${describeEnvironment(apiGateway)} rejected the sign-in (${response.status}).`);
|
|
210
|
+
}
|
|
211
|
+
const data = await response.json();
|
|
212
|
+
return { vibeAccessToken: data.accessToken, vibeRefreshToken: data.refreshToken };
|
|
187
213
|
}
|
|
188
214
|
async function requestRefreshedAccessToken(vibeRefreshToken, apiGateway) {
|
|
189
215
|
const url = `${apiGateway}/auth/jwt_refresh/`;
|
package/dist/auth/login.spec.js
CHANGED
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const amazon_cognito_identity_js_1 = require("amazon-cognito-identity-js");
|
|
4
|
+
const sdk_1 = require("@contrail/sdk");
|
|
5
|
+
const config_1 = require("../helpers/config/config");
|
|
3
6
|
const login_1 = require("./login");
|
|
7
|
+
const mockAuthenticateUser = jest.fn();
|
|
8
|
+
jest.mock('amazon-cognito-identity-js', () => ({
|
|
9
|
+
CognitoUserPool: jest.fn(),
|
|
10
|
+
AuthenticationDetails: jest.fn(),
|
|
11
|
+
CognitoUser: jest.fn().mockImplementation(() => ({
|
|
12
|
+
authenticateUser: (details, callbacks) => mockAuthenticateUser(details, callbacks),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
jest.mock('@contrail/sdk', () => ({
|
|
16
|
+
Auth: { config: {} },
|
|
17
|
+
getCurrentUser: jest.fn(),
|
|
18
|
+
setConfig: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
jest.mock('../helpers/config/config', () => ({
|
|
21
|
+
...jest.requireActual('../helpers/config/config'),
|
|
22
|
+
getConfig: jest.fn(),
|
|
23
|
+
storeConfig: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
jest.mock('../logger', () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: { debug: jest.fn(), info: jest.fn() },
|
|
28
|
+
}));
|
|
29
|
+
const DEV_GATEWAY = 'https://api.dev.vibeiq.com/dev/api';
|
|
30
|
+
function jsonResponse(status, body = {}) {
|
|
31
|
+
return new Response(JSON.stringify(body), { status });
|
|
32
|
+
}
|
|
33
|
+
describe('authUser with email and password', () => {
|
|
34
|
+
const originalFetch = global.fetch;
|
|
35
|
+
const originalEmail = process.env.CONTRAIL_CLI_EMAIL;
|
|
36
|
+
const originalPassword = process.env.CONTRAIL_CLI_PASSWORD;
|
|
37
|
+
let fetchMock;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
process.env.CONTRAIL_CLI_EMAIL = 'dev@vibeiq.com';
|
|
41
|
+
process.env.CONTRAIL_CLI_PASSWORD = 'secret';
|
|
42
|
+
config_1.getConfig.mockResolvedValue({ apiGateway: DEV_GATEWAY, orgSlug: 'adam-zone-1' });
|
|
43
|
+
sdk_1.getCurrentUser.mockResolvedValue({ id: 'user-1' });
|
|
44
|
+
mockAuthenticateUser.mockImplementation((_details, callbacks) => callbacks.onSuccess({ idToken: { jwtToken: 'cognito-id-token' } }));
|
|
45
|
+
fetchMock = jest.fn();
|
|
46
|
+
global.fetch = fetchMock;
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
global.fetch = originalFetch;
|
|
50
|
+
process.env.CONTRAIL_CLI_EMAIL = originalEmail;
|
|
51
|
+
process.env.CONTRAIL_CLI_PASSWORD = originalPassword;
|
|
52
|
+
});
|
|
53
|
+
it('should sign in through the dev pool when the config points at dev', async () => {
|
|
54
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { accessToken: 'vibe-access', refreshToken: 'vibe-refresh' }));
|
|
55
|
+
const context = await (0, login_1.authUser)();
|
|
56
|
+
expect(amazon_cognito_identity_js_1.CognitoUserPool).toHaveBeenCalledWith({
|
|
57
|
+
UserPoolId: 'us-east-1_e2cA0nJWu',
|
|
58
|
+
ClientId: '209s9c62o165cqab0fu5ihjtu9',
|
|
59
|
+
});
|
|
60
|
+
expect(context).toEqual({
|
|
61
|
+
apiUserToken: 'vibe-access',
|
|
62
|
+
apiRefreshToken: 'vibe-refresh',
|
|
63
|
+
apiGateway: DEV_GATEWAY,
|
|
64
|
+
orgSlug: 'adam-zone-1',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('should sign in through the prod pool when the config points at prod', async () => {
|
|
68
|
+
config_1.getConfig.mockResolvedValue({
|
|
69
|
+
apiGateway: 'https://api.vibeiq.com/prod/api',
|
|
70
|
+
orgSlug: 'adam-zone-1',
|
|
71
|
+
});
|
|
72
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { accessToken: 'vibe-access', refreshToken: 'vibe-refresh' }));
|
|
73
|
+
await (0, login_1.authUser)();
|
|
74
|
+
expect(amazon_cognito_identity_js_1.CognitoUserPool).toHaveBeenCalledWith({
|
|
75
|
+
UserPoolId: 'us-east-1_qIaNZdUfm',
|
|
76
|
+
ClientId: '7knkt34gfacbgd0ghnuspefdfg',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('should fail without saving tokens when no pool is known for the environment', async () => {
|
|
80
|
+
config_1.getConfig.mockResolvedValue({
|
|
81
|
+
apiGateway: 'http://localhost:3000/api',
|
|
82
|
+
orgSlug: 'adam-zone-1',
|
|
83
|
+
});
|
|
84
|
+
await expect((0, login_1.authUser)()).rejects.toThrow('No Cognito user pool is known for localhost:3000.');
|
|
85
|
+
expect(config_1.storeConfig).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
it('should tell the user to update the CLI when its Cognito client no longer exists', async () => {
|
|
88
|
+
mockAuthenticateUser.mockImplementation((_details, callbacks) => callbacks.onFailure({
|
|
89
|
+
code: 'ResourceNotFoundException',
|
|
90
|
+
message: 'User pool client 209s9c62o165cqab0fu5ihjtu9 does not exist.',
|
|
91
|
+
}));
|
|
92
|
+
await expect((0, login_1.authUser)()).rejects.toThrow('This version of the CLI signs in to dev with a Cognito client that no longer exists. Update the CLI: npm i -g @contrail/cli');
|
|
93
|
+
expect(config_1.storeConfig).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
it('should fail without saving tokens when the environment rejects the sign-in', async () => {
|
|
96
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(401, { message: 'Unauthorized' }));
|
|
97
|
+
await expect((0, login_1.authUser)()).rejects.toThrow('dev rejected the sign-in (401).');
|
|
98
|
+
expect(config_1.storeConfig).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
4
101
|
describe('getVersionFromPackageJson', () => {
|
|
5
102
|
it('should return the version value from package.json', () => {
|
|
6
103
|
const version = (0, login_1.getVersionFromPackageJson)();
|
|
@@ -15,12 +15,7 @@ export declare const ERROR_MESSAGES: {
|
|
|
15
15
|
ignoredAccessGrants: string;
|
|
16
16
|
invalidPermission: (permission: string) => string;
|
|
17
17
|
invalidAccessGrants: (error: string) => string;
|
|
18
|
-
invalidExtensionIcon: (identifier: string) => string;
|
|
19
|
-
invalidAppliesTo: (identifier: string, problem: string) => string;
|
|
20
18
|
};
|
|
21
|
-
export declare function isValidExtensionIcon(icon: unknown): boolean;
|
|
22
|
-
/** Icons and appliesTo are checked here, since the apps service stores what it is given. */
|
|
23
|
-
export declare function validateExtensionPlacement(appDeclaration: AppDeclaration): AppConfigValidationError;
|
|
24
19
|
export declare function validateAppConfig(appConfig: AppDeclaration): void;
|
|
25
20
|
export declare function appVersionValidator(appDeclaration: AppDeclaration): AppConfigValidationError;
|
|
26
21
|
export declare function visibilityValidator(appDeclaration: AppDeclaration): AppConfigValidationError;
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateAccessGrants = exports.visibilityValidator = exports.appVersionValidator = exports.validateAppConfig = exports.
|
|
3
|
+
exports.validateAccessGrants = exports.visibilityValidator = exports.appVersionValidator = exports.validateAppConfig = exports.ERROR_MESSAGES = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const interfaces_1 = require("../../interfaces");
|
|
6
6
|
const logger_1 = tslib_1.__importDefault(require("../../logger"));
|
|
7
|
-
const VALIDATORS = [
|
|
8
|
-
appVersionValidator,
|
|
9
|
-
visibilityValidator,
|
|
10
|
-
validateAccessGrants,
|
|
11
|
-
validateExtensionPlacement,
|
|
12
|
-
];
|
|
7
|
+
const VALIDATORS = [appVersionValidator, visibilityValidator, validateAccessGrants];
|
|
13
8
|
exports.ERROR_MESSAGES = {
|
|
14
9
|
missingVersionField: 'Missing "version" field". Semantic App Version looks like "1.0.0". Use "contrail app version [patch|minor|major]" to update the app version.',
|
|
15
10
|
invalidVersionField: '"version" field is not a string. Expecting a version like "1.0.0". Manually update the "version" field to be a string in the app.yml file.',
|
|
@@ -22,75 +17,7 @@ exports.ERROR_MESSAGES = {
|
|
|
22
17
|
ignoredAccessGrants: 'App is set to public. accessGrants will be ignored.',
|
|
23
18
|
invalidPermission: (permission) => `Invalid permission found in one of the accessGrants objects: ${permission}.`,
|
|
24
19
|
invalidAccessGrants: (error) => `Invalid accessGrants. ${error}`,
|
|
25
|
-
invalidExtensionIcon: (identifier) => `Extension ${identifier}: icon must be a default icon name (like "line-builder"), { material: <icon name> } or { url: <SVG path or http(s) URL> }.`,
|
|
26
|
-
invalidAppliesTo: (identifier, problem) => `Extension ${identifier}: appliesTo ${problem}.`,
|
|
27
20
|
};
|
|
28
|
-
const STRING_LIST_FIELDS = ['typePaths', 'excludeTypePaths', 'roles'];
|
|
29
|
-
const APPLIES_TO_FIELDS = [...STRING_LIST_FIELDS, 'minSelected', 'maxSelected', 'label'];
|
|
30
|
-
function isValidExtensionIcon(icon) {
|
|
31
|
-
if (icon === undefined || icon === null) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
if (typeof icon === 'string') {
|
|
35
|
-
return /^[a-z0-9-]+$/.test(icon);
|
|
36
|
-
}
|
|
37
|
-
if (typeof icon !== 'object' || Array.isArray(icon) || Object.keys(icon).length !== 1) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
const { material, url } = icon;
|
|
41
|
-
if ('material' in icon) {
|
|
42
|
-
return typeof material === 'string' && /^[a-z0-9_]+$/.test(material);
|
|
43
|
-
}
|
|
44
|
-
if ('url' in icon) {
|
|
45
|
-
return typeof url === 'string' && url.length > 0 && !/^(javascript|data):/i.test(url.trim());
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
exports.isValidExtensionIcon = isValidExtensionIcon;
|
|
50
|
-
function appliesToProblem(appliesTo) {
|
|
51
|
-
if (typeof appliesTo !== 'object' || appliesTo === null || Array.isArray(appliesTo)) {
|
|
52
|
-
return 'must be an object';
|
|
53
|
-
}
|
|
54
|
-
const fields = appliesTo;
|
|
55
|
-
const unknownField = Object.keys(fields).find((field) => !APPLIES_TO_FIELDS.includes(field));
|
|
56
|
-
if (unknownField) {
|
|
57
|
-
return `has an unknown field "${unknownField}" (use ${APPLIES_TO_FIELDS.join(', ')})`;
|
|
58
|
-
}
|
|
59
|
-
for (const field of STRING_LIST_FIELDS) {
|
|
60
|
-
const value = fields[field];
|
|
61
|
-
if (value !== undefined &&
|
|
62
|
-
(!Array.isArray(value) || value.some((entry) => typeof entry !== 'string'))) {
|
|
63
|
-
return `${field} must be a list of strings`;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
for (const [field, min] of [
|
|
67
|
-
['minSelected', 0],
|
|
68
|
-
['maxSelected', 1],
|
|
69
|
-
]) {
|
|
70
|
-
const value = fields[field];
|
|
71
|
-
if (value !== undefined && (!Number.isInteger(value) || value < min)) {
|
|
72
|
-
return `${field} must be a whole number of at least ${min}`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
/** Icons and appliesTo are checked here, since the apps service stores what it is given. */
|
|
78
|
-
function validateExtensionPlacement(appDeclaration) {
|
|
79
|
-
const errors = [];
|
|
80
|
-
for (const extension of appDeclaration?.extensions ?? []) {
|
|
81
|
-
if (!isValidExtensionIcon(extension.icon)) {
|
|
82
|
-
errors.push(exports.ERROR_MESSAGES.invalidExtensionIcon(extension.identifier));
|
|
83
|
-
}
|
|
84
|
-
if (extension.appliesTo !== undefined) {
|
|
85
|
-
const problem = appliesToProblem(extension.appliesTo);
|
|
86
|
-
if (problem) {
|
|
87
|
-
errors.push(exports.ERROR_MESSAGES.invalidAppliesTo(extension.identifier, problem));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return { errors, warnings: [] };
|
|
92
|
-
}
|
|
93
|
-
exports.validateExtensionPlacement = validateExtensionPlacement;
|
|
94
21
|
function validateAppConfig(appConfig) {
|
|
95
22
|
const errors = [];
|
|
96
23
|
const warnings = [];
|
|
@@ -103,7 +30,7 @@ function validateAppConfig(appConfig) {
|
|
|
103
30
|
logger_1.default.warn(warnings);
|
|
104
31
|
}
|
|
105
32
|
if (errors.length) {
|
|
106
|
-
throw new Error(`Invalid app config: \n ${errors.map((x) => x
|
|
33
|
+
throw new Error(`Invalid app config: \n ${errors.map((x) => x.error).join('\n')}`);
|
|
107
34
|
}
|
|
108
35
|
}
|
|
109
36
|
exports.validateAppConfig = validateAppConfig;
|
|
@@ -308,38 +308,3 @@ describe('validateAccessGrants', () => {
|
|
|
308
308
|
expect(validationResult).toEqual(expectedError);
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
|
-
describe('validateExtensionPlacement', () => {
|
|
312
|
-
const declarationWith = (extension) => ({
|
|
313
|
-
extensions: [{ identifier: 'line-builder', name: 'Line builder', ...extension }],
|
|
314
|
-
});
|
|
315
|
-
it('should accept default, Material and SVG icons, appliesTo, and extensions with neither', () => {
|
|
316
|
-
for (const icon of [
|
|
317
|
-
'line-builder',
|
|
318
|
-
{ material: 'grid_on' },
|
|
319
|
-
{ url: 'assets/icons/line-builder.svg' },
|
|
320
|
-
undefined,
|
|
321
|
-
]) {
|
|
322
|
-
expect((0, app_config_validators_1.validateExtensionPlacement)(declarationWith({ icon }))).toEqual(validResult);
|
|
323
|
-
}
|
|
324
|
-
expect((0, app_config_validators_1.validateExtensionPlacement)(declarationWith({
|
|
325
|
-
appliesTo: {
|
|
326
|
-
typePaths: ['custom-entity:silhouette', 'item:product'],
|
|
327
|
-
excludeTypePaths: ['item:material'],
|
|
328
|
-
roles: ['family'],
|
|
329
|
-
maxSelected: 20,
|
|
330
|
-
label: 'Designs or BOMs',
|
|
331
|
-
},
|
|
332
|
-
}))).toEqual(validResult);
|
|
333
|
-
});
|
|
334
|
-
it('should name the extension whose icon or appliesTo is wrong', () => {
|
|
335
|
-
expect((0, app_config_validators_1.validateExtensionPlacement)(declarationWith({ icon: { url: 'javascript:alert(1)' } })).errors).toEqual([app_config_validators_1.ERROR_MESSAGES.invalidExtensionIcon('line-builder')]);
|
|
336
|
-
expect((0, app_config_validators_1.validateExtensionPlacement)(declarationWith({ appliesTo: { typePath: 'item' } })).errors[0]).toContain('unknown field "typePath"');
|
|
337
|
-
expect((0, app_config_validators_1.validateExtensionPlacement)(declarationWith({ appliesTo: { maxSelected: 0 } })).errors[0]).toContain('maxSelected must be a whole number of at least 1');
|
|
338
|
-
});
|
|
339
|
-
it('should show the message when validation fails', () => {
|
|
340
|
-
expect(() => (0, app_config_validators_1.validateAppConfig)({
|
|
341
|
-
version: '1.0.0',
|
|
342
|
-
...declarationWith({ icon: 'Bad Icon' }),
|
|
343
|
-
})).toThrow(app_config_validators_1.ERROR_MESSAGES.invalidExtensionIcon('line-builder'));
|
|
344
|
-
});
|
|
345
|
-
});
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { AppExtensionDefinition } from '@contrail/entity-types';
|
|
2
|
-
import { App, AppDeclaration,
|
|
2
|
+
import { App, AppDeclaration, ExtensionDefinitionDeclaration } from '../../interfaces';
|
|
3
3
|
export declare function getExtensionDefinitionsForAppVersion(app: App): Promise<AppExtensionDefinition[]>;
|
|
4
|
-
/** Entity-types' definition plus placement, which the apps service stores as declared. */
|
|
5
|
-
export type PlacedExtensionDefinition = AppExtensionDefinition & {
|
|
6
|
-
icon?: ExtensionIconDeclaration;
|
|
7
|
-
appliesTo?: ExtensionAppliesToDeclaration;
|
|
8
|
-
group?: string;
|
|
9
|
-
};
|
|
10
|
-
/** The definition published for an app.yml extension, for a new version or an update alike. */
|
|
11
|
-
export declare function buildExtensionDefinition(extension: ExtensionDefinitionDeclaration, app: App, appName?: string): PlacedExtensionDefinition;
|
|
12
4
|
export declare class ExtensionsDeployer {
|
|
13
5
|
deployExtensions(appDeclaration: AppDeclaration, app: App): Promise<void>;
|
|
14
|
-
deployExtension(extension: ExtensionDefinitionDeclaration, app: App, existingExtensions: Array<any
|
|
6
|
+
deployExtension(extension: ExtensionDefinitionDeclaration, app: App, existingExtensions: Array<any>): Promise<void>;
|
|
15
7
|
private createNewExtensionDefinition;
|
|
16
8
|
private updateActionDefinition;
|
|
17
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ExtensionsDeployer = exports.
|
|
3
|
+
exports.ExtensionsDeployer = exports.getExtensionDefinitionsForAppVersion = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const sdk_1 = require("@contrail/sdk");
|
|
6
6
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
@@ -13,28 +13,6 @@ async function getExtensionDefinitionsForAppVersion(app) {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
exports.getExtensionDefinitionsForAppVersion = getExtensionDefinitionsForAppVersion;
|
|
16
|
-
/** The definition published for an app.yml extension, for a new version or an update alike. */
|
|
17
|
-
function buildExtensionDefinition(extension, app, appName) {
|
|
18
|
-
return {
|
|
19
|
-
identifier: util_1.AppUtil.createExtensionIdentifier({
|
|
20
|
-
extensionIdentifier: extension?.identifier,
|
|
21
|
-
appIdentifier: app.identifier,
|
|
22
|
-
}),
|
|
23
|
-
name: extension.name,
|
|
24
|
-
appId: app.id,
|
|
25
|
-
appVersionId: app.latestAppVersionId,
|
|
26
|
-
extensionType: extension.extensionType,
|
|
27
|
-
userApps: extension.userApps,
|
|
28
|
-
/** @deprecated since entity-types v1.5.1, use display instead */
|
|
29
|
-
modalDimensions: extension.modalDimensions,
|
|
30
|
-
display: extension.display,
|
|
31
|
-
iframeUrl: extension.iframeUrl,
|
|
32
|
-
icon: extension.icon,
|
|
33
|
-
appliesTo: extension.appliesTo,
|
|
34
|
-
group: extension.group ?? appName,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
exports.buildExtensionDefinition = buildExtensionDefinition;
|
|
38
16
|
class ExtensionsDeployer {
|
|
39
17
|
async deployExtensions(appDeclaration, app) {
|
|
40
18
|
// FETCH ALL EXTENSIONS FROM THE API.
|
|
@@ -48,11 +26,11 @@ class ExtensionsDeployer {
|
|
|
48
26
|
logger_1.default.debug(appDeclaration.extensions);
|
|
49
27
|
if (appDeclaration.extensions) {
|
|
50
28
|
for (const extension of appDeclaration?.extensions ?? []) {
|
|
51
|
-
await this.deployExtension(extension, app, existingExtensions
|
|
29
|
+
await this.deployExtension(extension, app, existingExtensions);
|
|
52
30
|
}
|
|
53
31
|
}
|
|
54
32
|
}
|
|
55
|
-
async deployExtension(extension, app, existingExtensions
|
|
33
|
+
async deployExtension(extension, app, existingExtensions) {
|
|
56
34
|
logger_1.default.info(`${chalk_1.default.yellow.bold('EXTENSION')} \tDeploying extension: ${extension?.identifier}`);
|
|
57
35
|
const identifier = util_1.AppUtil.createExtensionIdentifier({
|
|
58
36
|
appIdentifier: app.identifier,
|
|
@@ -60,14 +38,28 @@ class ExtensionsDeployer {
|
|
|
60
38
|
});
|
|
61
39
|
const existingDefinition = existingExtensions.find((ad) => ad.identifier === identifier);
|
|
62
40
|
if (!existingDefinition) {
|
|
63
|
-
await this.createNewExtensionDefinition(extension, app
|
|
41
|
+
await this.createNewExtensionDefinition(extension, app);
|
|
64
42
|
}
|
|
65
43
|
else {
|
|
66
|
-
await this.updateActionDefinition(existingDefinition, extension, app
|
|
44
|
+
await this.updateActionDefinition(existingDefinition, extension, app);
|
|
67
45
|
}
|
|
68
46
|
}
|
|
69
|
-
async createNewExtensionDefinition(extension, app
|
|
70
|
-
const newObject =
|
|
47
|
+
async createNewExtensionDefinition(extension, app) {
|
|
48
|
+
const newObject = {
|
|
49
|
+
identifier: util_1.AppUtil.createExtensionIdentifier({
|
|
50
|
+
extensionIdentifier: extension?.identifier,
|
|
51
|
+
appIdentifier: app.identifier,
|
|
52
|
+
}),
|
|
53
|
+
name: extension.name,
|
|
54
|
+
appId: app.id,
|
|
55
|
+
appVersionId: app.latestAppVersionId,
|
|
56
|
+
extensionType: extension.extensionType,
|
|
57
|
+
userApps: extension.userApps,
|
|
58
|
+
/** @deprecated since entity-types v1.5.1, use display instead */
|
|
59
|
+
modalDimensions: extension.modalDimensions,
|
|
60
|
+
display: extension.display,
|
|
61
|
+
iframeUrl: extension.iframeUrl,
|
|
62
|
+
};
|
|
71
63
|
const actionDefinition = await new sdk_1.Entities().create({
|
|
72
64
|
entityName: 'app-extension-definition',
|
|
73
65
|
object: newObject,
|
|
@@ -75,8 +67,22 @@ class ExtensionsDeployer {
|
|
|
75
67
|
logger_1.default.info(`${chalk_1.default.yellow.bold('EXTENSION')} Created new extension definition: ${extension?.identifier}`);
|
|
76
68
|
return actionDefinition;
|
|
77
69
|
}
|
|
78
|
-
async updateActionDefinition(existingActionDefinition, extension, app
|
|
79
|
-
const updates =
|
|
70
|
+
async updateActionDefinition(existingActionDefinition, extension, app) {
|
|
71
|
+
const updates = {
|
|
72
|
+
identifier: util_1.AppUtil.createExtensionIdentifier({
|
|
73
|
+
extensionIdentifier: extension?.identifier,
|
|
74
|
+
appIdentifier: app.identifier,
|
|
75
|
+
}),
|
|
76
|
+
name: extension.name,
|
|
77
|
+
appId: app.id,
|
|
78
|
+
appVersionId: app.latestAppVersionId,
|
|
79
|
+
extensionType: extension.extensionType,
|
|
80
|
+
userApps: extension.userApps,
|
|
81
|
+
/** @deprecated since entity-types v1.5.1, use display instead */
|
|
82
|
+
modalDimensions: extension.modalDimensions,
|
|
83
|
+
display: extension.display,
|
|
84
|
+
iframeUrl: extension.iframeUrl,
|
|
85
|
+
};
|
|
80
86
|
const extensionDefinition = await new sdk_1.Entities().update({
|
|
81
87
|
entityName: 'app-extension-definition',
|
|
82
88
|
id: existingActionDefinition.id,
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -55,32 +55,6 @@ export interface ExtensionDefinitionDeclaration {
|
|
|
55
55
|
modalDimensions?: any;
|
|
56
56
|
display?: AppExtensionDisplayConfiguration;
|
|
57
57
|
iframeUrl?: string;
|
|
58
|
-
/** A default icon's name, `{ material: <icon name> }`, or `{ url: <one-color SVG, absolute or beside iframeUrl> }`.
|
|
59
|
-
* An extension without an icon shows none. */
|
|
60
|
-
icon?: ExtensionIconDeclaration;
|
|
61
|
-
/** What the extension acts on. Apps offer it beside a selection it fits (Boards: the property bar's app row and the
|
|
62
|
-
* top of the context menu); without it, it is offered as before. */
|
|
63
|
-
appliesTo?: ExtensionAppliesToDeclaration;
|
|
64
|
-
/** The name the app's extensions are grouped under; defaults to appName. */
|
|
65
|
-
group?: string;
|
|
66
|
-
}
|
|
67
|
-
export type ExtensionIconDeclaration = string | {
|
|
68
|
-
material: string;
|
|
69
|
-
} | {
|
|
70
|
-
url: string;
|
|
71
|
-
};
|
|
72
|
-
export interface ExtensionAppliesToDeclaration {
|
|
73
|
-
/** Type paths, each matching itself and its subtypes: item:product matches item:product:footwear. */
|
|
74
|
-
typePaths?: string[];
|
|
75
|
-
/** Type paths left out, with their subtypes: item:material. */
|
|
76
|
-
excludeTypePaths?: string[];
|
|
77
|
-
/** Item roles the selection must have: family, option. */
|
|
78
|
-
roles?: string[];
|
|
79
|
-
/** Fewest records selected; 1 when left out. */
|
|
80
|
-
minSelected?: number;
|
|
81
|
-
maxSelected?: number;
|
|
82
|
-
/** What to select, for the hint shown while the selection doesn't fit: "a Design". */
|
|
83
|
-
label?: string;
|
|
84
58
|
}
|
|
85
59
|
export interface ConfigPropertyElements {
|
|
86
60
|
[key: string]: ConfigPropertyElement;
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const extensions_deployer_1 = require("./extensions-deployer");
|
|
4
|
-
const app = {
|
|
5
|
-
id: 'app-1',
|
|
6
|
-
identifier: '@vibeiq.com/gap-design-poc',
|
|
7
|
-
latestAppVersionId: 'version-1',
|
|
8
|
-
};
|
|
9
|
-
describe('buildExtensionDefinition', () => {
|
|
10
|
-
it('should publish placement as declared, grouped under the app name', () => {
|
|
11
|
-
const definition = (0, extensions_deployer_1.buildExtensionDefinition)({
|
|
12
|
-
identifier: 'line-builder',
|
|
13
|
-
name: 'Line builder',
|
|
14
|
-
iframeUrl: 'http://localhost:4395/index.html#/line-builder',
|
|
15
|
-
icon: 'line-builder',
|
|
16
|
-
appliesTo: { typePaths: ['custom-entity:silhouette'], maxSelected: 20 },
|
|
17
|
-
}, app, 'Gap');
|
|
18
|
-
expect(definition).toMatchObject({
|
|
19
|
-
identifier: '@vibeiq.com/gap-design-poc:line-builder',
|
|
20
|
-
appVersionId: 'version-1',
|
|
21
|
-
icon: 'line-builder',
|
|
22
|
-
appliesTo: { typePaths: ['custom-entity:silhouette'], maxSelected: 20 },
|
|
23
|
-
group: 'Gap',
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
it("should keep an extension's own group", () => {
|
|
27
|
-
expect((0, extensions_deployer_1.buildExtensionDefinition)({ identifier: 'x', name: 'X', group: 'Tools' }, app, 'Gap').group).toBe('Tools');
|
|
28
|
-
});
|
|
29
|
-
});
|