@unito/integration-cli 0.64.4 → 0.64.6

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.
@@ -0,0 +1,21 @@
1
+ import { BaseCommand } from '../baseCommand';
2
+ import * as GlobalConfiguration from '../resources/globalConfiguration';
3
+ export default class Graph extends BaseCommand<typeof Graph> {
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ operation: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ catch(error: Error): Promise<void>;
10
+ static flags: {
11
+ path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ port: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ environment: import("@oclif/core/lib/interfaces").OptionFlag<GlobalConfiguration.Environment, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ 'test-account': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ 'credential-payload': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ 'credential-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ 'config-path': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ output: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ };
20
+ run(): Promise<void>;
21
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const crypto_1 = tslib_1.__importDefault(require("crypto"));
5
+ const fs_1 = tslib_1.__importDefault(require("fs"));
6
+ const core_1 = require("@oclif/core");
7
+ const baseCommand_1 = require("../baseCommand");
8
+ const errors_1 = require("../errors");
9
+ const GlobalConfiguration = tslib_1.__importStar(require("../resources/globalConfiguration"));
10
+ const integrations_1 = require("../resources/integrations");
11
+ const configuration_1 = require("../resources/configuration");
12
+ const decryption_1 = require("../resources/decryption");
13
+ const credentials_1 = require("../resources/credentials");
14
+ class Graph extends baseCommand_1.BaseCommand {
15
+ static description = 'Query a running integration graph and print the response';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> --path=/sobjects/Opportunity/records/abc123',
18
+ '<%= config.bin %> <%= command.id %> get --path=/sobjects/Opportunity --port=9201',
19
+ ];
20
+ static args = {
21
+ operation: core_1.Args.string({
22
+ description: 'Operation to perform on the graph path',
23
+ required: false,
24
+ default: 'get',
25
+ options: ['get', 'getItem', 'getCollection', 'createItem', 'updateItem', 'deleteItem'],
26
+ }),
27
+ };
28
+ async catch(error) {
29
+ /* istanbul ignore if */
30
+ if ((0, errors_1.handleError)(this, error)) {
31
+ this.exit(-1);
32
+ }
33
+ throw error;
34
+ }
35
+ static flags = {
36
+ path: core_1.Flags.string({
37
+ description: 'Graph path to fetch (must start with /), e.g. /sobjects/Opportunity/records/abc123',
38
+ required: true,
39
+ }),
40
+ port: core_1.Flags.integer({
41
+ description: 'Port the integration is running on',
42
+ default: 9200,
43
+ }),
44
+ environment: core_1.Flags.custom({
45
+ description: 'the environment of the platform',
46
+ options: Object.values(GlobalConfiguration.Environment),
47
+ default: GlobalConfiguration.Environment.Production,
48
+ })(),
49
+ 'test-account': core_1.Flags.string({
50
+ description: 'test account to use',
51
+ options: Object.values(configuration_1.CredentialScope),
52
+ default: configuration_1.CredentialScope.DEVELOPMENT,
53
+ }),
54
+ 'credential-payload': core_1.Flags.string({
55
+ description: '(advanced) credential payload to use.',
56
+ exclusive: ['credential-id'],
57
+ }),
58
+ 'credential-id': core_1.Flags.string({
59
+ description: '(advanced) credential to use.',
60
+ exclusive: ['credential-payload'],
61
+ }),
62
+ 'config-path': core_1.Flags.string({
63
+ summary: 'relative path to a custom ".unito.json" file',
64
+ description: `Use a custom configuration file instead of the default '.unito.json'.
65
+
66
+ If you want to force the CLI to use a specific configuration file, you can use this flag to specify the relative
67
+ path from your integration's root folder (with a leading '/').
68
+
69
+ Usage: <%= config.bin %> <%= command.id %> --config-path=/myCustomConfig.json`,
70
+ }),
71
+ output: core_1.Flags.string({
72
+ char: 'o',
73
+ description: 'Write response body to file instead of stdout',
74
+ }),
75
+ };
76
+ async run() {
77
+ (0, integrations_1.validateIsIntegrationDirectory)();
78
+ const { args, flags } = await this.parse(Graph);
79
+ const operation = args.operation ?? 'get';
80
+ if (operation === 'createItem' || operation === 'updateItem' || operation === 'deleteItem') {
81
+ this.error(`Operation "${operation}" is not yet implemented`, { exit: 1 });
82
+ }
83
+ const environment = flags.environment ?? GlobalConfiguration.Environment.Production;
84
+ const configuration = await (0, configuration_1.getConfiguration)(environment, flags['config-path']);
85
+ // Credential resolution — identical to dev.ts
86
+ let credentialPayload = '{}';
87
+ if (flags['credential-id']) {
88
+ const credential = await (0, credentials_1.fetchCredential)(environment, this.config.configDir, flags['credential-id']);
89
+ credentialPayload = JSON.stringify({
90
+ ...credential.payload,
91
+ unitoCredentialId: credential.id,
92
+ unitoUserId: credential.unitoUserId,
93
+ });
94
+ }
95
+ else if (flags['credential-payload']) {
96
+ credentialPayload = flags['credential-payload'];
97
+ }
98
+ else {
99
+ const credentials = configuration.testAccounts?.[flags['test-account']];
100
+ if (credentials) {
101
+ const decryptedEntries = await (0, decryption_1.decryptEntries)(configuration.name, environment, this.config.configDir, credentials);
102
+ if (decryptedEntries.failed.length) {
103
+ throw new errors_1.EntryDecryptionError(decryptedEntries.failed.at(0), environment);
104
+ }
105
+ credentialPayload = JSON.stringify({
106
+ ...decryptedEntries.successful,
107
+ unitoCredentialId: flags['test-account'],
108
+ unitoUserId: flags['test-account'],
109
+ });
110
+ }
111
+ }
112
+ // Load secrets — identical to dev.ts
113
+ const { successful: secrets, failed: failedSecrets } = await (0, decryption_1.decryptEntries)(configuration.name, environment, this.config.configDir, configuration.secrets ?? {});
114
+ if (failedSecrets.length) {
115
+ throw new errors_1.EntryDecryptionError(failedSecrets.at(0), environment);
116
+ }
117
+ // Build Unito request headers.
118
+ // Encoding: base64(JSON.stringify(payload)) — same as integrationDebugger/src/services/crawlerDriver.ts line 424.
119
+ const headers = {
120
+ 'X-Unito-Credentials': Buffer.from(credentialPayload).toString('base64'),
121
+ 'X-Unito-Secrets': Buffer.from(JSON.stringify(secrets)).toString('base64'),
122
+ 'X-Unito-Correlation-Id': crypto_1.default.randomUUID(),
123
+ 'Content-Type': 'application/json',
124
+ };
125
+ const url = `http://localhost:${flags.port}${flags.path}`;
126
+ const response = await fetch(url, { headers });
127
+ if (!response.ok) {
128
+ const body = await response.json().catch(() => undefined);
129
+ if (body !== undefined) {
130
+ this.logToStderr(JSON.stringify(body, null, 2));
131
+ }
132
+ this.error(`HTTP ${response.status} from ${url}`, { exit: response.status });
133
+ }
134
+ const body = (await response.json());
135
+ const json = JSON.stringify(body, null, 2);
136
+ if (flags.output) {
137
+ await fs_1.default.promises.writeFile(flags.output, json, 'utf8');
138
+ }
139
+ else {
140
+ this.log(json);
141
+ }
142
+ }
143
+ }
144
+ exports.default = Graph;
@@ -3,7 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const core_1 = require("@oclif/core");
5
5
  const gradient = tslib_1.__importStar(require("gradient-string"));
6
- const displayLogo = async function () {
6
+ const updateNotifier_1 = require("./updateNotifier");
7
+ const displayLogo = async function ({ id }) {
8
+ if (id === 'graph')
9
+ return;
7
10
  const gradients = [
8
11
  gradient.atlas,
9
12
  gradient.mind,
@@ -33,5 +36,6 @@ const displayLogo = async function () {
33
36
  '+------------------------------------+',
34
37
  ].join('\n'));
35
38
  core_1.ux.log(UNITOCLI_LOGO);
39
+ (0, updateNotifier_1.checkForUpdate)(this.config.version);
36
40
  };
37
41
  exports.default = displayLogo;
@@ -0,0 +1,19 @@
1
+ import child_process from 'child_process';
2
+ interface UpdateCache {
3
+ latest: string;
4
+ checkedAt: number;
5
+ }
6
+ export interface UpdateNotifierDeps {
7
+ cacheFile: string;
8
+ spawn: typeof child_process.spawn;
9
+ log: typeof console.log;
10
+ upgrade: () => void;
11
+ }
12
+ export declare function isNewerVersion(latest: string, current: string): boolean;
13
+ export declare function readCache(cacheFile: string): UpdateCache | null;
14
+ export declare function isCacheStale(cache: UpdateCache | null, intervalMs?: number): boolean;
15
+ export declare function stripAnsi(str: string): string;
16
+ export declare function formatNotification(current: string, latest: string): string;
17
+ export declare function spawnBackgroundCheck(deps: UpdateNotifierDeps): void;
18
+ export declare function checkForUpdate(currentVersion: string, overrides?: Partial<UpdateNotifierDeps>): void;
19
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isNewerVersion = isNewerVersion;
4
+ exports.readCache = readCache;
5
+ exports.isCacheStale = isCacheStale;
6
+ exports.stripAnsi = stripAnsi;
7
+ exports.formatNotification = formatNotification;
8
+ exports.spawnBackgroundCheck = spawnBackgroundCheck;
9
+ exports.checkForUpdate = checkForUpdate;
10
+ const tslib_1 = require("tslib");
11
+ const child_process_1 = tslib_1.__importDefault(require("child_process"));
12
+ const fs_1 = tslib_1.__importDefault(require("fs"));
13
+ const os_1 = tslib_1.__importDefault(require("os"));
14
+ const path_1 = tslib_1.__importDefault(require("path"));
15
+ const core_1 = require("@oclif/core");
16
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
17
+ const PACKAGE_NAME = '@unito/integration-cli';
18
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
19
+ function getDefaultCacheFile() {
20
+ return path_1.default.join(os_1.default.homedir(), '.cache', 'integrationcli', 'update-check.json');
21
+ }
22
+ function defaultUpgrade() {
23
+ child_process_1.default.execSync(`npm install --force --global ${PACKAGE_NAME}`, {
24
+ stdio: ['ignore', 'ignore', 'pipe'],
25
+ });
26
+ }
27
+ function getDefaultDeps() {
28
+ return {
29
+ cacheFile: getDefaultCacheFile(),
30
+ spawn: child_process_1.default.spawn,
31
+ log: core_1.ux.log,
32
+ upgrade: defaultUpgrade,
33
+ };
34
+ }
35
+ function isNewerVersion(latest, current) {
36
+ const latestParts = latest.split('.').map(Number);
37
+ const currentParts = current.split('.').map(Number);
38
+ for (let i = 0; i < 3; i++) {
39
+ if ((latestParts[i] || 0) > (currentParts[i] || 0))
40
+ return true;
41
+ if ((latestParts[i] || 0) < (currentParts[i] || 0))
42
+ return false;
43
+ }
44
+ return false;
45
+ }
46
+ function readCache(cacheFile) {
47
+ try {
48
+ const data = fs_1.default.readFileSync(cacheFile, 'utf8');
49
+ const parsed = JSON.parse(data);
50
+ if (typeof parsed?.latest !== 'string' || typeof parsed?.checkedAt !== 'number')
51
+ return null;
52
+ return parsed;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function isCacheStale(cache, intervalMs = CHECK_INTERVAL_MS) {
59
+ if (!cache)
60
+ return true;
61
+ return Date.now() - cache.checkedAt > intervalMs;
62
+ }
63
+ // eslint-disable-next-line no-control-regex
64
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
65
+ function stripAnsi(str) {
66
+ return str.replace(ANSI_PATTERN, '');
67
+ }
68
+ function formatNotification(current, latest) {
69
+ const npmUrl = `https://www.npmjs.com/package/${PACKAGE_NAME}`;
70
+ const lines = [
71
+ '',
72
+ ` Update available ${chalk_1.default.gray(current)} ${chalk_1.default.gray('→')} ${chalk_1.default.greenBright(latest)}`,
73
+ ` ${chalk_1.default.cyan(npmUrl)}`,
74
+ '',
75
+ ` Run ${chalk_1.default.bold('integration-cli upgrade')} to update`,
76
+ '',
77
+ ];
78
+ const maxLen = Math.max(...lines.map(line => stripAnsi(line).length));
79
+ const border = chalk_1.default.yellowBright;
80
+ const boxLines = [
81
+ border(' ┌' + '─'.repeat(maxLen) + '┐'),
82
+ ...lines.map(line => {
83
+ const padding = maxLen - stripAnsi(line).length;
84
+ return border(' │') + line + ' '.repeat(padding) + border('│');
85
+ }),
86
+ border(' └' + '─'.repeat(maxLen) + '┘'),
87
+ ];
88
+ return '\n' + boxLines.join('\n') + '\n';
89
+ }
90
+ function spawnBackgroundCheck(deps) {
91
+ const cacheDir = path_1.default.dirname(deps.cacheFile);
92
+ const script = `
93
+ const https = require('https');
94
+ const fs = require('fs');
95
+
96
+ const cacheDir = ${JSON.stringify(cacheDir)};
97
+ const cacheFile = ${JSON.stringify(deps.cacheFile)};
98
+
99
+ const url = 'https://registry.npmjs.org/${PACKAGE_NAME.replace('/', '%2f')}';
100
+
101
+ const req = https.get(url, { headers: { Accept: 'application/vnd.npm.install-v1+json' } }, (res) => {
102
+ if (res.statusCode !== 200) return;
103
+ let data = '';
104
+ res.on('data', chunk => { data += chunk; });
105
+ res.on('end', () => {
106
+ try {
107
+ const pkg = JSON.parse(data);
108
+ const latest = pkg['dist-tags']?.latest;
109
+ if (!latest) return;
110
+ fs.mkdirSync(cacheDir, { recursive: true });
111
+ fs.writeFileSync(cacheFile, JSON.stringify({ latest, checkedAt: Date.now() }));
112
+ } catch {}
113
+ });
114
+ });
115
+ req.on('error', () => {});
116
+ req.setTimeout(10000, () => { req.destroy(); });
117
+ `;
118
+ const child = deps.spawn(process.execPath, ['-e', script], {
119
+ detached: true,
120
+ stdio: 'ignore',
121
+ windowsHide: true,
122
+ });
123
+ child.unref();
124
+ }
125
+ function checkForUpdate(currentVersion, overrides) {
126
+ try {
127
+ // Skip during upgrade command — it handles its own version messaging
128
+ if (process.argv[2] === 'upgrade')
129
+ return;
130
+ // Skip in non-interactive environments (CI, tests)
131
+ if (process.env.CI)
132
+ return;
133
+ const deps = { ...getDefaultDeps(), ...overrides };
134
+ const cache = readCache(deps.cacheFile);
135
+ // Auto-upgrade if we have a cached newer version
136
+ if (cache && isNewerVersion(cache.latest, currentVersion)) {
137
+ try {
138
+ deps.log(`Upgrading integration-cli ${currentVersion} → ${cache.latest}...`);
139
+ deps.upgrade();
140
+ deps.log(`Upgraded! The new version will be used on your next command.`);
141
+ }
142
+ catch (err) {
143
+ const reason = err instanceof Error ? err.message : String(err);
144
+ deps.log(chalk_1.default.yellow(` Auto-upgrade failed: ${reason}`));
145
+ deps.log(formatNotification(currentVersion, cache.latest));
146
+ }
147
+ }
148
+ // Spawn background check if cache is stale
149
+ if (isCacheStale(cache)) {
150
+ spawnBackgroundCheck(deps);
151
+ }
152
+ }
153
+ catch {
154
+ // Never let update checking break the CLI
155
+ }
156
+ }
@@ -328,10 +328,14 @@ export declare function getCredentials({ pagination, filters, }?: {
328
328
  unitoUserId?: string;
329
329
  /** The id of the credential account for which this credential belongs to. */
330
330
  credentialAccountId?: string;
331
+ /** The credential was created after the provided date. */
332
+ createdAfter?: string;
331
333
  /** The last access to the credential account occured before the provided date. */
332
334
  lastCredentialAccountAccessAtBefore?: string;
333
335
  /** The first failure to access the credential account occured after the provided date. */
334
336
  firstFailedCredentialAccountAccessAtAfter?: string;
337
+ /** Whether to filter archived credentials. */
338
+ isArchived?: boolean;
335
339
  };
336
340
  }, opts?: Oazapfts.RequestOpts): Promise<Pagination & {
337
341
  /** The credentials. */
@@ -40,6 +40,7 @@ declare class OAuth2Service {
40
40
  private tokenRequestParameters;
41
41
  private refreshRequestParameters;
42
42
  private credentialPayload;
43
+ private legacyRedirectUrl;
43
44
  /**
44
45
  * Constructs an instance of OAuthHelper.
45
46
  * @param clientId The client ID for your OAuth application.
@@ -49,6 +50,12 @@ declare class OAuth2Service {
49
50
  * @param providerTokenUrl The URL for the token endpoint of the provider.
50
51
  */
51
52
  constructor(authorizationInfo: Oauth2Payload, environment?: Environment, credentialPayload?: Record<string, unknown>);
53
+ /**
54
+ * Returns the redirect URI for the OAuth2 flow.
55
+ * When legacyRedirectUrl is configured, uses that URL (routed through maestro).
56
+ * Otherwise, uses the standard platformServer callback URL.
57
+ */
58
+ private get redirectUri();
52
59
  /**
53
60
  * Initiate the authorization flow and redirects the user to the provider's authorization page.
54
61
  */
@@ -32,6 +32,7 @@ class OAuth2Service {
32
32
  tokenRequestParameters;
33
33
  refreshRequestParameters;
34
34
  credentialPayload;
35
+ legacyRedirectUrl;
35
36
  /**
36
37
  * Constructs an instance of OAuthHelper.
37
38
  * @param clientId The client ID for your OAuth application.
@@ -41,7 +42,7 @@ class OAuth2Service {
41
42
  * @param providerTokenUrl The URL for the token endpoint of the provider.
42
43
  */
43
44
  constructor(authorizationInfo, environment = globalConfiguration_1.Environment.Production, credentialPayload) {
44
- const { clientId, clientSecret, authorizationUrl, scopes, tokenUrl, grantType, requestContentType, refreshRequestParameters, tokenRequestParameters, } = authorizationInfo;
45
+ const { clientId, clientSecret, authorizationUrl, scopes, tokenUrl, grantType, requestContentType, refreshRequestParameters, tokenRequestParameters, legacyRedirectUrl, } = authorizationInfo;
45
46
  this.clientId = clientId;
46
47
  this.clientSecret = clientSecret;
47
48
  this.providerAuthorizationUrl = authorizationUrl;
@@ -51,12 +52,22 @@ class OAuth2Service {
51
52
  this.requestContentType = requestContentType ?? configurationTypes_1.RequestContentType.URL_ENCODED;
52
53
  this.tokenRequestParameters = tokenRequestParameters;
53
54
  this.refreshRequestParameters = refreshRequestParameters;
55
+ this.legacyRedirectUrl = legacyRedirectUrl;
54
56
  this.environment = environment;
55
57
  this.credentialPayload = credentialPayload;
56
58
  if (!Object.values(configurationTypes_1.RequestContentType).includes(this.requestContentType)) {
57
59
  throw new errors_1.UnsupportedContentTypeError(`Request content type not supported: ${this.requestContentType}`);
58
60
  }
59
61
  }
62
+ /**
63
+ * Returns the redirect URI for the OAuth2 flow.
64
+ * When legacyRedirectUrl is configured, uses that URL (routed through maestro).
65
+ * Otherwise, uses the standard platformServer callback URL.
66
+ */
67
+ get redirectUri() {
68
+ return (this.legacyRedirectUrl ??
69
+ `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback`);
70
+ }
60
71
  /**
61
72
  * Initiate the authorization flow and redirects the user to the provider's authorization page.
62
73
  */
@@ -71,12 +82,18 @@ class OAuth2Service {
71
82
  if (this.scopes) {
72
83
  authorizationParams.set('scope', this.scopes.join(' '));
73
84
  }
74
- const state = Buffer.from(JSON.stringify({
85
+ const statePayload = {
75
86
  cliCallbackUrl: `${this.serverUrl}/oauth2/callback`,
76
- })).toString('base64');
87
+ // When using legacyRedirectUrl, the callback is routed through maestro first.
88
+ // maestro checks for redirectToIPS in state and forwards to platformServer's oauth2Callback,
89
+ // which then detects cliCallbackUrl and forwards to the CLI via ngrok.
90
+ // TLDR: redirectToIPS makes maestro act as a pass-through relay instead of handling the callback itself.
91
+ ...(this.legacyRedirectUrl ? { redirectToIPS: 'true' } : {}),
92
+ };
93
+ const state = Buffer.from(JSON.stringify(statePayload)).toString('base64');
77
94
  authorizationParams.set('state', state);
78
95
  authorizationParams.set('response_type', 'code');
79
- authorizationParams.set('redirect_uri', `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback-cli`);
96
+ authorizationParams.set('redirect_uri', this.redirectUri);
80
97
  const delimiter = this.providerAuthorizationUrl.includes('?') ? '&' : '?';
81
98
  const authorizationUrlTemplate = `${this.providerAuthorizationUrl}${delimiter}${authorizationParams.toString()}`;
82
99
  const authorizationUrl = (0, template_1.expandTemplate)(authorizationUrlTemplate, {
@@ -119,7 +136,7 @@ class OAuth2Service {
119
136
  ])),
120
137
  grant_type: this.grantType,
121
138
  code: req.query.code,
122
- redirect_uri: `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback-cli`,
139
+ redirect_uri: this.redirectUri,
123
140
  ...(this.clientId && { client_id: this.clientId }),
124
141
  ...(this.clientSecret && { client_secret: this.clientSecret }),
125
142
  };
@@ -235,7 +252,7 @@ class OAuth2Service {
235
252
  bodyData.client_assertion = this.clientSecret;
236
253
  bodyData.assertion = refreshToken;
237
254
  bodyData.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
238
- bodyData.redirect_uri = `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback-cli`;
255
+ bodyData.redirect_uri = `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback`;
239
256
  }
240
257
  const templateVariables = structuredClone(this.credentialPayload ?? {});
241
258
  templateVariables.clientId ??= this.clientId;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs_1 = tslib_1.__importDefault(require("fs"));
5
+ const test_1 = require("@oclif/test");
6
+ const sinon_1 = tslib_1.__importDefault(require("sinon"));
7
+ const Configuration = tslib_1.__importStar(require("../../src/resources/configuration"));
8
+ const Decryption = tslib_1.__importStar(require("../../src/resources/decryption"));
9
+ const Integrations = tslib_1.__importStar(require("../../src/resources/integrations"));
10
+ const MOCK_CONFIG = {
11
+ name: 'salesforce-v2',
12
+ testAccounts: {
13
+ development: { token: 'test-token' },
14
+ },
15
+ secrets: {},
16
+ };
17
+ const MOCK_RESPONSE = {
18
+ label: 'Opportunity',
19
+ fields: [{ name: 'Name', type: 'string' }],
20
+ relations: [{ name: 'tasks', semantic: 'subtasks', path: '/sobjects/Task/records?filter=WhatId=abc' }],
21
+ };
22
+ describe('graph', () => {
23
+ beforeEach(() => {
24
+ sinon_1.default.stub(Integrations, 'validateIsIntegrationDirectory');
25
+ sinon_1.default.stub(Decryption, 'decryptEntries').resolves({
26
+ successful: { token: 'test-token' },
27
+ failed: [],
28
+ });
29
+ });
30
+ afterEach(() => sinon_1.default.restore());
31
+ test_1.test
32
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
33
+ .stub(global, 'fetch', stub => stub.resolves({
34
+ ok: true,
35
+ status: 200,
36
+ json: () => Promise.resolve(MOCK_RESPONSE),
37
+ }))
38
+ .stdout()
39
+ .command(['graph', '--path=/sobjects/Opportunity/records/abc123'])
40
+ .it('prints pretty-printed JSON response to stdout', ctx => {
41
+ (0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
42
+ (0, test_1.expect)(ctx.stdout).to.contain('"semantic": "subtasks"');
43
+ });
44
+ test_1.test
45
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
46
+ .stub(global, 'fetch', stub => stub.resolves({
47
+ ok: true,
48
+ status: 200,
49
+ json: () => Promise.resolve(MOCK_RESPONSE),
50
+ }))
51
+ .stdout()
52
+ .command(['graph', '--path=/sobjects/Opportunity/records/abc123'])
53
+ .it('calls fetch with the correct default-port URL', () => {
54
+ const fetchStub = global.fetch;
55
+ (0, test_1.expect)(fetchStub.calledOnce).to.be.true;
56
+ const [calledUrl] = fetchStub.firstCall.args;
57
+ (0, test_1.expect)(calledUrl).to.equal('http://localhost:9200/sobjects/Opportunity/records/abc123');
58
+ });
59
+ test_1.test
60
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
61
+ .stub(global, 'fetch', stub => stub.resolves({
62
+ ok: true,
63
+ status: 200,
64
+ json: () => Promise.resolve(MOCK_RESPONSE),
65
+ }))
66
+ .stdout()
67
+ .command(['graph', '--path=/sobjects/Opportunity', '--port=9201'])
68
+ .it('uses custom port when specified', () => {
69
+ const fetchStub = global.fetch;
70
+ const [calledUrl] = fetchStub.firstCall.args;
71
+ (0, test_1.expect)(calledUrl).to.equal('http://localhost:9201/sobjects/Opportunity');
72
+ });
73
+ test_1.test
74
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
75
+ .stub(global, 'fetch', stub => stub.resolves({
76
+ ok: false,
77
+ status: 404,
78
+ json: () => Promise.resolve({ error: 'Not found' }),
79
+ }))
80
+ .stdout()
81
+ .stderr()
82
+ .command(['graph', '--path=/sobjects/NonExistent/records/abc'])
83
+ .catch(err => {
84
+ (0, test_1.expect)(err.message).to.contain('404');
85
+ (0, test_1.expect)(err.oclif?.exit).to.equal(404);
86
+ })
87
+ .it('exits with HTTP status code and prints error body to stderr', ctx => {
88
+ (0, test_1.expect)(ctx.stderr).to.contain('"error": "Not found"');
89
+ });
90
+ test_1.test
91
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
92
+ .stub(global, 'fetch', stub => stub.resolves({
93
+ ok: true,
94
+ status: 200,
95
+ json: () => Promise.resolve(MOCK_RESPONSE),
96
+ }))
97
+ .stdout()
98
+ .command(['graph', '--path=/sobjects/Opportunity'])
99
+ .it('sets base64-encoded X-Unito-Credentials header', () => {
100
+ const fetchStub = global.fetch;
101
+ const [, options] = fetchStub.firstCall.args;
102
+ const decoded = Buffer.from(options.headers['X-Unito-Credentials'], 'base64').toString('utf8');
103
+ const credentials = JSON.parse(decoded);
104
+ (0, test_1.expect)(credentials).to.have.property('token', 'test-token');
105
+ });
106
+ test_1.test
107
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
108
+ .stub(global, 'fetch', stub => stub.resolves({
109
+ ok: true,
110
+ status: 200,
111
+ json: () => Promise.resolve(MOCK_RESPONSE),
112
+ }))
113
+ .stub(fs_1.default.promises, 'writeFile', stub => stub.resolves())
114
+ .stdout()
115
+ .command(['graph', '--path=/sobjects/Opportunity', '--output=/tmp/response.json'])
116
+ .it('writes response to file when --output is provided', ctx => {
117
+ const writeStub = fs_1.default.promises.writeFile;
118
+ (0, test_1.expect)(writeStub.calledOnce).to.be.true;
119
+ const [path, content] = writeStub.firstCall.args;
120
+ (0, test_1.expect)(path).to.equal('/tmp/response.json');
121
+ (0, test_1.expect)(content).to.contain('"label": "Opportunity"');
122
+ (0, test_1.expect)(ctx.stdout).to.equal('');
123
+ });
124
+ test_1.test
125
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
126
+ .stub(global, 'fetch', stub => stub.resolves({
127
+ ok: true,
128
+ status: 200,
129
+ json: () => Promise.resolve(MOCK_RESPONSE),
130
+ }))
131
+ .stdout()
132
+ .command(['graph', '--path=/sobjects/Opportunity'])
133
+ .it('prints to stdout when --output is not provided', ctx => {
134
+ (0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
135
+ });
136
+ test_1.test
137
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
138
+ .stub(global, 'fetch', stub => stub.resolves({
139
+ ok: true,
140
+ status: 200,
141
+ json: () => Promise.resolve(MOCK_RESPONSE),
142
+ }))
143
+ .stdout()
144
+ .command(['graph', 'get', '--path=/sobjects/Opportunity'])
145
+ .it('accepts explicit "get" operation arg', ctx => {
146
+ (0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
147
+ });
148
+ test_1.test
149
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
150
+ .stub(global, 'fetch', stub => stub.resolves({
151
+ ok: true,
152
+ status: 200,
153
+ json: () => Promise.resolve(MOCK_RESPONSE),
154
+ }))
155
+ .stdout()
156
+ .command(['graph', 'getItem', '--path=/sobjects/Opportunity'])
157
+ .it('treats "getItem" as a get operation', ctx => {
158
+ (0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
159
+ });
160
+ test_1.test
161
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
162
+ .stub(global, 'fetch', stub => stub.resolves({
163
+ ok: true,
164
+ status: 200,
165
+ json: () => Promise.resolve(MOCK_RESPONSE),
166
+ }))
167
+ .stdout()
168
+ .command(['graph', 'getCollection', '--path=/sobjects/Opportunity'])
169
+ .it('treats "getCollection" as a get operation', ctx => {
170
+ (0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
171
+ });
172
+ for (const op of ['createItem', 'updateItem', 'deleteItem']) {
173
+ test_1.test
174
+ .stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
175
+ .command(['graph', op, '--path=/sobjects/Opportunity'])
176
+ .catch(err => {
177
+ (0, test_1.expect)(err.message).to.contain(`Operation "${op}" is not yet implemented`);
178
+ })
179
+ .it(`rejects "${op}" as not yet implemented`);
180
+ }
181
+ test_1.test
182
+ .stderr()
183
+ .command(['graph', 'put', '--path=/sobjects/Opportunity'])
184
+ .catch(err => {
185
+ (0, test_1.expect)(err.message).to.contain('Expected put to be one of');
186
+ })
187
+ .it('rejects invalid operation arg');
188
+ });