@contrail/flexplm 1.7.3-alpha.b33e995 → 1.7.4-alpha.7bbda17
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/lib/cli/commands/config-create.d.ts +5 -0
- package/lib/cli/commands/config-create.js +85 -0
- package/lib/cli/commands/config-create.spec.d.ts +1 -0
- package/lib/cli/commands/config-create.spec.js +80 -0
- package/lib/cli/commands/config-upload.d.ts +19 -0
- package/lib/cli/commands/config-upload.js +249 -0
- package/lib/cli/commands/config-upload.spec.d.ts +1 -0
- package/lib/cli/commands/config-upload.spec.js +90 -0
- package/lib/cli/config-index.d.ts +5 -0
- package/lib/cli/config-index.js +61 -0
- package/lib/cli/config-index.spec.d.ts +1 -0
- package/lib/cli/config-index.spec.js +68 -0
- package/lib/cli/template/config-template.json.template +22 -0
- package/lib/interfaces/interfaces.d.ts +1 -1
- package/lib/util/config-defaults.d.ts +2 -0
- package/lib/util/config-defaults.js +22 -8
- package/lib/util/config-defaults.spec.js +14 -0
- package/lib/util/data-converter.spec.js +15 -30
- package/lib/util/thumbnail-util.js +6 -8
- package/lib/util/thumbnail-util.spec.js +35 -0
- package/package.json +3 -2
- package/scripts/copy-template.js +9 -5
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ConfigCreateCommand = void 0;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const readline = __importStar(require("readline"));
|
|
30
|
+
const TEMPLATE_FILENAME = 'config-template.json.template';
|
|
31
|
+
const ORG_PLACEHOLDER = '<ORG_NAME>';
|
|
32
|
+
const APP_IDENTIFIER_PLACEHOLDER = '<APP_IDENTIFIER>';
|
|
33
|
+
const DEFAULT_APP_IDENTIFIER = '@vibeiq/flexplm-connector';
|
|
34
|
+
class ConfigCreateCommand {
|
|
35
|
+
prompt(rl, question) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
rl.question(question, (answer) => resolve(answer));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
findTemplate() {
|
|
41
|
+
const candidates = [
|
|
42
|
+
path.join(__dirname, '..', 'template', TEMPLATE_FILENAME),
|
|
43
|
+
path.join(__dirname, '..', '..', '..', 'src', 'cli', 'template', TEMPLATE_FILENAME),
|
|
44
|
+
];
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (fs.existsSync(candidate)) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Could not locate ${TEMPLATE_FILENAME}. Tried:\n ${candidates.join('\n ')}`);
|
|
51
|
+
}
|
|
52
|
+
async run() {
|
|
53
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
+
const onSigint = () => {
|
|
55
|
+
rl.close();
|
|
56
|
+
process.stdout.write('\n');
|
|
57
|
+
process.exit(130);
|
|
58
|
+
};
|
|
59
|
+
process.once('SIGINT', onSigint);
|
|
60
|
+
let orgName;
|
|
61
|
+
let appIdentifier;
|
|
62
|
+
try {
|
|
63
|
+
orgName = (await this.prompt(rl, 'orgName: ')).trim();
|
|
64
|
+
appIdentifier = (await this.prompt(rl, `appIdentifier (default: ${DEFAULT_APP_IDENTIFIER}): `)).trim() || DEFAULT_APP_IDENTIFIER;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
process.removeListener('SIGINT', onSigint);
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
70
|
+
if (!orgName) {
|
|
71
|
+
throw new Error('orgName is required');
|
|
72
|
+
}
|
|
73
|
+
const templatePath = this.findTemplate();
|
|
74
|
+
const templateBody = fs.readFileSync(templatePath, 'utf8');
|
|
75
|
+
const rendered = templateBody.split(ORG_PLACEHOLDER).join(orgName).split(APP_IDENTIFIER_PLACEHOLDER).join(appIdentifier);
|
|
76
|
+
const outPath = path.resolve(process.cwd(), `${orgName}-flexplmConfig.json`);
|
|
77
|
+
if (fs.existsSync(outPath)) {
|
|
78
|
+
throw new Error(`Refusing to overwrite existing file: ${outPath}`);
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(outPath, rendered, 'utf8');
|
|
81
|
+
console.log(`Created ${outPath}`);
|
|
82
|
+
console.log('Only "orgName" and "appIdentifier" are required. See the "_availableAttributes" block in the file for other attributes this org can set (apiHost, identifierAtts, LCSMaterial, etc.) — add the ones you need as real top-level keys, then delete "_availableAttributes"; the connector ignores it and applies defaults at runtime for anything you omit.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.ConfigCreateCommand = ConfigCreateCommand;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const fs = __importStar(require("fs"));
|
|
27
|
+
const os = __importStar(require("os"));
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
let answers = [];
|
|
30
|
+
jest.mock('readline', () => ({
|
|
31
|
+
createInterface: () => ({
|
|
32
|
+
question: (_q, cb) => cb(answers.shift() || ''),
|
|
33
|
+
close: () => { },
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
const config_create_1 = require("./config-create");
|
|
37
|
+
describe('ConfigCreateCommand', () => {
|
|
38
|
+
let tempDir;
|
|
39
|
+
let originalCwd;
|
|
40
|
+
let logSpy;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
originalCwd = process.cwd();
|
|
43
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flexplm-config-create-'));
|
|
44
|
+
process.chdir(tempDir);
|
|
45
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
46
|
+
answers = [];
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
logSpy.mockRestore();
|
|
50
|
+
process.chdir(originalCwd);
|
|
51
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
it('writes <orgName>-flexplmConfig.json with orgName and appIdentifier filled in', async () => {
|
|
54
|
+
answers = ['acme', '@vibeiq/flexplm-connector'];
|
|
55
|
+
await new config_create_1.ConfigCreateCommand().run();
|
|
56
|
+
const outPath = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
57
|
+
expect(fs.existsSync(outPath)).toBe(true);
|
|
58
|
+
const config = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
|
59
|
+
expect(config.orgName).toEqual('acme');
|
|
60
|
+
expect(config.appIdentifier).toEqual('@vibeiq/flexplm-connector');
|
|
61
|
+
});
|
|
62
|
+
it('defaults appIdentifier to @vibeiq/flexplm-connector when left blank', async () => {
|
|
63
|
+
answers = ['acme', ''];
|
|
64
|
+
await new config_create_1.ConfigCreateCommand().run();
|
|
65
|
+
const outPath = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
66
|
+
const config = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
|
67
|
+
expect(config.appIdentifier).toEqual('@vibeiq/flexplm-connector');
|
|
68
|
+
});
|
|
69
|
+
it('throws when orgName is empty', async () => {
|
|
70
|
+
answers = [' '];
|
|
71
|
+
await expect(new config_create_1.ConfigCreateCommand().run()).rejects.toThrow(/orgName is required/);
|
|
72
|
+
});
|
|
73
|
+
it('refuses to overwrite an existing file', async () => {
|
|
74
|
+
const existing = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
75
|
+
fs.writeFileSync(existing, 'do not clobber', 'utf8');
|
|
76
|
+
answers = ['acme', '@vibeiq/flexplm-connector'];
|
|
77
|
+
await expect(new config_create_1.ConfigCreateCommand().run()).rejects.toThrow(/Refusing to overwrite/);
|
|
78
|
+
expect(fs.readFileSync(existing, 'utf8')).toEqual('do not clobber');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface ConfigUploadOptions {
|
|
2
|
+
filePath: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
skipGit: boolean;
|
|
6
|
+
updateConfig: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class ConfigUploadCommand {
|
|
9
|
+
static parseArgs(args: string[]): ConfigUploadOptions;
|
|
10
|
+
static buildCommitMessage(userMessage: string, fileId: string): string;
|
|
11
|
+
private prompt;
|
|
12
|
+
private promptHidden;
|
|
13
|
+
private runGit;
|
|
14
|
+
private tryRunGit;
|
|
15
|
+
private commitToGit;
|
|
16
|
+
run(args: string[]): Promise<void>;
|
|
17
|
+
private setConfigFileOnAppOrg;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ConfigUploadCommand = void 0;
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const fs = __importStar(require("fs"));
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
const readline = __importStar(require("readline"));
|
|
31
|
+
const sdk_1 = require("@contrail/sdk");
|
|
32
|
+
class ConfigUploadCommand {
|
|
33
|
+
static parseArgs(args) {
|
|
34
|
+
let filePath;
|
|
35
|
+
let message;
|
|
36
|
+
let branch;
|
|
37
|
+
let skipGit = false;
|
|
38
|
+
let updateConfig = false;
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
const a = args[i];
|
|
41
|
+
if (a === '-m') {
|
|
42
|
+
message = args[++i];
|
|
43
|
+
if (message === undefined) {
|
|
44
|
+
throw new Error('-m requires a commit message argument');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (a === '-b') {
|
|
48
|
+
branch = args[++i];
|
|
49
|
+
if (branch === undefined) {
|
|
50
|
+
throw new Error('-b requires a branch name argument');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (a === '--skip-git' || a === '--skipGit') {
|
|
54
|
+
skipGit = true;
|
|
55
|
+
}
|
|
56
|
+
else if (a === '--update-config') {
|
|
57
|
+
updateConfig = true;
|
|
58
|
+
}
|
|
59
|
+
else if (a.startsWith('-')) {
|
|
60
|
+
throw new Error(`Unknown option: ${a}`);
|
|
61
|
+
}
|
|
62
|
+
else if (!filePath) {
|
|
63
|
+
filePath = a;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new Error(`Unexpected argument: ${a}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!filePath) {
|
|
70
|
+
throw new Error('upload: missing <path.json> argument');
|
|
71
|
+
}
|
|
72
|
+
return { filePath, message, branch, skipGit, updateConfig };
|
|
73
|
+
}
|
|
74
|
+
static buildCommitMessage(userMessage, fileId) {
|
|
75
|
+
const lines = userMessage.split(/\r?\n/);
|
|
76
|
+
lines[0] = `${lines[0]} [fileId: ${fileId}]`;
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
prompt(question) {
|
|
80
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
rl.question(question, (answer) => {
|
|
83
|
+
rl.close();
|
|
84
|
+
resolve(answer.trim());
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
promptHidden(question) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
91
|
+
const rlAny = rl;
|
|
92
|
+
rlAny._writeToOutput = (str) => {
|
|
93
|
+
if (str.includes(question)) {
|
|
94
|
+
rlAny.output.write(str);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
rl.question(question, (answer) => {
|
|
98
|
+
rl.close();
|
|
99
|
+
process.stdout.write('\n');
|
|
100
|
+
resolve(answer);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
runGit(args, cwd) {
|
|
105
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
106
|
+
}
|
|
107
|
+
tryRunGit(args, cwd) {
|
|
108
|
+
try {
|
|
109
|
+
const stdout = this.runGit(args, cwd);
|
|
110
|
+
return { ok: true, stdout, stderr: '' };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
stdout: err && err.stdout ? err.stdout.toString() : '',
|
|
116
|
+
stderr: err && err.stderr ? err.stderr.toString() : (err && err.message) || '',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async commitToGit(absPath, fileId, options) {
|
|
121
|
+
const repoDir = path.dirname(absPath);
|
|
122
|
+
const relPath = path.basename(absPath);
|
|
123
|
+
const versionCheck = this.tryRunGit(['--version'], repoDir);
|
|
124
|
+
if (!versionCheck.ok) {
|
|
125
|
+
console.log('git command not available; skipping git commit.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const insideRepo = this.tryRunGit(['rev-parse', '--is-inside-work-tree'], repoDir);
|
|
129
|
+
if (!insideRepo.ok || insideRepo.stdout.trim() !== 'true') {
|
|
130
|
+
console.log(`Not inside a git working tree (${repoDir}); skipping git commit.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const tracked = this.tryRunGit(['ls-files', '--error-unmatch', relPath], repoDir);
|
|
134
|
+
if (!tracked.ok) {
|
|
135
|
+
const answer = (await this.prompt(`File is not tracked by git: ${relPath}\nAdd it to git? (Y/n): `)).toLowerCase();
|
|
136
|
+
if (answer === 'n' || answer === 'no') {
|
|
137
|
+
console.log('Nothing was done in git.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options.branch) {
|
|
142
|
+
const branchResult = this.tryRunGit(['checkout', '-b', options.branch], repoDir);
|
|
143
|
+
if (!branchResult.ok) {
|
|
144
|
+
throw new Error(`Failed to create branch "${options.branch}": ${branchResult.stderr.trim()}`);
|
|
145
|
+
}
|
|
146
|
+
console.log(`Created and switched to branch "${options.branch}"`);
|
|
147
|
+
}
|
|
148
|
+
let message = options.message;
|
|
149
|
+
if (!message) {
|
|
150
|
+
message = await this.prompt('Commit message: ');
|
|
151
|
+
if (!message) {
|
|
152
|
+
throw new Error('A commit message is required');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const finalMessage = ConfigUploadCommand.buildCommitMessage(message, fileId);
|
|
156
|
+
const addResult = this.tryRunGit(['add', '--', relPath], repoDir);
|
|
157
|
+
if (!addResult.ok) {
|
|
158
|
+
throw new Error(`git add failed: ${addResult.stderr.trim()}`);
|
|
159
|
+
}
|
|
160
|
+
const commitResult = this.tryRunGit(['commit', '-m', finalMessage, '--', relPath], repoDir);
|
|
161
|
+
if (!commitResult.ok) {
|
|
162
|
+
throw new Error(`git commit failed: ${commitResult.stderr.trim() || commitResult.stdout.trim()}`);
|
|
163
|
+
}
|
|
164
|
+
console.log(commitResult.stdout.trim());
|
|
165
|
+
}
|
|
166
|
+
async run(args) {
|
|
167
|
+
const options = ConfigUploadCommand.parseArgs(args);
|
|
168
|
+
const absPath = path.resolve(process.cwd(), options.filePath);
|
|
169
|
+
if (!fs.existsSync(absPath)) {
|
|
170
|
+
throw new Error(`File not found: ${absPath}`);
|
|
171
|
+
}
|
|
172
|
+
if (!absPath.endsWith('.json')) {
|
|
173
|
+
throw new Error(`Expected a .json file, got: ${absPath}`);
|
|
174
|
+
}
|
|
175
|
+
const raw = fs.readFileSync(absPath, 'utf8');
|
|
176
|
+
let config;
|
|
177
|
+
try {
|
|
178
|
+
config = JSON.parse(raw);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new Error(`File is not valid JSON: ${absPath}\n${err && err.message ? err.message : err}`);
|
|
182
|
+
}
|
|
183
|
+
const orgName = config && config.orgName;
|
|
184
|
+
const appIdentifier = config && config.appIdentifier;
|
|
185
|
+
if (!orgName) {
|
|
186
|
+
throw new Error(`Config file is missing "orgName": ${absPath}`);
|
|
187
|
+
}
|
|
188
|
+
if (!appIdentifier) {
|
|
189
|
+
throw new Error(`Config file is missing "appIdentifier": ${absPath}`);
|
|
190
|
+
}
|
|
191
|
+
let email = process.env.CONTRAIL_CLI_EMAIL;
|
|
192
|
+
let password = process.env.CONTRAIL_CLI_PASSWORD;
|
|
193
|
+
if (!email) {
|
|
194
|
+
email = await this.prompt('Email: ');
|
|
195
|
+
}
|
|
196
|
+
if (!password) {
|
|
197
|
+
password = await this.promptHidden('Password: ');
|
|
198
|
+
}
|
|
199
|
+
if (!email || !password) {
|
|
200
|
+
throw new Error('Email and password are required');
|
|
201
|
+
}
|
|
202
|
+
await (0, sdk_1.login)({ orgSlug: orgName, email, password });
|
|
203
|
+
console.log(`Logged in to org "${orgName}" as ${email}`);
|
|
204
|
+
const apps = await new sdk_1.Entities().get({
|
|
205
|
+
entityName: 'app',
|
|
206
|
+
criteria: { identifier: appIdentifier },
|
|
207
|
+
});
|
|
208
|
+
if (!apps || apps.length !== 1) {
|
|
209
|
+
throw new Error(`Expected exactly one app with identifier "${appIdentifier}" in org "${orgName}", found ${apps ? apps.length : 0}`);
|
|
210
|
+
}
|
|
211
|
+
const app = apps[0];
|
|
212
|
+
const buffer = fs.readFileSync(absPath);
|
|
213
|
+
const fileName = path.basename(absPath);
|
|
214
|
+
const fileOwner = `app:${app.id}`;
|
|
215
|
+
const uploadedFile = await new sdk_1.Files().createAndUploadFileFromBuffer(buffer, 'application/json', fileName, fileOwner);
|
|
216
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
217
|
+
const responsePath = `${absPath}.uploaded-${timestamp}.json`;
|
|
218
|
+
fs.writeFileSync(responsePath, JSON.stringify(uploadedFile, null, 2), 'utf8');
|
|
219
|
+
console.log(`Wrote response to ${responsePath}`);
|
|
220
|
+
console.log(`FILE ID: ${uploadedFile.id}`);
|
|
221
|
+
if (!options.skipGit) {
|
|
222
|
+
await this.commitToGit(absPath, uploadedFile.id, options);
|
|
223
|
+
}
|
|
224
|
+
if (options.updateConfig) {
|
|
225
|
+
await this.setConfigFileOnAppOrg(app.id, appIdentifier, orgName, uploadedFile.id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async setConfigFileOnAppOrg(appId, appIdentifier, orgName, fileId) {
|
|
229
|
+
const appOrgs = await new sdk_1.Entities().get({
|
|
230
|
+
entityName: 'app-org',
|
|
231
|
+
criteria: { appId },
|
|
232
|
+
});
|
|
233
|
+
if (!appOrgs || appOrgs.length === 0) {
|
|
234
|
+
throw new Error(`Failed to set the file onto the app config for "${appIdentifier}" because it is not installed in org "${orgName}". Install it via the admin console before using --update-config. You can paste the uploaded file's ID into the app config without needing to re-run this command.`);
|
|
235
|
+
}
|
|
236
|
+
if (appOrgs.length > 1) {
|
|
237
|
+
throw new Error(`Failed to set the file onto the app config for "${appIdentifier}" in org "${orgName}" because ${appOrgs.length} installations were identified. Expected one. Please contact customer support for assistance.`);
|
|
238
|
+
}
|
|
239
|
+
const appOrg = appOrgs[0];
|
|
240
|
+
const nextAppConfig = { ...(appOrg.appConfig || {}), configFile: fileId };
|
|
241
|
+
await new sdk_1.Entities().update({
|
|
242
|
+
entityName: 'app-org',
|
|
243
|
+
id: appOrg.id,
|
|
244
|
+
object: { appConfig: nextAppConfig },
|
|
245
|
+
});
|
|
246
|
+
console.log(`Successfully set "appConfig.configFile" for installed "${appIdentifier}" to new FILE ID: "${fileId}" on org "${orgName}"`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exports.ConfigUploadCommand = ConfigUploadCommand;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('@contrail/sdk', () => ({
|
|
4
|
+
Entities: jest.fn(),
|
|
5
|
+
Files: jest.fn(),
|
|
6
|
+
login: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
const config_upload_1 = require("./config-upload");
|
|
9
|
+
describe('ConfigUploadCommand.parseArgs', () => {
|
|
10
|
+
it('parses a bare file path', () => {
|
|
11
|
+
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json']);
|
|
12
|
+
expect(opts).toEqual({
|
|
13
|
+
filePath: 'config.json',
|
|
14
|
+
message: undefined,
|
|
15
|
+
branch: undefined,
|
|
16
|
+
skipGit: false,
|
|
17
|
+
updateConfig: false,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('parses -m commit message option', () => {
|
|
21
|
+
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-m', 'my message']);
|
|
22
|
+
expect(opts.message).toEqual('my message');
|
|
23
|
+
expect(opts.skipGit).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it('parses -b branch option', () => {
|
|
26
|
+
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-b', 'feature/x']);
|
|
27
|
+
expect(opts.branch).toEqual('feature/x');
|
|
28
|
+
});
|
|
29
|
+
it('parses --skip-git flag', () => {
|
|
30
|
+
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--skip-git']);
|
|
31
|
+
expect(opts.skipGit).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('parses --update-config flag', () => {
|
|
34
|
+
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--update-config']);
|
|
35
|
+
expect(opts.updateConfig).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('throws when -m is missing its value', () => {
|
|
38
|
+
expect(() => config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-m'])).toThrow(/-m requires a commit message/);
|
|
39
|
+
});
|
|
40
|
+
it('throws on unknown option', () => {
|
|
41
|
+
expect(() => config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--bogus'])).toThrow(/Unknown option: --bogus/);
|
|
42
|
+
});
|
|
43
|
+
it('throws when no file path is provided', () => {
|
|
44
|
+
expect(() => config_upload_1.ConfigUploadCommand.parseArgs([])).toThrow(/missing <path\.json>/);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('ConfigUploadCommand.buildCommitMessage', () => {
|
|
48
|
+
it('appends fileId to the first line of a single-line message', () => {
|
|
49
|
+
expect(config_upload_1.ConfigUploadCommand.buildCommitMessage('initial commit', 'abc123')).toEqual('initial commit [fileId: abc123]');
|
|
50
|
+
});
|
|
51
|
+
it('handles CRLF line endings', () => {
|
|
52
|
+
const result = config_upload_1.ConfigUploadCommand.buildCommitMessage('header\r\nbody', 'fid');
|
|
53
|
+
expect(result).toEqual('header [fileId: fid]\nbody');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('ConfigUploadCommand.run', () => {
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
const os = require('os');
|
|
59
|
+
const path = require('path');
|
|
60
|
+
let tempDir;
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flexplm-config-upload-'));
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
it('throws when the config file is missing orgName', async () => {
|
|
68
|
+
const filePath = path.join(tempDir, 'bad.json');
|
|
69
|
+
fs.writeFileSync(filePath, JSON.stringify({ appIdentifier: '@vibeiq/flexplm-connector' }), 'utf8');
|
|
70
|
+
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/missing "orgName"/);
|
|
71
|
+
});
|
|
72
|
+
it('throws when the config file is missing appIdentifier', async () => {
|
|
73
|
+
const filePath = path.join(tempDir, 'bad.json');
|
|
74
|
+
fs.writeFileSync(filePath, JSON.stringify({ orgName: 'acme' }), 'utf8');
|
|
75
|
+
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/missing "appIdentifier"/);
|
|
76
|
+
});
|
|
77
|
+
it('throws when the file is not valid JSON', async () => {
|
|
78
|
+
const filePath = path.join(tempDir, 'bad.json');
|
|
79
|
+
fs.writeFileSync(filePath, '{not json', 'utf8');
|
|
80
|
+
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/not valid JSON/);
|
|
81
|
+
});
|
|
82
|
+
it('throws when the file does not exist', async () => {
|
|
83
|
+
await expect(new config_upload_1.ConfigUploadCommand().run([path.join(tempDir, 'nope.json'), '--skip-git'])).rejects.toThrow(/File not found/);
|
|
84
|
+
});
|
|
85
|
+
it('throws when the file is not a .json file', async () => {
|
|
86
|
+
const filePath = path.join(tempDir, 'config.txt');
|
|
87
|
+
fs.writeFileSync(filePath, '{}', 'utf8');
|
|
88
|
+
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/Expected a \.json file/);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.main = exports.ConfigCli = void 0;
|
|
5
|
+
const config_create_1 = require("./commands/config-create");
|
|
6
|
+
const config_upload_1 = require("./commands/config-upload");
|
|
7
|
+
const USAGE = `Usage: flexplm-config <command> [args]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
create Scaffold a new connector config .json file in the current directory
|
|
11
|
+
upload <path.json> [opts] Upload a connector config .json file to VibeIQ
|
|
12
|
+
|
|
13
|
+
Upload options:
|
|
14
|
+
-m <message> Git commit message (prompted if omitted)
|
|
15
|
+
-b <branch> Create a new git branch before committing
|
|
16
|
+
--skip-git Skip the post-upload git commit (default: commit)
|
|
17
|
+
--update-config Patch the app-org appConfig.configFile with the uploaded file ID without needing to paste into the admin console
|
|
18
|
+
|
|
19
|
+
Environment (upload):
|
|
20
|
+
CONTRAIL_CLI_EMAIL VibeIQ user email
|
|
21
|
+
CONTRAIL_CLI_PASSWORD VibeIQ user password
|
|
22
|
+
`;
|
|
23
|
+
class ConfigCli {
|
|
24
|
+
async main() {
|
|
25
|
+
const [, , command, ...rest] = process.argv;
|
|
26
|
+
switch (command) {
|
|
27
|
+
case 'create':
|
|
28
|
+
await new config_create_1.ConfigCreateCommand().run();
|
|
29
|
+
return;
|
|
30
|
+
case 'upload':
|
|
31
|
+
if (!rest[0]) {
|
|
32
|
+
console.error('upload: missing <path.json> argument');
|
|
33
|
+
console.error(USAGE);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
await new config_upload_1.ConfigUploadCommand().run(rest);
|
|
37
|
+
return;
|
|
38
|
+
case undefined:
|
|
39
|
+
case '-h':
|
|
40
|
+
case '--help':
|
|
41
|
+
case 'help':
|
|
42
|
+
console.log(USAGE);
|
|
43
|
+
return;
|
|
44
|
+
default:
|
|
45
|
+
console.error(`Unknown command: ${command}`);
|
|
46
|
+
console.error(USAGE);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.ConfigCli = ConfigCli;
|
|
52
|
+
function main() {
|
|
53
|
+
return new ConfigCli().main();
|
|
54
|
+
}
|
|
55
|
+
exports.main = main;
|
|
56
|
+
if (require.main === module) {
|
|
57
|
+
main().catch((err) => {
|
|
58
|
+
console.error(err && err.message ? err.message : err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createRunMock = jest.fn().mockResolvedValue(undefined);
|
|
4
|
+
const uploadRunMock = jest.fn().mockResolvedValue(undefined);
|
|
5
|
+
jest.mock('./commands/config-create', () => ({
|
|
6
|
+
ConfigCreateCommand: jest.fn().mockImplementation(() => ({ run: createRunMock })),
|
|
7
|
+
}));
|
|
8
|
+
jest.mock('./commands/config-upload', () => ({
|
|
9
|
+
ConfigUploadCommand: jest.fn().mockImplementation(() => ({ run: uploadRunMock })),
|
|
10
|
+
}));
|
|
11
|
+
const config_index_1 = require("./config-index");
|
|
12
|
+
describe('config cli main dispatcher', () => {
|
|
13
|
+
let originalArgv;
|
|
14
|
+
let logSpy;
|
|
15
|
+
let errorSpy;
|
|
16
|
+
let exitSpy;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
originalArgv = process.argv;
|
|
19
|
+
createRunMock.mockClear();
|
|
20
|
+
uploadRunMock.mockClear();
|
|
21
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
24
|
+
throw new Error(`__EXIT__:${code}`);
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
process.argv = originalArgv;
|
|
29
|
+
logSpy.mockRestore();
|
|
30
|
+
errorSpy.mockRestore();
|
|
31
|
+
exitSpy.mockRestore();
|
|
32
|
+
});
|
|
33
|
+
function setArgv(...args) {
|
|
34
|
+
process.argv = ['node', 'cli', ...args];
|
|
35
|
+
}
|
|
36
|
+
it('dispatches the create command', async () => {
|
|
37
|
+
setArgv('create');
|
|
38
|
+
await (0, config_index_1.main)();
|
|
39
|
+
expect(createRunMock).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(uploadRunMock).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
it('dispatches the upload command and forwards remaining args', async () => {
|
|
43
|
+
setArgv('upload', 'config.json', '-m', 'msg', '--skip-git');
|
|
44
|
+
await (0, config_index_1.main)();
|
|
45
|
+
expect(uploadRunMock).toHaveBeenCalledWith(['config.json', '-m', 'msg', '--skip-git']);
|
|
46
|
+
});
|
|
47
|
+
it('exits when upload is missing its argument', async () => {
|
|
48
|
+
setArgv('upload');
|
|
49
|
+
await expect((0, config_index_1.main)()).rejects.toThrow('__EXIT__:1');
|
|
50
|
+
expect(uploadRunMock).not.toHaveBeenCalled();
|
|
51
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/missing <path\.json>/));
|
|
52
|
+
});
|
|
53
|
+
it.each(['help', '-h', '--help'])('prints usage for %s', async (helpFlag) => {
|
|
54
|
+
setArgv(helpFlag);
|
|
55
|
+
await (0, config_index_1.main)();
|
|
56
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/Usage: flexplm-config/));
|
|
57
|
+
});
|
|
58
|
+
it('prints usage when no command is provided', async () => {
|
|
59
|
+
setArgv();
|
|
60
|
+
await (0, config_index_1.main)();
|
|
61
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/Usage: flexplm-config/));
|
|
62
|
+
});
|
|
63
|
+
it('exits on an unknown command', async () => {
|
|
64
|
+
setArgv('bogus');
|
|
65
|
+
await expect((0, config_index_1.main)()).rejects.toThrow('__EXIT__:1');
|
|
66
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Unknown command: bogus/));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"orgName": "<ORG_NAME>",
|
|
3
|
+
"appIdentifier": "<APP_IDENTIFIER>",
|
|
4
|
+
|
|
5
|
+
"_availableAttributes": {
|
|
6
|
+
"_note": "Reference only — the connector never reads this key. Add any of the attributes below as real top-level keys if this org needs them, then delete this whole \"_availableAttributes\" block.",
|
|
7
|
+
"apiHost": "FlexPLM API host base URL (required for the connector to reach FlexPLM)",
|
|
8
|
+
"userName": "FlexPLM user name used for Basic Auth (required)",
|
|
9
|
+
"password": "FlexPLM password used for Basic Auth (required)",
|
|
10
|
+
"plmEnviornment": "Sent as the PLM_ENV header on every FlexPLM request",
|
|
11
|
+
"urlContext": "Path prefix for FlexPLM URLs. Default: '/Windchill'",
|
|
12
|
+
"csrfEndpoint": "CSRF token endpoint path. Default: '/servlet/rest/security/csrf'",
|
|
13
|
+
"vibeEventEndpoint": "Endpoint VibeIQ posts inbound events to. Default: '/rfa/vibeiq/vibeEvents'",
|
|
14
|
+
"itemPreDevelopmentLifecycleStages": "Item lifecycle stages during which items are not synced to FlexPLM. Default: ['concept']",
|
|
15
|
+
"identifierAtts": "Map of FlexPLM object class to identifier attribute name(s), e.g. { \"LCSProduct\": [\"itemNumber\"] }",
|
|
16
|
+
"LCSMaterial": "{ \"processAsItem\": true } routes LCSMaterial to item:material instead of custom-entity",
|
|
17
|
+
"sendMode": "Map of event type to send mode, e.g. { \"ASYNC_PUBLISH_SEASON\": \"vibeiqfile\" }",
|
|
18
|
+
"payloadDefaultAsArray": "Whether outbound payload values default to arrays. Default: true",
|
|
19
|
+
"flexplmConnect": "{ \"staticHeaders\": { ... } } adds custom static headers to every FlexPLM request",
|
|
20
|
+
"propertyMapping": "Reserved for custom property-mapping overrides"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface FCConfig {
|
|
|
14
14
|
urlContext: string;
|
|
15
15
|
vibeEventEndpoint: string;
|
|
16
16
|
csrfEndpoint: string;
|
|
17
|
-
useDistinctRestEndPointForImages
|
|
17
|
+
useDistinctRestEndPointForImages?: boolean;
|
|
18
18
|
itemPreDevelopmentLifecycleStages: string[];
|
|
19
19
|
identifierAtts?: {
|
|
20
20
|
[key: string]: string[];
|
|
@@ -2,6 +2,8 @@ import { FCConfig } from '../interfaces/interfaces';
|
|
|
2
2
|
export declare class ConfigDefaults {
|
|
3
3
|
static NEED_CONFIG_VALUES: string;
|
|
4
4
|
static STATIC_CONFIG_CACHE: {};
|
|
5
|
+
static PROTO_KEYS: string[];
|
|
6
|
+
static stripProtoKeys(obj: any): any;
|
|
5
7
|
static setConfigDefaults(config: any): Promise<FCConfig>;
|
|
6
8
|
static getDefaultConfig(): {
|
|
7
9
|
urlContext: string;
|
|
@@ -5,16 +5,29 @@ const sdk_1 = require("@contrail/sdk");
|
|
|
5
5
|
const util_1 = require("@contrail/util");
|
|
6
6
|
const type_defaults_1 = require("./type-defaults");
|
|
7
7
|
class ConfigDefaults {
|
|
8
|
+
static stripProtoKeys(obj) {
|
|
9
|
+
if (obj && typeof obj === 'object' && !(obj instanceof Date)) {
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
if (ConfigDefaults.PROTO_KEYS.includes(key)) {
|
|
12
|
+
delete obj[key];
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
ConfigDefaults.stripProtoKeys(obj[key]); // recurse into nested payloads
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
8
21
|
static async setConfigDefaults(config) {
|
|
9
|
-
//Validate config
|
|
22
|
+
// Validate config
|
|
10
23
|
if (!config.apiHost || !config.userName || !config.password) {
|
|
11
24
|
throw new Error(ConfigDefaults.NEED_CONFIG_VALUES);
|
|
12
25
|
}
|
|
13
26
|
if (config.complexConfig && typeof config.complexConfig === 'object') {
|
|
14
|
-
Object.assign(config, config.complexConfig);
|
|
27
|
+
Object.assign(config, ConfigDefaults.stripProtoKeys(config.complexConfig));
|
|
15
28
|
delete config.complexConfig;
|
|
16
29
|
}
|
|
17
|
-
//List will be comma separated list in UI, so convert to array
|
|
30
|
+
// List will be comma separated list in UI, so convert to array
|
|
18
31
|
if (config?.itemPreDevelopmentLifecycleStages && !(config?.itemPreDevelopmentLifecycleStages instanceof Array)) {
|
|
19
32
|
config.itemPreDevelopmentLifecycleStages = config.itemPreDevelopmentLifecycleStages.split(',');
|
|
20
33
|
}
|
|
@@ -32,7 +45,7 @@ class ConfigDefaults {
|
|
|
32
45
|
const pass = outputConfig.password;
|
|
33
46
|
outputConfig.userName = () => uName;
|
|
34
47
|
outputConfig.password = () => pass;
|
|
35
|
-
//Don't allow overwriting this.
|
|
48
|
+
// Don't allow overwriting this.
|
|
36
49
|
outputConfig['OOBvibeEventEndpoint'] = '/rfa/vibeiq/vibeEvents';
|
|
37
50
|
type_defaults_1.TypeDefaults.applyConfig(outputConfig);
|
|
38
51
|
console.log('outputConfig: ' + JSON.stringify(outputConfig));
|
|
@@ -42,21 +55,21 @@ class ConfigDefaults {
|
|
|
42
55
|
return {
|
|
43
56
|
urlContext: '/Windchill',
|
|
44
57
|
sendMode: {
|
|
45
|
-
ASYNC_PUBLISH_SEASON: 'vibeiqfile'
|
|
58
|
+
ASYNC_PUBLISH_SEASON: 'vibeiqfile',
|
|
46
59
|
},
|
|
47
60
|
itemPreDevelopmentLifecycleStages: ['concept'],
|
|
48
61
|
identifierAtts: {
|
|
49
62
|
LCSProduct: ['itemNumber'],
|
|
50
63
|
LCSSeason: ['flexPLMSeasonName'],
|
|
51
|
-
LCSSKU: ['itemNumber']
|
|
64
|
+
LCSSKU: ['itemNumber'],
|
|
52
65
|
},
|
|
53
66
|
LCSMaterial: {
|
|
54
|
-
processAsItem: false
|
|
67
|
+
processAsItem: false,
|
|
55
68
|
},
|
|
56
69
|
useDistinctRestEndPointForImages: false,
|
|
57
70
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
58
71
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
59
|
-
payloadDefaultAsArray: true
|
|
72
|
+
payloadDefaultAsArray: true,
|
|
60
73
|
};
|
|
61
74
|
}
|
|
62
75
|
static async getConfigFile(fileId) {
|
|
@@ -101,3 +114,4 @@ class ConfigDefaults {
|
|
|
101
114
|
exports.ConfigDefaults = ConfigDefaults;
|
|
102
115
|
ConfigDefaults.NEED_CONFIG_VALUES = 'To connect to FlexPLM all these APP values need to be set apiHost, userName, and password';
|
|
103
116
|
ConfigDefaults.STATIC_CONFIG_CACHE = {};
|
|
117
|
+
ConfigDefaults.PROTO_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
@@ -378,4 +378,18 @@ describe('all tests', () => {
|
|
|
378
378
|
expect(Object.keys(config).length).toEqual(0);
|
|
379
379
|
});
|
|
380
380
|
});
|
|
381
|
+
describe('prototype pollution', () => {
|
|
382
|
+
const config = {
|
|
383
|
+
apiHost: 'http://test.com',
|
|
384
|
+
userName: 'vibeiq',
|
|
385
|
+
password: 'vibeiq'
|
|
386
|
+
};
|
|
387
|
+
it('does not allow prototype pollution via complexConfig', async () => {
|
|
388
|
+
const startConfig = Object.assign({}, config, {
|
|
389
|
+
complexConfig: JSON.parse('{"__proto__":{"polluted":true}}')
|
|
390
|
+
});
|
|
391
|
+
await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
|
|
392
|
+
expect({}.polluted).toBeUndefined();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
381
395
|
});
|
|
@@ -38,8 +38,7 @@ describe('getFlexPLMValue multi_select', () => {
|
|
|
38
38
|
urlContext: 'xxx',
|
|
39
39
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
40
40
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
41
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
42
|
-
useDistinctRestEndPointForImages: false
|
|
41
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
43
42
|
};
|
|
44
43
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
45
44
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -97,8 +96,7 @@ describe('getEnumerationValue', () => {
|
|
|
97
96
|
urlContext: 'xxx',
|
|
98
97
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
99
98
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
100
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
101
|
-
useDistinctRestEndPointForImages: false
|
|
99
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
102
100
|
};
|
|
103
101
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
104
102
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -155,8 +153,7 @@ describe('getEnumerationValue multi_select', () => {
|
|
|
155
153
|
urlContext: 'xxx',
|
|
156
154
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
157
155
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
158
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
159
|
-
useDistinctRestEndPointForImages: false
|
|
156
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
160
157
|
};
|
|
161
158
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
162
159
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -216,8 +213,7 @@ describe('getPersistableChanges', () => {
|
|
|
216
213
|
urlContext: 'xxx',
|
|
217
214
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
218
215
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
219
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
220
|
-
useDistinctRestEndPointForImages: false
|
|
216
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
221
217
|
};
|
|
222
218
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
223
219
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -302,8 +298,7 @@ describe('getObjectReferenceValue cache', () => {
|
|
|
302
298
|
urlContext: 'xxx',
|
|
303
299
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
304
300
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
305
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
306
|
-
useDistinctRestEndPointForImages: false
|
|
301
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
307
302
|
};
|
|
308
303
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
309
304
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -361,8 +356,7 @@ describe('getObjectReferenceValue bad value', () => {
|
|
|
361
356
|
urlContext: 'xxx',
|
|
362
357
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
363
358
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
364
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
365
|
-
useDistinctRestEndPointForImages: false
|
|
359
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
366
360
|
};
|
|
367
361
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
368
362
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -468,8 +462,7 @@ describe('setObjectReferenceValue - identity service', () => {
|
|
|
468
462
|
urlContext: 'xxx',
|
|
469
463
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
470
464
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
471
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
472
|
-
useDistinctRestEndPointForImages: false
|
|
465
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
473
466
|
});
|
|
474
467
|
const refProp = {
|
|
475
468
|
id: 'cJoZQvoj7dkfCBJq',
|
|
@@ -927,8 +920,7 @@ describe('getEntityValues', () => {
|
|
|
927
920
|
urlContext: 'xxx',
|
|
928
921
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
929
922
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
930
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
931
|
-
useDistinctRestEndPointForImages: false
|
|
923
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
932
924
|
};
|
|
933
925
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
934
926
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -969,8 +961,7 @@ describe('setEnumerationKeys', () => {
|
|
|
969
961
|
urlContext: 'xxx',
|
|
970
962
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
971
963
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
972
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
973
|
-
useDistinctRestEndPointForImages: false
|
|
964
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
974
965
|
};
|
|
975
966
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
976
967
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1034,8 +1025,7 @@ describe('checkKeysAndValues', () => {
|
|
|
1034
1025
|
urlContext: 'xxx',
|
|
1035
1026
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1036
1027
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1037
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1038
|
-
useDistinctRestEndPointForImages: false
|
|
1028
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1039
1029
|
};
|
|
1040
1030
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1041
1031
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1112,8 +1102,7 @@ describe('filterOutArchivedAndTrashedEntities', () => {
|
|
|
1112
1102
|
urlContext: 'xxx',
|
|
1113
1103
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1114
1104
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1115
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1116
|
-
useDistinctRestEndPointForImages: false
|
|
1105
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1117
1106
|
};
|
|
1118
1107
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1119
1108
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1153,8 +1142,7 @@ describe('setUserListValue', () => {
|
|
|
1153
1142
|
urlContext: 'xxx',
|
|
1154
1143
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1155
1144
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1156
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1157
|
-
useDistinctRestEndPointForImages: false
|
|
1145
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1158
1146
|
};
|
|
1159
1147
|
const userListProp1 = {};
|
|
1160
1148
|
const userEmailMapping = [
|
|
@@ -1250,8 +1238,7 @@ describe('getUserListValue', () => {
|
|
|
1250
1238
|
urlContext: 'xxx',
|
|
1251
1239
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1252
1240
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1253
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1254
|
-
useDistinctRestEndPointForImages: false
|
|
1241
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1255
1242
|
};
|
|
1256
1243
|
const userListProp1 = {
|
|
1257
1244
|
slug: 'userList1'
|
|
@@ -1353,8 +1340,7 @@ describe('getFlexPLMValue size_range', () => {
|
|
|
1353
1340
|
urlContext: 'xxx',
|
|
1354
1341
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1355
1342
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1356
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1357
|
-
useDistinctRestEndPointForImages: false
|
|
1343
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1358
1344
|
};
|
|
1359
1345
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1360
1346
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1387,8 +1373,7 @@ describe('getEntityValue size_range', () => {
|
|
|
1387
1373
|
urlContext: 'xxx',
|
|
1388
1374
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1389
1375
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1390
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1391
|
-
useDistinctRestEndPointForImages: false
|
|
1376
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1392
1377
|
};
|
|
1393
1378
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1394
1379
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -181,7 +181,7 @@ class ThumbnailUtil {
|
|
|
181
181
|
const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
|
|
182
182
|
const encodedUrl = urlParts.map(part => encodeURIComponent(part)).join('/');
|
|
183
183
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
184
|
-
console.
|
|
184
|
+
console.debug('the useDistinctRestEndPointForImages is --> ' + this.config.useDistinctRestEndPointForImages);
|
|
185
185
|
let response;
|
|
186
186
|
if (this.config.useDistinctRestEndPointForImages) {
|
|
187
187
|
// Route through the connector endpoint (Basic auth + VIBEIQGROUP enforced there).
|
|
@@ -200,17 +200,15 @@ class ThumbnailUtil {
|
|
|
200
200
|
returnFullResponse: true,
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
|
-
// Ensure we actually receive an image content type.
|
|
204
|
-
const contTypeHeader = response.headers.get('content-type');
|
|
205
|
-
const contType = contTypeHeader ? contTypeHeader.split(';')[0] : 'application/octet-stream';
|
|
206
|
-
if (!contType.startsWith('image/')) {
|
|
207
|
-
const message = `Expected image content from FlexPLM but received '${contType}' for ${thumbnailUrl}`;
|
|
208
|
-
throw new Error(message);
|
|
209
|
-
}
|
|
210
203
|
const fileBuffer = await response.arrayBuffer();
|
|
211
204
|
const buffer = Buffer.from(fileBuffer);
|
|
212
205
|
const contentTypeHeader = response.headers.get('content-type');
|
|
213
206
|
const contentType = contentTypeHeader ? contentTypeHeader.split(';')[0] : 'application/octet-stream';
|
|
207
|
+
// Ensure we actually receive an image content type.
|
|
208
|
+
if (!contentType.startsWith('image/')) {
|
|
209
|
+
const message = `Expected image content from FlexPLM but received '${contentType}' for ${thumbnailUrl}`;
|
|
210
|
+
throw new Error(message);
|
|
211
|
+
}
|
|
214
212
|
const contentHolderReference = `${entityName}:${entityId}`;
|
|
215
213
|
const content = await new sdk_1.Content().create({
|
|
216
214
|
fileBuffer: buffer,
|
|
@@ -437,6 +437,41 @@ describe('ThumbnailUtil Tests', () => {
|
|
|
437
437
|
expect(mockEntitiesDelete).not.toHaveBeenCalled();
|
|
438
438
|
});
|
|
439
439
|
});
|
|
440
|
+
describe('syncThumbnailToVibeIQ - useDistinctRestEndPointForImages enabled', () => {
|
|
441
|
+
let tu;
|
|
442
|
+
const distinctConfig = { useDistinctRestEndPointForImages: true };
|
|
443
|
+
beforeEach(() => {
|
|
444
|
+
jest.clearAllMocks();
|
|
445
|
+
tu = new thumbnail_util_1.ThumbnailUtil(distinctConfig);
|
|
446
|
+
mockEntitiesGet.mockImplementation((opts) => {
|
|
447
|
+
if (opts.entityName === 'content-custom-size')
|
|
448
|
+
return Promise.resolve([]);
|
|
449
|
+
return Promise.resolve({});
|
|
450
|
+
});
|
|
451
|
+
mockEntitiesUpdate.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
|
|
452
|
+
mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
|
|
453
|
+
});
|
|
454
|
+
it('routes through the distinct image endpoint with includeUrlContext true', async () => {
|
|
455
|
+
const mockResponse = {
|
|
456
|
+
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
|
|
457
|
+
headers: { get: jest.fn().mockReturnValue('image/png') },
|
|
458
|
+
};
|
|
459
|
+
mockGetRequest.mockResolvedValue(mockResponse);
|
|
460
|
+
mockContentCreate.mockResolvedValue({
|
|
461
|
+
id: 'distinctContent1', contentType: 'image/png', fileName: 'thumb.png',
|
|
462
|
+
primaryFileUrl: 'https://files/primary.png', largeViewableUrl: null,
|
|
463
|
+
mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
|
|
464
|
+
});
|
|
465
|
+
const thumbnailUrl = '/rest/thumbnail/thumb.png';
|
|
466
|
+
const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: thumbnailUrl } };
|
|
467
|
+
await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
|
|
468
|
+
expect(mockGetRequest).toHaveBeenCalledWith({
|
|
469
|
+
urlPath: '/servlet/rest/rfa/vibeiq/image?path=' + encodeURIComponent(thumbnailUrl),
|
|
470
|
+
includeUrlContext: true,
|
|
471
|
+
returnFullResponse: true,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
440
475
|
describe('ThumbnailUtil - iteratedThumbnailId (THUMBNAIL key)', () => {
|
|
441
476
|
const config = {};
|
|
442
477
|
let tu;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/flexplm",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4-alpha.7bbda17",
|
|
4
4
|
"description": "Library used for integration with flexplm.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"flexplm-mapping": "lib/cli/index.js"
|
|
8
|
+
"flexplm-mapping": "lib/cli/index.js",
|
|
9
|
+
"flexplm-config": "lib/cli/config-index.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"lib/**/*",
|
package/scripts/copy-template.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const DST = path.join('lib', 'cli', 'template', 'mapping-template.ts.template');
|
|
5
|
+
const TEMPLATES = ['mapping-template.ts.template', 'config-template.json.template'];
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
for (const templateFilename of TEMPLATES) {
|
|
8
|
+
const SRC = path.join('src', 'cli', 'template', templateFilename);
|
|
9
|
+
const DST = path.join('lib', 'cli', 'template', templateFilename);
|
|
10
|
+
|
|
11
|
+
fs.mkdirSync(path.dirname(DST), { recursive: true });
|
|
12
|
+
fs.copyFileSync(SRC, DST);
|
|
13
|
+
console.log(`Copied ${SRC} -> ${DST}`);
|
|
14
|
+
}
|