@ductape/cli 0.3.11 → 0.3.13
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/dist/commands/marketplace.d.ts +1 -0
- package/dist/commands/marketplace.js +1 -1
- package/dist/commands/products.d.ts +10 -0
- package/dist/commands/products.js +39 -6
- package/dist/commands/resources.d.ts +3 -0
- package/dist/commands/resources.js +90 -14
- package/dist/index.js +38 -3
- package/dist/lib/api-client.js +22 -10
- package/dist/lib/http.d.ts +9 -2
- package/dist/lib/http.js +23 -9
- package/dist/lib/operation-error.d.ts +17 -0
- package/dist/lib/operation-error.js +20 -0
- package/dist/lib/platform-api.d.ts +9 -2
- package/dist/lib/platform-api.js +221 -9
- package/dist/lib/proxy/sdk-proxy.d.ts +1 -1
- package/dist/lib/proxy/sdk-proxy.js +6 -1
- package/dist/lib/resources.js +21 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ export async function runMarketplace(verb, opts, args) {
|
|
|
17
17
|
const tag = opts.tag ?? args[0];
|
|
18
18
|
if (!tag)
|
|
19
19
|
throw new Error('marketplace get requires <tag> or --tag <tag>');
|
|
20
|
-
result = await getMarketplaceApp(ctx, tag);
|
|
20
|
+
result = await getMarketplaceApp(ctx, tag, Boolean(opts.full));
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
23
|
case 'categories':
|
|
@@ -10,8 +10,18 @@ export declare function runProducts(verb: string, opts: {
|
|
|
10
10
|
export declare function runProductApps(verb: string, opts: {
|
|
11
11
|
profile?: string;
|
|
12
12
|
product?: string;
|
|
13
|
+
app?: string;
|
|
14
|
+
envMap?: string[];
|
|
13
15
|
json?: boolean;
|
|
16
|
+
full?: boolean;
|
|
14
17
|
}, extraArgs: string[]): Promise<void>;
|
|
18
|
+
export declare function runProductAppActions(verb: string, opts: {
|
|
19
|
+
profile?: string;
|
|
20
|
+
product?: string;
|
|
21
|
+
app?: string;
|
|
22
|
+
action?: string;
|
|
23
|
+
json?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
15
25
|
export declare function runProductComponents(verb: string, opts: {
|
|
16
26
|
profile?: string;
|
|
17
27
|
tag?: string;
|
|
@@ -2,7 +2,7 @@ import { schemaKeyForEntity } from '../lib/forms/index.js';
|
|
|
2
2
|
import { isInteractive, promptTag } from '../lib/forms/prompt.js';
|
|
3
3
|
import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
4
4
|
import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
5
|
-
import { createProduct, deleteProduct, getProduct, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
|
|
5
|
+
import { connectMarketplaceApp, createProduct, deleteProduct, getProduct, getProductAppAction, listProductAppActions, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
|
|
6
6
|
import { printJson } from '../lib/output.js';
|
|
7
7
|
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
8
8
|
import { getSdkProxyForContext, getOptionalProject } from '../lib/proxy/context.js';
|
|
@@ -63,16 +63,49 @@ export async function runProducts(verb, opts, extraArgs) {
|
|
|
63
63
|
printJson(result, Boolean(opts.json));
|
|
64
64
|
}
|
|
65
65
|
export async function runProductApps(verb, opts, extraArgs) {
|
|
66
|
-
if (verb.toLowerCase() !== 'list') {
|
|
67
|
-
throw new Error('Only supported: ductape products apps list --product <id>');
|
|
68
|
-
}
|
|
69
66
|
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
70
67
|
const productId = opts.product ?? extraArgs[0];
|
|
71
68
|
if (!productId)
|
|
72
|
-
throw new Error('Provide --product <
|
|
73
|
-
|
|
69
|
+
throw new Error('Provide --product <product_id_or_tag>');
|
|
70
|
+
let result;
|
|
71
|
+
if (verb.toLowerCase() === 'list') {
|
|
72
|
+
result = await listProductApps(ctx, productId, Boolean(opts.full));
|
|
73
|
+
}
|
|
74
|
+
else if (verb.toLowerCase() === 'connect') {
|
|
75
|
+
if (!opts.app)
|
|
76
|
+
throw new Error('Provide --app <marketplace_app_tag>');
|
|
77
|
+
const envs = (opts.envMap ?? []).map((mapping) => {
|
|
78
|
+
const [appEnv, productEnv, ...extra] = mapping.split(':');
|
|
79
|
+
if (!appEnv || !productEnv || extra.length) {
|
|
80
|
+
throw new Error(`Invalid --env-map "${mapping}"; expected <app_env>:<product_env>`);
|
|
81
|
+
}
|
|
82
|
+
return { app_env_slug: appEnv, product_env_slug: productEnv };
|
|
83
|
+
});
|
|
84
|
+
result = await connectMarketplaceApp(ctx, productId, opts.app, envs);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error('Supported verbs: list | connect');
|
|
88
|
+
}
|
|
74
89
|
printJson(result, Boolean(opts.json));
|
|
75
90
|
}
|
|
91
|
+
export async function runProductAppActions(verb, opts) {
|
|
92
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
93
|
+
if (!opts.product)
|
|
94
|
+
throw new Error('Provide --product <product_id_or_tag>');
|
|
95
|
+
if (!opts.app)
|
|
96
|
+
throw new Error('Provide --app <app_tag>');
|
|
97
|
+
if (verb === 'list') {
|
|
98
|
+
printJson(await listProductAppActions(ctx, opts.product, opts.app), Boolean(opts.json));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (verb === 'get') {
|
|
102
|
+
if (!opts.action)
|
|
103
|
+
throw new Error('Provide --action <action_tag>');
|
|
104
|
+
printJson(await getProductAppAction(ctx, opts.product, opts.app, opts.action), Boolean(opts.json));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
throw new Error('Supported verbs: list | get');
|
|
108
|
+
}
|
|
76
109
|
const COMPONENT_KEYS = {
|
|
77
110
|
apps: ['apps'],
|
|
78
111
|
databases: ['databases'],
|
|
@@ -16,6 +16,9 @@ export declare function runEventTopicCrud(verb: string, opts: {
|
|
|
16
16
|
file?: string;
|
|
17
17
|
dir?: string;
|
|
18
18
|
json?: boolean;
|
|
19
|
+
resume?: boolean;
|
|
20
|
+
concurrency?: string;
|
|
21
|
+
timeout?: string;
|
|
19
22
|
} & CommandInteractiveFlags, extraArgs: string[]): Promise<void>;
|
|
20
23
|
export declare function runNotificationMessageCrud(verb: string, opts: {
|
|
21
24
|
tag?: string;
|
|
@@ -5,6 +5,7 @@ import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
|
5
5
|
import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
6
6
|
import { requireSession, getSdkProxy } from '../lib/proxy/context.js';
|
|
7
7
|
import { printJson } from '../lib/output.js';
|
|
8
|
+
import { DuctapeOperationError } from '../lib/operation-error.js';
|
|
8
9
|
import { readCanonicalTopicFile, topicBodyForTransport, validateEventTopicsProject, } from '../lib/event-topics.js';
|
|
9
10
|
const CRUD_VERBS = ['create', 'list', 'get', 'update', 'delete', 'connect'];
|
|
10
11
|
export function resolveResourceProductTag(override, linkedProductTag) {
|
|
@@ -67,20 +68,55 @@ export async function runEventTopicCrud(verb, opts, extraArgs) {
|
|
|
67
68
|
const proxy = getSdkProxy(session);
|
|
68
69
|
if (verb === 'create-all') {
|
|
69
70
|
const validation = await validateEventTopicsProject(process.cwd(), opts.dir);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
const concurrency = positiveInteger(opts.concurrency, 1, '--concurrency');
|
|
72
|
+
const timeoutMs = positiveInteger(opts.timeout, 120_000, '--timeout');
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
const items = new Array(validation.topics.length);
|
|
75
|
+
let cursor = 0;
|
|
76
|
+
const worker = async () => {
|
|
77
|
+
while (true) {
|
|
78
|
+
const index = cursor++;
|
|
79
|
+
const asset = validation.topics[index];
|
|
80
|
+
if (!asset)
|
|
81
|
+
return;
|
|
82
|
+
const tag = asset.definition.tag;
|
|
83
|
+
if (Date.now() >= deadline) {
|
|
84
|
+
items[index] = { tag, status: 'not_attempted', error: `Total timeout of ${timeoutMs}ms reached` };
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const existing = await fetchTopicIfPresent(proxy, productTag, tag);
|
|
89
|
+
if (existing) {
|
|
90
|
+
items[index] = { tag, status: 'already_exists', topic: existing };
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await proxy.execute('messageBrokers', 'topics.create', [productTag, topicBodyForTransport(asset.definition)]);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const reconciled = await fetchTopicIfPresent(proxy, productTag, tag);
|
|
98
|
+
if (reconciled) {
|
|
99
|
+
items[index] = { tag, status: 'created_after_reconciliation', topic: reconciled };
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const verified = await fetchTopicIfPresent(proxy, productTag, tag);
|
|
105
|
+
if (!verified)
|
|
106
|
+
throw new Error(`Creation returned without an error, but the topic was not found during verification`);
|
|
107
|
+
items[index] = { tag, status: 'created', topic: verified };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
items[index] = { tag, status: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
111
|
+
}
|
|
80
112
|
}
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
113
|
+
};
|
|
114
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, validation.topics.length) }, worker));
|
|
115
|
+
const counts = items.reduce((result, item) => {
|
|
116
|
+
result[item.status] = (result[item.status] ?? 0) + 1;
|
|
117
|
+
return result;
|
|
118
|
+
}, {});
|
|
119
|
+
printJson({ complete: !counts.failed && !counts.not_attempted, resume: Boolean(opts.resume), counts, topics: items }, Boolean(opts.json));
|
|
84
120
|
return;
|
|
85
121
|
}
|
|
86
122
|
const interactive = toInteractiveOpts(opts);
|
|
@@ -149,6 +185,24 @@ export async function runEventTopicCrud(verb, opts, extraArgs) {
|
|
|
149
185
|
}
|
|
150
186
|
printJson(result, Boolean(opts.json));
|
|
151
187
|
}
|
|
188
|
+
function positiveInteger(value, fallback, option) {
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
return fallback;
|
|
191
|
+
const parsed = Number(value);
|
|
192
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
193
|
+
throw new Error(`${option} must be a positive integer`);
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
async function fetchTopicIfPresent(proxy, productTag, tag) {
|
|
197
|
+
try {
|
|
198
|
+
return await proxy.execute('messageBrokers', 'topics.fetch', [productTag, tag]) ?? null;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (/not[ -]?found|does not exist|404/i.test(error instanceof Error ? error.message : String(error)))
|
|
202
|
+
return null;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
152
206
|
export async function runNotificationMessageCrud(verb, opts) {
|
|
153
207
|
const crud = verb.toLowerCase();
|
|
154
208
|
if (!['create', 'list', 'get', 'update'].includes(crud)) {
|
|
@@ -215,6 +269,7 @@ export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
|
215
269
|
}
|
|
216
270
|
}
|
|
217
271
|
const method = proxyMethodForVerb(crud);
|
|
272
|
+
const proxyMethod = module === 'models' && method === 'list' ? 'fetchAll' : method;
|
|
218
273
|
const interactive = toInteractiveOpts(opts);
|
|
219
274
|
let tag = opts.tag ?? extraArgs[0];
|
|
220
275
|
if (!tag && ['get', 'update', 'delete'].includes(crud) && isInteractive(interactive)) {
|
|
@@ -251,7 +306,28 @@ export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
|
251
306
|
printJson(await listSessionsWithProductFallback(proxy, productTag), Boolean(opts.json));
|
|
252
307
|
return;
|
|
253
308
|
}
|
|
254
|
-
|
|
309
|
+
let result;
|
|
310
|
+
try {
|
|
311
|
+
result = await proxy.execute(module, proxyMethod, params);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const createdTag = String(body?.tag ?? '').trim();
|
|
315
|
+
if (crud !== 'create' || !createdTag || !(error instanceof DuctapeOperationError) || error.details.mutationState !== 'unknown') {
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const reconciled = await proxy.execute(module, 'fetch', buildCrudParams(module, 'fetch', productTag, { tag: createdTag }));
|
|
320
|
+
if (!reconciled)
|
|
321
|
+
throw error;
|
|
322
|
+
printJson({ created: true, reconciled: true, outcome: 'succeeded_after_reconciliation', resource: reconciled }, Boolean(opts.json));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
catch (reconcileError) {
|
|
326
|
+
if (reconcileError === error)
|
|
327
|
+
throw error;
|
|
328
|
+
throw new DuctapeOperationError(`${error.message} Reconciliation could not confirm whether "${createdTag}" exists.`, { ...error.details, code: 'MUTATION_OUTCOME_UNKNOWN', mutationState: 'unknown' }, { cause: reconcileError });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
255
331
|
if (crud === 'create' && module === 'sessions') {
|
|
256
332
|
const createdTag = String(body?.tag ?? '').trim();
|
|
257
333
|
if (!createdTag) {
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { runSecretImportEnv, runSecrets } from './commands/secrets.js';
|
|
|
22
22
|
import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
|
|
23
23
|
import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
|
|
24
24
|
import { runCompletion } from './commands/completion.js';
|
|
25
|
-
import { runProducts, runProductApps, runProductComponents, runProductEnvironments } from './commands/products.js';
|
|
25
|
+
import { runProducts, runProductApps, runProductAppActions, runProductComponents, runProductEnvironments } from './commands/products.js';
|
|
26
26
|
import { runApps } from './commands/apps.js';
|
|
27
27
|
import { runAppsImport } from './commands/apps-import.js';
|
|
28
28
|
import { runMarketplace } from './commands/marketplace.js';
|
|
@@ -42,6 +42,7 @@ import { runMigrationVerificationValidate } from './commands/migration-verificat
|
|
|
42
42
|
import { runMigrationArtifactMigrate, runMigrationArtifactRecover, runMigrationArtifactSchemas, runMigrationArtifactValidate, } from './commands/migration-artifact.js';
|
|
43
43
|
import { runMigrationCapabilities } from './commands/migration-capabilities.js';
|
|
44
44
|
import { structuredMigrationError } from './lib/migration-artifact.js';
|
|
45
|
+
import { DuctapeOperationError } from './lib/operation-error.js';
|
|
45
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
47
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
47
48
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -51,7 +52,9 @@ function wrap(fn) {
|
|
|
51
52
|
const command = process.argv[2] ?? '';
|
|
52
53
|
console.error(command === 'migrate-codebase' || command.startsWith('migration-')
|
|
53
54
|
? structuredMigrationError(err)
|
|
54
|
-
: err instanceof
|
|
55
|
+
: err instanceof DuctapeOperationError && process.argv.includes('--json')
|
|
56
|
+
? JSON.stringify(err.toJSON())
|
|
57
|
+
: err instanceof Error ? err.message : err);
|
|
55
58
|
process.exit(1);
|
|
56
59
|
});
|
|
57
60
|
};
|
|
@@ -530,12 +533,40 @@ const products = program
|
|
|
530
533
|
return runProducts(verb, opts, []);
|
|
531
534
|
}));
|
|
532
535
|
const productApps = products.command('apps').description('Apps connected to a product');
|
|
536
|
+
function collectOption(value, previous) {
|
|
537
|
+
return previous.concat(value);
|
|
538
|
+
}
|
|
533
539
|
productApps
|
|
534
540
|
.command('list')
|
|
535
541
|
.option('--profile <name>')
|
|
536
|
-
.option('--product <id>', 'Product _id')
|
|
542
|
+
.option('--product <id-or-tag>', 'Product _id or tag')
|
|
543
|
+
.option('--full', 'Include complete app versions, actions, schemas, and responses')
|
|
537
544
|
.option('--json', 'JSON output')
|
|
538
545
|
.action(wrap((opts) => runProductApps('list', opts, [])));
|
|
546
|
+
productApps
|
|
547
|
+
.command('connect')
|
|
548
|
+
.requiredOption('--product <id-or-tag>', 'Product _id or tag')
|
|
549
|
+
.requiredOption('--app <tag>', 'Public marketplace app tag')
|
|
550
|
+
.requiredOption('--env-map <app-env:product-env>', 'Environment mapping; repeat for every product environment', collectOption, [])
|
|
551
|
+
.option('--profile <name>')
|
|
552
|
+
.option('--json', 'JSON output')
|
|
553
|
+
.action(wrap((opts) => runProductApps('connect', opts, [])));
|
|
554
|
+
const productAppActions = productApps.command('actions').description('Actions exposed by an app linked to a product');
|
|
555
|
+
productAppActions
|
|
556
|
+
.command('list')
|
|
557
|
+
.requiredOption('--product <id-or-tag>', 'Product _id or tag')
|
|
558
|
+
.requiredOption('--app <tag>', 'Linked app tag')
|
|
559
|
+
.option('--profile <name>')
|
|
560
|
+
.option('--json', 'JSON output')
|
|
561
|
+
.action(wrap((opts) => runProductAppActions('list', opts)));
|
|
562
|
+
productAppActions
|
|
563
|
+
.command('get')
|
|
564
|
+
.requiredOption('--product <id-or-tag>', 'Product _id or tag')
|
|
565
|
+
.requiredOption('--app <tag>', 'Linked app tag')
|
|
566
|
+
.requiredOption('--action <tag>', 'Action tag')
|
|
567
|
+
.option('--profile <name>')
|
|
568
|
+
.option('--json', 'JSON output')
|
|
569
|
+
.action(wrap((opts) => runProductAppActions('get', opts)));
|
|
539
570
|
const productComponents = products
|
|
540
571
|
.command('components')
|
|
541
572
|
.description('Compact, non-secret product component inventory');
|
|
@@ -635,6 +666,7 @@ program
|
|
|
635
666
|
.option('-c, --category <category>', 'Filter by category/domain name')
|
|
636
667
|
.option('-l, --limit <count>', 'Maximum results', '20')
|
|
637
668
|
.option('-t, --tag <tag>', 'Marketplace app tag for get')
|
|
669
|
+
.option('--full', 'Return the complete marketplace app definition for get')
|
|
638
670
|
.option('--json', 'JSON output')
|
|
639
671
|
.action(wrap((verb, terms, opts) => runMarketplace(verb, opts, terms)));
|
|
640
672
|
const events = program.command('events').description('Message broker CRUD (sdk-proxy, requires access key login)');
|
|
@@ -644,6 +676,9 @@ eventTopics
|
|
|
644
676
|
.option('-t, --tag <tag>', 'Broker tag (list) or topic tag in broker:topic form (get, update, delete)')
|
|
645
677
|
.option('-f, --file <path>', 'JSON body (create, update)')
|
|
646
678
|
.option('--dir <path>', 'Events asset directory (must resolve to ductape/events)', 'ductape/events')
|
|
679
|
+
.option('--resume', 'Reconcile live topic state and continue incomplete imports')
|
|
680
|
+
.option('--concurrency <count>', 'Maximum concurrent topic operations', '1')
|
|
681
|
+
.option('--timeout <ms>', 'Total create-all deadline in milliseconds', '120000')
|
|
647
682
|
.option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
|
|
648
683
|
.option('--no-interactive', 'Require -f JSON for create/update')
|
|
649
684
|
.option('--json', 'JSON output')
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getApiUrl } from './config.js';
|
|
2
|
+
import { parseJsonResponse } from './http.js';
|
|
3
|
+
import { DuctapeOperationError, safeEndpoint } from './operation-error.js';
|
|
2
4
|
export class ApiClient {
|
|
3
5
|
apiUrl;
|
|
4
6
|
token;
|
|
@@ -23,22 +25,32 @@ export class ApiClient {
|
|
|
23
25
|
headers,
|
|
24
26
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
25
27
|
});
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new Error(`Invalid JSON from ${url}: ${text.slice(0, 200)}`);
|
|
33
|
-
}
|
|
28
|
+
const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase());
|
|
29
|
+
const parsed = await parseJsonResponse(res, url, {
|
|
30
|
+
operation: `${method.toUpperCase()} ${path.split('?')[0]}`,
|
|
31
|
+
mutationState: mutating ? 'unknown' : 'not_attempted',
|
|
32
|
+
});
|
|
34
33
|
if (!res.ok) {
|
|
35
34
|
const msg = parsed.message ??
|
|
36
35
|
parsed.error ??
|
|
37
36
|
`HTTP ${res.status}`;
|
|
38
|
-
throw new
|
|
37
|
+
throw new DuctapeOperationError(msg, {
|
|
38
|
+
code: 'HTTP_ERROR', operation: `${method.toUpperCase()} ${path.split('?')[0]}`,
|
|
39
|
+
endpoint: safeEndpoint(url), httpStatus: res.status,
|
|
40
|
+
requestId: res.headers.get('x-request-id') ?? res.headers.get('x-correlation-id') ?? undefined,
|
|
41
|
+
retryable: res.status === 408 || res.status === 429 || res.status >= 500,
|
|
42
|
+
mutationState: mutating ? 'unknown' : 'not_attempted',
|
|
43
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
44
|
+
});
|
|
39
45
|
}
|
|
40
46
|
if (typeof parsed.status === 'boolean' && !parsed.status) {
|
|
41
|
-
throw new
|
|
47
|
+
throw new DuctapeOperationError(parsed.message ?? 'Request failed', {
|
|
48
|
+
code: 'OPERATION_FAILED', operation: `${method.toUpperCase()} ${path.split('?')[0]}`,
|
|
49
|
+
endpoint: safeEndpoint(url), httpStatus: res.status,
|
|
50
|
+
requestId: res.headers.get('x-request-id') ?? res.headers.get('x-correlation-id') ?? undefined,
|
|
51
|
+
retryable: false, mutationState: mutating ? 'confirmed_failed' : 'not_attempted',
|
|
52
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
53
|
+
});
|
|
42
54
|
}
|
|
43
55
|
const data = parsed.data;
|
|
44
56
|
if (data && typeof data === 'object' && 'result' in data) {
|
package/dist/lib/http.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { type MutationState } from './operation-error.js';
|
|
1
2
|
/** Parse fetch body as JSON; surface HTML/misconfigured api_url clearly. */
|
|
2
|
-
export declare function parseJsonResponse<T = Record<string, unknown>>(res: Response, url: string
|
|
3
|
+
export declare function parseJsonResponse<T = Record<string, unknown>>(res: Response, url: string, context?: {
|
|
4
|
+
operation?: string;
|
|
5
|
+
mutationState?: MutationState;
|
|
6
|
+
}): Promise<T>;
|
|
3
7
|
export interface ProxyJsonResponse<T> {
|
|
4
8
|
status?: boolean;
|
|
5
9
|
data?: {
|
|
@@ -7,4 +11,7 @@ export interface ProxyJsonResponse<T> {
|
|
|
7
11
|
};
|
|
8
12
|
message?: string;
|
|
9
13
|
}
|
|
10
|
-
export declare function postProxyJson<T>(url: string, body: Record<string, unknown>, headers: Record<string, string
|
|
14
|
+
export declare function postProxyJson<T>(url: string, body: Record<string, unknown>, headers: Record<string, string>, context?: {
|
|
15
|
+
operation?: string;
|
|
16
|
+
mutationState?: MutationState;
|
|
17
|
+
}): Promise<T>;
|
package/dist/lib/http.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { gzipSync } from 'node:zlib';
|
|
2
|
+
import { DuctapeOperationError, safeEndpoint } from './operation-error.js';
|
|
2
3
|
const GZIP_THRESHOLD = 200 * 1024;
|
|
3
4
|
/** Parse fetch body as JSON; surface HTML/misconfigured api_url clearly. */
|
|
4
|
-
export async function parseJsonResponse(res, url) {
|
|
5
|
+
export async function parseJsonResponse(res, url, context = {}) {
|
|
6
|
+
const endpoint = safeEndpoint(url);
|
|
5
7
|
const text = await res.text();
|
|
6
8
|
const contentType = res.headers.get('content-type') ?? '';
|
|
7
9
|
if (!text.trim()) {
|
|
8
10
|
if (!res.ok)
|
|
9
|
-
throw
|
|
11
|
+
throw responseError(`Empty response (HTTP ${res.status})`, res, url, context, 'EMPTY_RESPONSE');
|
|
10
12
|
return {};
|
|
11
13
|
}
|
|
12
14
|
try {
|
|
@@ -14,8 +16,8 @@ export async function parseJsonResponse(res, url) {
|
|
|
14
16
|
}
|
|
15
17
|
catch {
|
|
16
18
|
if (res.status === 504) {
|
|
17
|
-
throw
|
|
18
|
-
'verifiable result; retry only after checking whether the asset was created.');
|
|
19
|
+
throw responseError(`Ductape proxy gateway timeout (HTTP 504) for ${endpoint}. The operation did not return a ` +
|
|
20
|
+
'verifiable result; retry only after checking whether the asset was created.', res, url, context, 'GATEWAY_TIMEOUT');
|
|
19
21
|
}
|
|
20
22
|
const htmlHint = text.trimStart().startsWith('<')
|
|
21
23
|
? [
|
|
@@ -27,10 +29,22 @@ export async function parseJsonResponse(res, url) {
|
|
|
27
29
|
'Then: ductape login --profile local',
|
|
28
30
|
].join('\n')
|
|
29
31
|
: '';
|
|
30
|
-
throw
|
|
32
|
+
throw responseError(`Invalid JSON from ${endpoint} (HTTP ${res.status}, Content-Type: ${contentType}): ${text.slice(0, 200)}${htmlHint}`, res, url, context, 'INVALID_JSON_RESPONSE');
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
function responseError(message, res, url, context, code) {
|
|
36
|
+
return new DuctapeOperationError(message, {
|
|
37
|
+
code,
|
|
38
|
+
operation: context.operation ?? 'http.request',
|
|
39
|
+
endpoint: safeEndpoint(url),
|
|
40
|
+
httpStatus: res.status,
|
|
41
|
+
requestId: res.headers.get('x-request-id') ?? res.headers.get('x-correlation-id') ?? undefined,
|
|
42
|
+
retryable: res.status === 408 || res.status === 429 || res.status >= 500,
|
|
43
|
+
mutationState: context.mutationState ?? 'not_attempted',
|
|
44
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function postProxyJson(url, body, headers, context = {}) {
|
|
34
48
|
const json = JSON.stringify(body);
|
|
35
49
|
const reqHeaders = { ...headers, 'Content-Type': 'application/json' };
|
|
36
50
|
let bodyInit = json;
|
|
@@ -44,12 +58,12 @@ export async function postProxyJson(url, body, headers) {
|
|
|
44
58
|
headers: reqHeaders,
|
|
45
59
|
body: bodyInit,
|
|
46
60
|
});
|
|
47
|
-
const parsed = await parseJsonResponse(res, url);
|
|
61
|
+
const parsed = await parseJsonResponse(res, url, context);
|
|
48
62
|
if (!res.ok) {
|
|
49
|
-
throw
|
|
63
|
+
throw responseError(parsed.message ?? `Request failed: ${res.status}`, res, url, context, 'HTTP_ERROR');
|
|
50
64
|
}
|
|
51
65
|
if (typeof parsed.status === 'boolean' && !parsed.status) {
|
|
52
|
-
throw
|
|
66
|
+
throw responseError(parsed.message ?? 'Proxy operation failed', res, url, context, 'OPERATION_FAILED');
|
|
53
67
|
}
|
|
54
68
|
return parsed.data?.data;
|
|
55
69
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type MutationState = 'not_attempted' | 'unknown' | 'confirmed_failed';
|
|
2
|
+
export interface OperationErrorDetails {
|
|
3
|
+
code: string;
|
|
4
|
+
operation: string;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
httpStatus?: number;
|
|
7
|
+
requestId?: string;
|
|
8
|
+
retryable: boolean;
|
|
9
|
+
mutationState: MutationState;
|
|
10
|
+
contentType?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class DuctapeOperationError extends Error {
|
|
13
|
+
readonly details: OperationErrorDetails;
|
|
14
|
+
constructor(message: string, details: OperationErrorDetails, options?: ErrorOptions);
|
|
15
|
+
toJSON(): Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function safeEndpoint(value: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class DuctapeOperationError extends Error {
|
|
2
|
+
details;
|
|
3
|
+
constructor(message, details, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = 'DuctapeOperationError';
|
|
6
|
+
this.details = details;
|
|
7
|
+
}
|
|
8
|
+
toJSON() {
|
|
9
|
+
return { error: this.message, ...this.details };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function safeEndpoint(value) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return `${url.origin}${url.pathname}`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return value.split('?')[0];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -7,7 +7,14 @@ export declare function getProduct(ctx: WorkspaceContext, opts: {
|
|
|
7
7
|
export declare function createProduct(ctx: WorkspaceContext, body: Record<string, unknown>): Promise<unknown>;
|
|
8
8
|
export declare function updateProduct(ctx: WorkspaceContext, tag: string, body: Record<string, unknown>): Promise<unknown>;
|
|
9
9
|
export declare function deleteProduct(ctx: WorkspaceContext, productId: string): Promise<unknown>;
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function summarizeConnectedApp(value: unknown): unknown;
|
|
11
|
+
export declare function listProductApps(ctx: WorkspaceContext, productIdOrTag: string, full?: boolean): Promise<unknown[]>;
|
|
12
|
+
export declare function listProductAppActions(ctx: WorkspaceContext, product: string, appTag: string): Promise<unknown[]>;
|
|
13
|
+
export declare function getProductAppAction(ctx: WorkspaceContext, product: string, appTag: string, actionTag: string): Promise<unknown>;
|
|
14
|
+
export declare function connectMarketplaceApp(ctx: WorkspaceContext, product: string, appTag: string, envs: Array<{
|
|
15
|
+
app_env_slug: string;
|
|
16
|
+
product_env_slug: string;
|
|
17
|
+
}>): Promise<unknown>;
|
|
11
18
|
export declare function listApps(ctx: WorkspaceContext, status?: string): Promise<unknown[]>;
|
|
12
19
|
export declare function getApp(ctx: WorkspaceContext, opts: {
|
|
13
20
|
id?: string;
|
|
@@ -23,7 +30,7 @@ export interface MarketplaceSearchOptions {
|
|
|
23
30
|
limit?: number;
|
|
24
31
|
}
|
|
25
32
|
export declare function listMarketplaceCategories(ctx: WorkspaceContext): Promise<unknown[]>;
|
|
26
|
-
export declare function getMarketplaceApp(ctx: WorkspaceContext, tag: string): Promise<unknown>;
|
|
33
|
+
export declare function getMarketplaceApp(ctx: WorkspaceContext, tag: string, full?: boolean): Promise<unknown>;
|
|
27
34
|
export declare function searchMarketplaceApps(ctx: WorkspaceContext, opts: MarketplaceSearchOptions): Promise<unknown[]>;
|
|
28
35
|
export interface TierQuery {
|
|
29
36
|
provider?: string;
|
package/dist/lib/platform-api.js
CHANGED
|
@@ -56,8 +56,20 @@ export async function getProduct(ctx, opts) {
|
|
|
56
56
|
// The integrations REST fetch-by-tag route rejects current CLI login tokens with HTTP 401.
|
|
57
57
|
// Product inventory is an administrative read, so route it through the authenticated CLI
|
|
58
58
|
// SDK proxy (x-access-token), not the publishable-key runtime MCP proxy.
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
try {
|
|
60
|
+
const result = await proxy(ctx).execute('product', 'fetch', [opts.tag]);
|
|
61
|
+
return unwrapOne(result);
|
|
62
|
+
}
|
|
63
|
+
catch (proxyError) {
|
|
64
|
+
// Some deployed proxy versions fail product.fetch while the authenticated workspace
|
|
65
|
+
// inventory remains healthy. Resolve the exact tag from that inventory; never guess or
|
|
66
|
+
// turn a transport failure into an empty result.
|
|
67
|
+
const products = await listProducts(ctx, 'all');
|
|
68
|
+
const match = products.find((product) => product && typeof product === 'object' && product.tag === opts.tag);
|
|
69
|
+
if (match)
|
|
70
|
+
return match;
|
|
71
|
+
throw proxyError;
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
throw new Error('Provide --id <product_id> or --tag <product_tag>');
|
|
63
75
|
}
|
|
@@ -77,9 +89,185 @@ export async function updateProduct(ctx, tag, body) {
|
|
|
77
89
|
export async function deleteProduct(ctx, productId) {
|
|
78
90
|
return client(ctx).delete(`/integrations/v1/${productId}?${new URLSearchParams(workspaceAuthQuery(ctx)).toString()}`);
|
|
79
91
|
}
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
export function summarizeConnectedApp(value) {
|
|
93
|
+
if (!value || typeof value !== 'object')
|
|
94
|
+
return value;
|
|
95
|
+
const app = value;
|
|
96
|
+
const versions = Array.isArray(app.versions) ? app.versions : [];
|
|
97
|
+
const linkedEnvs = Array.isArray(app.envs) ? app.envs : [];
|
|
98
|
+
return {
|
|
99
|
+
_id: app._id ?? app.id,
|
|
100
|
+
tag: app.tag ?? app.app_tag,
|
|
101
|
+
access_tag: app.access_tag,
|
|
102
|
+
name: app.app_name ?? app.name,
|
|
103
|
+
description: app.description,
|
|
104
|
+
status: app.status,
|
|
105
|
+
active: app.active,
|
|
106
|
+
environments: linkedEnvs.map((item) => {
|
|
107
|
+
const env = item && typeof item === 'object' ? item : {};
|
|
108
|
+
return {
|
|
109
|
+
app_env_slug: env.app_env_slug,
|
|
110
|
+
product_env_slug: env.product_env_slug,
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
versions: versions.map((item) => {
|
|
114
|
+
const version = item && typeof item === 'object' ? item : {};
|
|
115
|
+
return {
|
|
116
|
+
tag: version.tag,
|
|
117
|
+
latest: version.latest,
|
|
118
|
+
active: version.active,
|
|
119
|
+
environments: Array.isArray(version.envs) ? version.envs.length : 0,
|
|
120
|
+
actions: Array.isArray(version.actions) ? version.actions.length : 0,
|
|
121
|
+
webhooks: Array.isArray(version.webhooks) ? version.webhooks.length : 0,
|
|
122
|
+
};
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function listProductApps(ctx, productIdOrTag, full = false) {
|
|
127
|
+
const isObjectId = /^[a-f\d]{24}$/i.test(productIdOrTag);
|
|
128
|
+
// A tag is already sufficient for the compact SDK method; do not fetch the
|
|
129
|
+
// product catalogue merely to rediscover that same tag.
|
|
130
|
+
if (!full && !isObjectId) {
|
|
131
|
+
try {
|
|
132
|
+
const result = await proxy(ctx).execute('product', 'apps.list', [productIdOrTag]);
|
|
133
|
+
return unwrapList(result).map(summarizeConnectedApp);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Older deployments may not expose product.apps.list reliably. The product
|
|
137
|
+
// record itself still contains compact link records and is the safe fallback.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const product = await getProduct(ctx, isObjectId ? { id: productIdOrTag } : { tag: productIdOrTag });
|
|
141
|
+
const row = product && typeof product === 'object' ? product : {};
|
|
142
|
+
const productId = String(row._id ?? row.id ?? (isObjectId ? productIdOrTag : ''));
|
|
143
|
+
const productTag = String(row.tag ?? (!isObjectId ? productIdOrTag : ''));
|
|
144
|
+
// The SDK proxy returns only the product's link records and avoids expanding every
|
|
145
|
+
// action contract. Prefer it for normal discovery; retain the expanded REST route
|
|
146
|
+
// solely for --full and compatibility with older deployed proxy versions.
|
|
147
|
+
if (!full && Array.isArray(row.apps)) {
|
|
148
|
+
return row.apps.map(summarizeConnectedApp);
|
|
149
|
+
}
|
|
150
|
+
if (!full && productTag) {
|
|
151
|
+
try {
|
|
152
|
+
const result = await proxy(ctx).execute('product', 'apps.list', [productTag]);
|
|
153
|
+
return unwrapList(result).map(summarizeConnectedApp);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Fall through to the linked-app REST endpoint.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (productId) {
|
|
160
|
+
try {
|
|
161
|
+
const result = await client(ctx).getPath(`/integrations/v1/fetch-product-apps/${productId}`, workspaceAuthQuery(ctx));
|
|
162
|
+
const apps = unwrapList(result);
|
|
163
|
+
return full ? apps : apps.map(summarizeConnectedApp);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (!productTag)
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!productTag)
|
|
171
|
+
throw new Error(`Could not resolve product tag for "${productIdOrTag}"`);
|
|
172
|
+
const result = await proxy(ctx).execute('product', 'apps.list', [productTag]);
|
|
173
|
+
const apps = unwrapList(result);
|
|
174
|
+
return full ? apps : apps.map(summarizeConnectedApp);
|
|
175
|
+
}
|
|
176
|
+
function actionSummary(value) {
|
|
177
|
+
if (!value || typeof value !== 'object')
|
|
178
|
+
return value;
|
|
179
|
+
const action = value;
|
|
180
|
+
return {
|
|
181
|
+
tag: action.tag,
|
|
182
|
+
name: action.name,
|
|
183
|
+
description: action.description,
|
|
184
|
+
method: action.method,
|
|
185
|
+
resource: action.resource,
|
|
186
|
+
request_type: action.request_type,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function actionsFromLinkedApp(app) {
|
|
190
|
+
const versions = Array.isArray(app.versions) ? app.versions : [];
|
|
191
|
+
const version = versions.find((item) => item.latest === true) ?? versions[0];
|
|
192
|
+
return Array.isArray(version?.actions) ? version.actions : [];
|
|
193
|
+
}
|
|
194
|
+
async function requireLinkedApp(ctx, product, appTag) {
|
|
195
|
+
const apps = await listProductApps(ctx, product);
|
|
196
|
+
const app = apps.find((item) => {
|
|
197
|
+
if (!item || typeof item !== 'object')
|
|
198
|
+
return false;
|
|
199
|
+
const row = item;
|
|
200
|
+
return row.tag === appTag || row.app_tag === appTag;
|
|
201
|
+
});
|
|
202
|
+
if (!app || typeof app !== 'object')
|
|
203
|
+
throw new Error(`App "${appTag}" is not linked to product "${product}"`);
|
|
204
|
+
return app;
|
|
205
|
+
}
|
|
206
|
+
export async function listProductAppActions(ctx, product, appTag) {
|
|
207
|
+
await requireLinkedApp(ctx, product, appTag);
|
|
208
|
+
try {
|
|
209
|
+
const result = await proxy(ctx).execute('app', 'actions.list', [appTag]);
|
|
210
|
+
return unwrapList(result).map(actionSummary);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
const app = await getApp(ctx, { tag: appTag });
|
|
214
|
+
if (!app || typeof app !== 'object')
|
|
215
|
+
throw new Error(`App "${appTag}" could not be fetched`);
|
|
216
|
+
return actionsFromLinkedApp(app).map(actionSummary);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export async function getProductAppAction(ctx, product, appTag, actionTag) {
|
|
220
|
+
await requireLinkedApp(ctx, product, appTag);
|
|
221
|
+
try {
|
|
222
|
+
const result = unwrapOne(await proxy(ctx).execute('app', 'actions.fetch', [appTag, actionTag]));
|
|
223
|
+
if (result)
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Compatibility fallback for deployed SDKs predating app.actions.
|
|
228
|
+
}
|
|
229
|
+
const app = await getApp(ctx, { tag: appTag });
|
|
230
|
+
if (!app || typeof app !== 'object')
|
|
231
|
+
throw new Error(`App "${appTag}" could not be fetched`);
|
|
232
|
+
const action = actionsFromLinkedApp(app).find((item) => item && typeof item === 'object' && item.tag === actionTag);
|
|
233
|
+
if (!action)
|
|
234
|
+
throw new Error(`Action "${actionTag}" was not found on linked app "${appTag}"`);
|
|
235
|
+
return action;
|
|
236
|
+
}
|
|
237
|
+
export async function connectMarketplaceApp(ctx, product, appTag, envs) {
|
|
238
|
+
if (envs.length === 0)
|
|
239
|
+
throw new Error('At least one app-to-product environment mapping is required');
|
|
240
|
+
const marketplaceApp = await getMarketplaceApp(ctx, appTag, true);
|
|
241
|
+
if (!marketplaceApp || typeof marketplaceApp !== 'object')
|
|
242
|
+
throw new Error(`Marketplace app "${appTag}" was not found`);
|
|
243
|
+
const app = marketplaceApp;
|
|
244
|
+
if (app.status !== 'public')
|
|
245
|
+
throw new Error(`App "${appTag}" is not public in the marketplace`);
|
|
246
|
+
const versions = Array.isArray(app.versions) ? app.versions : [];
|
|
247
|
+
const latest = versions.find((version) => version.latest === true) ?? versions[0];
|
|
248
|
+
const availableAppEnvs = new Set((Array.isArray(latest?.envs) ? latest.envs : []).map((env) => String(env && typeof env === 'object' ? env.slug ?? '' : env)).filter(Boolean));
|
|
249
|
+
const productEnvs = unwrapList(await proxy(ctx).execute('product', 'environments.list', [product]));
|
|
250
|
+
const availableProductEnvs = new Set(productEnvs.map((env) => String(env.slug ?? '')).filter(Boolean));
|
|
251
|
+
for (const mapping of envs) {
|
|
252
|
+
if (!availableAppEnvs.has(mapping.app_env_slug)) {
|
|
253
|
+
throw new Error(`Marketplace app "${appTag}" has no environment "${mapping.app_env_slug}"`);
|
|
254
|
+
}
|
|
255
|
+
if (!availableProductEnvs.has(mapping.product_env_slug)) {
|
|
256
|
+
throw new Error(`Product "${product}" has no environment "${mapping.product_env_slug}"`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const accessResult = unwrapOne(await proxy(ctx).execute('product', 'apps.connect', [product, appTag]));
|
|
260
|
+
const accessTag = String(accessResult?.access_tag ?? '');
|
|
261
|
+
if (!accessTag)
|
|
262
|
+
throw new Error(`Could not create or resolve access for marketplace app "${appTag}"`);
|
|
263
|
+
await proxy(ctx).execute('product', 'apps.add', [product, { access_tag: accessTag, envs }]);
|
|
264
|
+
return {
|
|
265
|
+
connected: true,
|
|
266
|
+
product,
|
|
267
|
+
app_tag: appTag,
|
|
268
|
+
access_tag: accessTag,
|
|
269
|
+
environments: envs,
|
|
270
|
+
};
|
|
83
271
|
}
|
|
84
272
|
// —— Apps (apps REST) ——
|
|
85
273
|
export async function listApps(ctx, status = 'all') {
|
|
@@ -99,7 +287,9 @@ export async function getApp(ctx, opts) {
|
|
|
99
287
|
return unwrapOne(result);
|
|
100
288
|
}
|
|
101
289
|
if (opts.tag) {
|
|
102
|
-
|
|
290
|
+
// The REST tag route is not consistent for namespaced/private apps. The authenticated SDK
|
|
291
|
+
// proxy uses AppBuilder's canonical tag lookup and works for tags such as ductape:paystack.
|
|
292
|
+
const result = await proxy(ctx).execute('app', 'fetch', [opts.tag]);
|
|
103
293
|
return unwrapOne(result);
|
|
104
294
|
}
|
|
105
295
|
throw new Error('Provide --id <app_id> or --tag <app_tag>');
|
|
@@ -164,13 +354,35 @@ function searchableMarketplaceText(app) {
|
|
|
164
354
|
}),
|
|
165
355
|
].filter((item) => typeof item === 'string').join(' ').toLowerCase();
|
|
166
356
|
}
|
|
357
|
+
function summarizeMarketplaceApp(value) {
|
|
358
|
+
if (!value || typeof value !== 'object')
|
|
359
|
+
return value;
|
|
360
|
+
const app = value;
|
|
361
|
+
const versions = Array.isArray(app.versions) ? app.versions : [];
|
|
362
|
+
const current = app.currentVersion && typeof app.currentVersion === 'object'
|
|
363
|
+
? app.currentVersion
|
|
364
|
+
: versions.find((version) => version.latest === true) ?? versions[0] ?? {};
|
|
365
|
+
const actions = Array.isArray(current.actions) ? current.actions : [];
|
|
366
|
+
return {
|
|
367
|
+
tag: app.tag,
|
|
368
|
+
name: app.app_name ?? app.name,
|
|
369
|
+
description: app.description,
|
|
370
|
+
categories: app.domains ?? [],
|
|
371
|
+
version: current.tag,
|
|
372
|
+
environments: Array.isArray(current.envs)
|
|
373
|
+
? current.envs.map((env) => env && typeof env === 'object' ? env.slug : env)
|
|
374
|
+
: [],
|
|
375
|
+
actions: actions.map(actionSummary),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
167
378
|
export async function listMarketplaceCategories(ctx) {
|
|
168
379
|
const result = await client(ctx).getPath('/apps/v1/domains');
|
|
169
380
|
return unwrapList(result);
|
|
170
381
|
}
|
|
171
|
-
export async function getMarketplaceApp(ctx, tag) {
|
|
382
|
+
export async function getMarketplaceApp(ctx, tag, full = false) {
|
|
172
383
|
const result = await client(ctx).getPath('/apps/v1/fetch/tag', { tag });
|
|
173
|
-
|
|
384
|
+
const app = unwrapOne(result);
|
|
385
|
+
return full ? app : summarizeMarketplaceApp(app);
|
|
174
386
|
}
|
|
175
387
|
export async function searchMarketplaceApps(ctx, opts) {
|
|
176
388
|
const result = await client(ctx).getPath('/apps/v1/domains/all');
|
|
@@ -188,7 +400,7 @@ export async function searchMarketplaceApps(ctx, opts) {
|
|
|
188
400
|
const domains = app.domains;
|
|
189
401
|
return Array.isArray(domains) && domains.some((domain) => typeof domain === 'string' && domain.toLowerCase().includes(category));
|
|
190
402
|
});
|
|
191
|
-
return filtered.slice(0, Math.max(1, Math.min(opts.limit ?? 20, 100)));
|
|
403
|
+
return filtered.slice(0, Math.max(1, Math.min(opts.limit ?? 20, 100))).map(summarizeMarketplaceApp);
|
|
192
404
|
}
|
|
193
405
|
export async function listCloudTiers(ctx, q) {
|
|
194
406
|
const params = workspaceAuthQuery(ctx);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SDKModule = 'product' | 'app' | 'databases' | 'graph' | 'webhooks' | 'notifications' | 'messageBrokers' | 'storage' | 'vector' | 'caches' | 'sessions' | 'quotas' | 'actions' | 'features' | 'jobs' | 'logs' | 'resilience' | 'health' | 'fallback' | 'secrets' | 'cloud';
|
|
1
|
+
export type SDKModule = 'product' | 'app' | 'databases' | 'graph' | 'webhooks' | 'notifications' | 'messageBrokers' | 'storage' | 'vector' | 'caches' | 'sessions' | 'quotas' | 'actions' | 'features' | 'jobs' | 'logs' | 'resilience' | 'health' | 'fallback' | 'secrets' | 'models' | 'cloud';
|
|
2
2
|
export interface SdkProxyContext {
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
workspaceId: string;
|
|
@@ -14,7 +14,12 @@ export class SdkProxyClient {
|
|
|
14
14
|
workspace_id: this.ctx.workspaceId,
|
|
15
15
|
user_id: this.ctx.userId,
|
|
16
16
|
public_key: this.ctx.publicKey,
|
|
17
|
-
}, { 'x-access-token': this.ctx.token }
|
|
17
|
+
}, { 'x-access-token': this.ctx.token }, {
|
|
18
|
+
operation: `${module}.${method}`,
|
|
19
|
+
mutationState: /(^|\.)(create|update|delete|connect|import|provision|complete)$/.test(method)
|
|
20
|
+
? 'unknown'
|
|
21
|
+
: 'not_attempted',
|
|
22
|
+
});
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
export function createSdkProxyClient(ctx) {
|
package/dist/lib/resources.js
CHANGED
|
@@ -32,6 +32,8 @@ export const RESOURCE_MODULES = {
|
|
|
32
32
|
health: 'health',
|
|
33
33
|
healthcheck: 'health',
|
|
34
34
|
fallback: 'fallback',
|
|
35
|
+
model: 'models',
|
|
36
|
+
models: 'models',
|
|
35
37
|
product: 'product',
|
|
36
38
|
app: 'app',
|
|
37
39
|
};
|
|
@@ -45,6 +47,8 @@ export function resolveResourceType(name) {
|
|
|
45
47
|
export function proxyMethodForVerb(verb) {
|
|
46
48
|
if (verb === 'get')
|
|
47
49
|
return 'fetch';
|
|
50
|
+
if (verb === 'list')
|
|
51
|
+
return 'list';
|
|
48
52
|
return verb;
|
|
49
53
|
}
|
|
50
54
|
/** Build params array matching Workbench sdkProxy.ts conventions */
|
|
@@ -65,6 +69,22 @@ export function buildCrudParams(module, method, productTag, args) {
|
|
|
65
69
|
return [payload];
|
|
66
70
|
}
|
|
67
71
|
}
|
|
72
|
+
if (module === 'models') {
|
|
73
|
+
if (method === 'list')
|
|
74
|
+
return [{ product: productTag }];
|
|
75
|
+
if (method === 'create')
|
|
76
|
+
return [{ product: productTag, ...(body ?? {}) }];
|
|
77
|
+
if (method === 'fetch' || method === 'delete') {
|
|
78
|
+
if (!tag)
|
|
79
|
+
throw new Error('tag is required');
|
|
80
|
+
return [{ product: productTag, tag }];
|
|
81
|
+
}
|
|
82
|
+
if (method === 'update') {
|
|
83
|
+
if (!tag)
|
|
84
|
+
throw new Error('tag is required');
|
|
85
|
+
return [{ product: productTag, tag, ...(body ?? {}) }];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
68
88
|
if ((module === 'storage' || module === 'databases') && method === 'create') {
|
|
69
89
|
// databases.create and storage.create expect a single config object with product embedded,
|
|
70
90
|
// not the default (productTag, data) two-argument form.
|
|
@@ -145,6 +165,7 @@ export function listResourceTypes() {
|
|
|
145
165
|
'quotas',
|
|
146
166
|
'health',
|
|
147
167
|
'fallback',
|
|
168
|
+
'models',
|
|
148
169
|
'product',
|
|
149
170
|
'app',
|
|
150
171
|
];
|
package/package.json
CHANGED