@ductape/cli 0.3.14 → 0.3.16
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/cloud.d.ts +5 -0
- package/dist/commands/cloud.js +74 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +123 -0
- package/dist/commands/resources.js +43 -5
- package/dist/index.js +14 -1
- package/dist/lib/resources.d.ts +1 -0
- package/dist/lib/resources.js +27 -2
- package/dist/lib/templates.js +7 -0
- package/package.json +1 -1
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
type CloudTarget = 'connections' | 'resources' | 'tiers';
|
|
2
|
+
export declare function runCloudPreflight(opts: {
|
|
3
|
+
product?: string;
|
|
4
|
+
file?: string;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
2
7
|
export declare function runCloud(target: CloudTarget, verb: string, opts: {
|
|
3
8
|
id?: string;
|
|
4
9
|
file?: string;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -3,6 +3,80 @@ import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
|
|
|
3
3
|
import { printJson } from '../lib/output.js';
|
|
4
4
|
import { listCloudTiers } from '../lib/platform-api.js';
|
|
5
5
|
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
6
|
+
const CLOUD_SERVICE_CAPABILITIES = [
|
|
7
|
+
{ service: 's3', provider: 'aws', types: ['storage'], actions: ['list', 'import', 'provision'] },
|
|
8
|
+
{ service: 'gcs', provider: 'gcp', types: ['storage'], actions: ['list', 'import', 'provision'] },
|
|
9
|
+
{ service: 'azure_blob', provider: 'azure', types: ['storage'], actions: ['list', 'import', 'provision'] },
|
|
10
|
+
{ service: 'rds', provider: 'aws', types: ['database'], actions: ['list', 'import', 'provision'] },
|
|
11
|
+
{ service: 'cloud_sql', provider: 'gcp', types: ['database'], actions: ['list', 'import', 'provision'] },
|
|
12
|
+
{ service: 'azure_database', provider: 'azure', types: ['database'], actions: ['list', 'import', 'provision'] },
|
|
13
|
+
{ service: 'mongodb_atlas', provider: 'mongodb_atlas', types: ['database'], actions: ['list', 'import', 'provision'] },
|
|
14
|
+
{ service: 'neo4j_aura', provider: 'neo4j_aura', types: ['graph'], actions: ['list', 'import', 'provision'] },
|
|
15
|
+
{ service: 'vertex_ai_vector_search', provider: 'gcp', types: ['vector'], actions: ['list', 'import', 'provision'] },
|
|
16
|
+
{ service: 'opensearch', provider: 'aws', types: ['vector'], actions: ['list', 'import', 'provision'] },
|
|
17
|
+
{ service: 'pubsub', provider: 'gcp', types: ['messageBroker'], actions: ['list', 'import', 'provision'] },
|
|
18
|
+
{ service: 'sns_sqs', provider: 'aws', types: ['messageBroker'], actions: ['list', 'import', 'provision'] },
|
|
19
|
+
];
|
|
20
|
+
export async function runCloudPreflight(opts) {
|
|
21
|
+
const session = requireSession();
|
|
22
|
+
const proxy = getSdkProxy(session);
|
|
23
|
+
const product = opts.product ?? session.project.product_tag;
|
|
24
|
+
const input = readJsonBody(opts.file);
|
|
25
|
+
const [connectionsResult, environmentsResult] = await Promise.allSettled([
|
|
26
|
+
proxy.execute('cloud', 'connections.list', []),
|
|
27
|
+
proxy.execute('product', 'environments.list', [product]),
|
|
28
|
+
]);
|
|
29
|
+
const ctx = requireWorkspaceContext();
|
|
30
|
+
const tiersResult = await Promise.allSettled([listCloudTiers(ctx, {})]);
|
|
31
|
+
const discoveries = Array.isArray(input?.discoveries) ? input.discoveries : [];
|
|
32
|
+
const discoveryResults = await Promise.all(discoveries.map(async (request) => {
|
|
33
|
+
try {
|
|
34
|
+
return { request, ok: true, resources: await proxy.execute('cloud', 'resources.list', [request]) };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { request, ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
const environments = environmentsResult.status === 'fulfilled' ? environmentsResult.value : null;
|
|
41
|
+
const envList = Array.isArray(environments)
|
|
42
|
+
? environments
|
|
43
|
+
: environments && typeof environments === 'object' && Array.isArray(environments.data)
|
|
44
|
+
? environments.data
|
|
45
|
+
: [];
|
|
46
|
+
const inactive = envList.filter((env) => env && typeof env === 'object' && env.active === false);
|
|
47
|
+
const unresolved = [];
|
|
48
|
+
if (connectionsResult.status === 'rejected')
|
|
49
|
+
unresolved.push(`Cloud connections could not be listed: ${String(connectionsResult.reason)}`);
|
|
50
|
+
if (environmentsResult.status === 'rejected')
|
|
51
|
+
unresolved.push(`Product environments could not be listed: ${String(environmentsResult.reason)}`);
|
|
52
|
+
if (tiersResult[0].status === 'rejected')
|
|
53
|
+
unresolved.push(`Cloud tiers could not be listed: ${String(tiersResult[0].reason)}`);
|
|
54
|
+
if (discoveries.length === 0)
|
|
55
|
+
unresolved.push('No instance discovery requests supplied; pass --file with {"discoveries":[{cloud,service,type,...}]}.');
|
|
56
|
+
if (inactive.length > 0)
|
|
57
|
+
unresolved.push('Inactive environments still require an explicit configuration decision; preflight does not assume shared or provisioned infrastructure.');
|
|
58
|
+
for (const result of discoveryResults)
|
|
59
|
+
if (!result.ok)
|
|
60
|
+
unresolved.push(`Instance discovery failed for ${String(result.request.service ?? 'unknown service')}: ${result.error}`);
|
|
61
|
+
printJson({
|
|
62
|
+
product,
|
|
63
|
+
read_only: true,
|
|
64
|
+
supported_services: CLOUD_SERVICE_CAPABILITIES,
|
|
65
|
+
required_discovery_fields: ['cloud', 'service', 'type'],
|
|
66
|
+
connections: connectionsResult.status === 'fulfilled' ? connectionsResult.value : null,
|
|
67
|
+
environments: envList,
|
|
68
|
+
inactive_environment_policy: 'explicit_configuration_required_no_automatic_provisioning',
|
|
69
|
+
tiers: tiersResult[0].status === 'fulfilled' ? tiersResult[0].value : null,
|
|
70
|
+
instances: discoveryResults,
|
|
71
|
+
mutation_actions: {
|
|
72
|
+
import_existing: ['import-persist', 'import-persist-all'],
|
|
73
|
+
provision_new: ['provision-persist', 'provision-persist-all'],
|
|
74
|
+
destructive_deprovision: false,
|
|
75
|
+
},
|
|
76
|
+
unresolved_decisions: unresolved,
|
|
77
|
+
ready: unresolved.length === 0,
|
|
78
|
+
}, Boolean(opts.json));
|
|
79
|
+
}
|
|
6
80
|
export async function runCloud(target, verb, opts, extraArgs) {
|
|
7
81
|
if (target === 'tiers') {
|
|
8
82
|
const ctx = requireWorkspaceContext();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { findProjectConfig, getActiveProfileName, getApiUrl, loadCredentials, loadGlobalConfig, } from '../lib/config.js';
|
|
5
|
+
function findUp(name, start = process.cwd()) {
|
|
6
|
+
let dir = path.resolve(start);
|
|
7
|
+
const root = path.parse(dir).root;
|
|
8
|
+
while (true) {
|
|
9
|
+
const candidate = path.join(dir, name);
|
|
10
|
+
if (fs.existsSync(candidate))
|
|
11
|
+
return candidate;
|
|
12
|
+
if (dir === root)
|
|
13
|
+
return null;
|
|
14
|
+
dir = path.dirname(dir);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function readJson(file) {
|
|
18
|
+
if (!file)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function dependencyVersion(pkg, name) {
|
|
28
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
29
|
+
const values = pkg?.[key];
|
|
30
|
+
if (values && typeof values === 'object' && typeof values[name] === 'string') {
|
|
31
|
+
return values[name];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function configuredMcpVersion(config) {
|
|
37
|
+
const servers = config?.mcpServers;
|
|
38
|
+
if (!servers || typeof servers !== 'object')
|
|
39
|
+
return null;
|
|
40
|
+
for (const value of Object.values(servers)) {
|
|
41
|
+
if (!value || typeof value !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
const args = value.args;
|
|
44
|
+
if (!Array.isArray(args))
|
|
45
|
+
continue;
|
|
46
|
+
const spec = args.find((arg) => typeof arg === 'string' && arg.startsWith('@ductape/mcp'));
|
|
47
|
+
if (typeof spec === 'string')
|
|
48
|
+
return spec.replace(/^@ductape\/mcp@?/, '') || 'unversioned';
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export async function runDoctor(opts) {
|
|
53
|
+
const project = findProjectConfig();
|
|
54
|
+
const profile = project?.config
|
|
55
|
+
? getActiveProfileName(project.config)
|
|
56
|
+
: loadGlobalConfig().default_profile;
|
|
57
|
+
const apiUrl = getApiUrl(profile);
|
|
58
|
+
const credentials = loadCredentials();
|
|
59
|
+
const packageFile = findUp('package.json');
|
|
60
|
+
const pkg = readJson(packageFile);
|
|
61
|
+
const mcpFile = findUp('.mcp.json');
|
|
62
|
+
const warnings = [];
|
|
63
|
+
let schema;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${apiUrl}/proxy/v1/schema`, {
|
|
66
|
+
headers: { Accept: 'application/json' },
|
|
67
|
+
signal: AbortSignal.timeout(15_000),
|
|
68
|
+
});
|
|
69
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
70
|
+
const body = await response.text();
|
|
71
|
+
const isJson = contentType.toLowerCase().includes('json');
|
|
72
|
+
schema = {
|
|
73
|
+
reachable: response.ok && isJson,
|
|
74
|
+
status: response.status,
|
|
75
|
+
content_type: contentType,
|
|
76
|
+
revision: response.ok && isJson
|
|
77
|
+
? `sha256:${createHash('sha256').update(body).digest('hex')}`
|
|
78
|
+
: null,
|
|
79
|
+
api_version: response.headers.get('x-api-version'),
|
|
80
|
+
request_id: response.headers.get('x-request-id') ?? response.headers.get('x-correlation-id'),
|
|
81
|
+
};
|
|
82
|
+
if (!response.ok || !isJson)
|
|
83
|
+
warnings.push(`Schema endpoint returned HTTP ${response.status} (${contentType || 'unknown content type'}).`);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
schema = { reachable: false, error: error instanceof Error ? error.message : String(error) };
|
|
87
|
+
warnings.push('The configured API/schema endpoint is not reachable.');
|
|
88
|
+
}
|
|
89
|
+
const versions = {
|
|
90
|
+
cli: opts.cliVersion,
|
|
91
|
+
sdk: dependencyVersion(pkg, '@ductape/sdk'),
|
|
92
|
+
nestjs: dependencyVersion(pkg, '@ductape/nestjs'),
|
|
93
|
+
mcp: configuredMcpVersion(readJson(mcpFile)),
|
|
94
|
+
};
|
|
95
|
+
if (!credentials)
|
|
96
|
+
warnings.push('Not logged in; administrative CLI operations will fail.');
|
|
97
|
+
if (!project)
|
|
98
|
+
warnings.push('No linked Ductape project was found from the current directory.');
|
|
99
|
+
if (!versions.sdk)
|
|
100
|
+
warnings.push('@ductape/sdk is not declared in the nearest package.json.');
|
|
101
|
+
if (!versions.mcp)
|
|
102
|
+
warnings.push('No @ductape/mcp package was found in the nearest .mcp.json.');
|
|
103
|
+
const result = {
|
|
104
|
+
ok: warnings.length === 0,
|
|
105
|
+
versions,
|
|
106
|
+
api: { profile, url: apiUrl, schema },
|
|
107
|
+
authentication: {
|
|
108
|
+
logged_in: Boolean(credentials),
|
|
109
|
+
user_id: credentials?.user_id ?? null,
|
|
110
|
+
email: credentials?.email ?? null,
|
|
111
|
+
active_workspace_id: loadGlobalConfig().active_workspace_id ?? null,
|
|
112
|
+
},
|
|
113
|
+
linkage: project ? { root: project.dir, ...project.config } : null,
|
|
114
|
+
sources: { package_json: packageFile, mcp_json: mcpFile },
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
if (opts.json)
|
|
118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
119
|
+
else {
|
|
120
|
+
console.log(`Ductape doctor: ${result.ok ? 'ready' : `${warnings.length} warning(s)`}`);
|
|
121
|
+
console.log(JSON.stringify(result, null, 2));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCrudParams, listResourceTypes, proxyMethodForVerb, resolveResourceType, } from '../lib/resources.js';
|
|
1
|
+
import { buildCrudParams, listComponentsFromProduct, listResourceTypes, proxyMethodForVerb, resolveResourceType, } from '../lib/resources.js';
|
|
2
2
|
import { resolveResourceSchema } from '../lib/forms/index.js';
|
|
3
3
|
import { isInteractive, promptTag } from '../lib/forms/prompt.js';
|
|
4
4
|
import { toInteractiveOpts } from '../lib/interactive-opts.js';
|
|
@@ -19,6 +19,26 @@ function unwrapArray(value) {
|
|
|
19
19
|
}
|
|
20
20
|
return undefined;
|
|
21
21
|
}
|
|
22
|
+
async function reconcileCreatedComponent(proxy, module, productTag, tag, timeoutMs = 20_000) {
|
|
23
|
+
const started = Date.now();
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
do {
|
|
26
|
+
attempts += 1;
|
|
27
|
+
try {
|
|
28
|
+
const product = await proxy.execute('product', 'fetch', [productTag]);
|
|
29
|
+
const catalogue = listComponentsFromProduct(module, product);
|
|
30
|
+
const resource = catalogue?.find((item) => item && typeof item === 'object' && item.tag === tag);
|
|
31
|
+
if (resource)
|
|
32
|
+
return { resource, attempts, elapsed_ms: Date.now() - started };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Preserve the original mutation error. Reconciliation is a bounded best-effort read.
|
|
36
|
+
}
|
|
37
|
+
if (Date.now() - started < timeoutMs)
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
39
|
+
} while (Date.now() - started < timeoutMs);
|
|
40
|
+
return { resource: null, attempts, elapsed_ms: Date.now() - started };
|
|
41
|
+
}
|
|
22
42
|
/**
|
|
23
43
|
* Session inventories on older platform versions can fail when the component does not exist.
|
|
24
44
|
* Confirm the state through the product catalogue before treating that failure as an empty list,
|
|
@@ -302,6 +322,14 @@ export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
|
302
322
|
body: payload,
|
|
303
323
|
});
|
|
304
324
|
const proxy = getSdkProxy(session);
|
|
325
|
+
if (crud === 'list') {
|
|
326
|
+
const product = await proxy.execute('product', 'fetch', [productTag]);
|
|
327
|
+
const catalogue = listComponentsFromProduct(module, product);
|
|
328
|
+
if (catalogue !== undefined) {
|
|
329
|
+
printJson(catalogue, Boolean(opts.json));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
305
333
|
if (crud === 'list' && module === 'sessions') {
|
|
306
334
|
printJson(await listSessionsWithProductFallback(proxy, productTag), Boolean(opts.json));
|
|
307
335
|
return;
|
|
@@ -316,15 +344,25 @@ export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
|
316
344
|
throw error;
|
|
317
345
|
}
|
|
318
346
|
try {
|
|
319
|
-
const
|
|
320
|
-
if (!
|
|
321
|
-
throw error;
|
|
322
|
-
|
|
347
|
+
const reconciliation = await reconcileCreatedComponent(proxy, module, productTag, createdTag);
|
|
348
|
+
if (!reconciliation.resource) {
|
|
349
|
+
throw new DuctapeOperationError(`${error.message} Reconciliation polled the administrative product catalogue ${reconciliation.attempts} time(s) for ${reconciliation.elapsed_ms}ms and did not find "${createdTag}".`, { ...error.details, code: 'MUTATION_OUTCOME_UNKNOWN', mutationState: 'unknown' });
|
|
350
|
+
}
|
|
351
|
+
printJson({
|
|
352
|
+
created: true,
|
|
353
|
+
reconciled: true,
|
|
354
|
+
outcome: 'succeeded_after_reconciliation',
|
|
355
|
+
reconciliation: { attempts: reconciliation.attempts, elapsed_ms: reconciliation.elapsed_ms },
|
|
356
|
+
resource: reconciliation.resource,
|
|
357
|
+
}, Boolean(opts.json));
|
|
323
358
|
return;
|
|
324
359
|
}
|
|
325
360
|
catch (reconcileError) {
|
|
326
361
|
if (reconcileError === error)
|
|
327
362
|
throw error;
|
|
363
|
+
if (reconcileError instanceof DuctapeOperationError && reconcileError.details.code === 'MUTATION_OUTCOME_UNKNOWN') {
|
|
364
|
+
throw reconcileError;
|
|
365
|
+
}
|
|
328
366
|
throw new DuctapeOperationError(`${error.message} Reconciliation could not confirm whether "${createdTag}" exists.`, { ...error.details, code: 'MUTATION_OUTCOME_UNKNOWN', mutationState: 'unknown' }, { cause: reconcileError });
|
|
329
367
|
}
|
|
330
368
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { runLogin } from './commands/login.js';
|
|
7
7
|
import { runLogout } from './commands/logout.js';
|
|
8
8
|
import { runWhoami } from './commands/whoami.js';
|
|
9
|
+
import { runDoctor } from './commands/doctor.js';
|
|
9
10
|
import { runProfilesList, runProfilesUse } from './commands/profiles.js';
|
|
10
11
|
import { runLink } from './commands/link.js';
|
|
11
12
|
import { runUnlink } from './commands/unlink.js';
|
|
@@ -13,7 +14,7 @@ import { runInit } from './commands/init.js';
|
|
|
13
14
|
import { runInstall } from './commands/install.js';
|
|
14
15
|
import { runStart, runStop, runStatus } from './commands/platform.js';
|
|
15
16
|
import { runResourceCrud, runResourcesList, runEventTopicCrud, runNotificationMessageCrud } from './commands/resources.js';
|
|
16
|
-
import { runCloud } from './commands/cloud.js';
|
|
17
|
+
import { runCloud, runCloudPreflight } from './commands/cloud.js';
|
|
17
18
|
import { runDb, runDbContext } from './commands/db.js';
|
|
18
19
|
import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
|
|
19
20
|
import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
|
|
@@ -84,6 +85,11 @@ program
|
|
|
84
85
|
skipWorkspaceSelect: Boolean(opts.skipWorkspaceSelect),
|
|
85
86
|
})));
|
|
86
87
|
program.command('logout').description('Clear local credentials').action(wrap(runLogout));
|
|
88
|
+
program
|
|
89
|
+
.command('doctor')
|
|
90
|
+
.description('Report CLI, SDK, MCP, API/schema, authentication, and project compatibility facts')
|
|
91
|
+
.option('--json', 'JSON output')
|
|
92
|
+
.action(wrap((opts) => runDoctor({ json: Boolean(opts.json), cliVersion: pkg.version })));
|
|
87
93
|
program
|
|
88
94
|
.command('whoami')
|
|
89
95
|
.description('Show login and linked project')
|
|
@@ -704,6 +710,13 @@ resources
|
|
|
704
710
|
.option('--json', 'JSON output')
|
|
705
711
|
.action(wrap((type, verb, opts) => runResourceCrud(type, verb, opts, [])));
|
|
706
712
|
const cloud = program.command('cloud').description('Cloud connections & resources');
|
|
713
|
+
cloud
|
|
714
|
+
.command('preflight')
|
|
715
|
+
.description('Read-only provider capability, connection, tier, instance, and environment preflight')
|
|
716
|
+
.option('--product <tag>', 'Product tag (defaults to linked project)')
|
|
717
|
+
.option('-f, --file <path>', 'Optional JSON with discovery requests')
|
|
718
|
+
.option('--json', 'JSON output')
|
|
719
|
+
.action(wrap((opts) => runCloudPreflight({ product: opts.product, file: opts.file, json: opts.json })));
|
|
707
720
|
cloud
|
|
708
721
|
.command('connections <verb> [id]')
|
|
709
722
|
.option('-f, --file <path>')
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export declare function buildCrudParams(module: SDKModule, method: string, produ
|
|
|
10
10
|
body?: unknown;
|
|
11
11
|
}): unknown[];
|
|
12
12
|
export declare function listResourceTypes(): string[];
|
|
13
|
+
export declare function listComponentsFromProduct(module: SDKModule, value: unknown): unknown[] | undefined;
|
package/dist/lib/resources.js
CHANGED
|
@@ -85,8 +85,8 @@ export function buildCrudParams(module, method, productTag, args) {
|
|
|
85
85
|
return [{ product: productTag, tag, ...(body ?? {}) }];
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
if ((module === 'storage' || module === 'databases') && method === 'create') {
|
|
89
|
-
//
|
|
88
|
+
if ((module === 'storage' || module === 'databases' || module === 'graph') && method === 'create') {
|
|
89
|
+
// These create methods expect a single config object with product embedded,
|
|
90
90
|
// not the default (productTag, data) two-argument form.
|
|
91
91
|
const payload = body && typeof body === 'object'
|
|
92
92
|
? { product: productTag, ...body }
|
|
@@ -170,3 +170,28 @@ export function listResourceTypes() {
|
|
|
170
170
|
'app',
|
|
171
171
|
];
|
|
172
172
|
}
|
|
173
|
+
const PRODUCT_COMPONENT_KEYS = {
|
|
174
|
+
graph: ['graphs'],
|
|
175
|
+
databases: ['databases'],
|
|
176
|
+
storage: ['storage'],
|
|
177
|
+
vector: ['vectors', 'vector_databases'],
|
|
178
|
+
caches: ['caches'],
|
|
179
|
+
notifications: ['notifications'],
|
|
180
|
+
messageBrokers: ['message_brokers', 'messageBrokers', 'brokers'],
|
|
181
|
+
sessions: ['sessions'],
|
|
182
|
+
quotas: ['quota', 'quotas'],
|
|
183
|
+
health: ['healthchecks'],
|
|
184
|
+
fallback: ['fallback', 'fallbacks'],
|
|
185
|
+
models: ['models'],
|
|
186
|
+
};
|
|
187
|
+
export function listComponentsFromProduct(module, value) {
|
|
188
|
+
const keys = PRODUCT_COMPONENT_KEYS[module];
|
|
189
|
+
if (!keys || !value || typeof value !== 'object')
|
|
190
|
+
return undefined;
|
|
191
|
+
const envelope = value;
|
|
192
|
+
const product = envelope.data && typeof envelope.data === 'object'
|
|
193
|
+
? envelope.data
|
|
194
|
+
: envelope;
|
|
195
|
+
const key = keys.find((candidate) => Array.isArray(product[candidate]));
|
|
196
|
+
return key ? product[key] : [];
|
|
197
|
+
}
|
package/dist/lib/templates.js
CHANGED
|
@@ -90,7 +90,14 @@ Docs: https://docs.ductape.app/docs/cli/
|
|
|
90
90
|
name: 'User session',
|
|
91
91
|
description: 'Standard authenticated user session',
|
|
92
92
|
expiry: 604800,
|
|
93
|
+
period: 'seconds',
|
|
93
94
|
refresh_expiry: 2592000,
|
|
95
|
+
refresh_period: 'seconds',
|
|
96
|
+
refresh_rotation: 'rotate_on_use',
|
|
97
|
+
selector: 'userId',
|
|
98
|
+
schema: {
|
|
99
|
+
userId: { type: 'string', required: true },
|
|
100
|
+
},
|
|
94
101
|
},
|
|
95
102
|
];
|
|
96
103
|
fs.writeFileSync(sessionsPath, JSON.stringify(sessionsTemplate, null, 2) + '\n');
|
package/package.json
CHANGED