@ductape/cli 0.3.12 → 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/resources.d.ts +3 -0
- package/dist/commands/resources.js +90 -14
- package/dist/index.js +7 -1
- 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/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
|
@@ -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
|
@@ -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
|
};
|
|
@@ -673,6 +676,9 @@ eventTopics
|
|
|
673
676
|
.option('-t, --tag <tag>', 'Broker tag (list) or topic tag in broker:topic form (get, update, delete)')
|
|
674
677
|
.option('-f, --file <path>', 'JSON body (create, update)')
|
|
675
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')
|
|
676
682
|
.option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
|
|
677
683
|
.option('--no-interactive', 'Require -f JSON for create/update')
|
|
678
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
|
+
}
|
|
@@ -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