@xano/cli 0.0.94 → 0.0.95-beta.10
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/README.md +28 -1
- package/dist/base-command.d.ts +25 -0
- package/dist/base-command.js +37 -0
- package/dist/commands/auth/index.js +1 -1
- package/dist/commands/profile/create/index.js +2 -2
- package/dist/commands/profile/edit/index.js +2 -2
- package/dist/commands/profile/me/index.js +21 -2
- package/dist/commands/profile/wizard/index.js +3 -3
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/release/deploy/index.d.ts +17 -0
- package/dist/commands/release/deploy/index.js +107 -0
- package/dist/commands/sandbox/env/delete/index.d.ts +14 -0
- package/dist/commands/sandbox/env/delete/index.js +87 -0
- package/dist/commands/sandbox/env/get/index.d.ts +12 -0
- package/dist/commands/sandbox/env/get/index.js +63 -0
- package/dist/commands/sandbox/env/get_all/index.d.ts +13 -0
- package/dist/commands/sandbox/env/get_all/index.js +76 -0
- package/dist/commands/sandbox/env/list/index.d.ts +11 -0
- package/dist/commands/sandbox/env/list/index.js +65 -0
- package/dist/commands/sandbox/env/set/index.d.ts +13 -0
- package/dist/commands/sandbox/env/set/index.js +72 -0
- package/dist/commands/sandbox/env/set_all/index.d.ts +13 -0
- package/dist/commands/sandbox/env/set_all/index.js +84 -0
- package/dist/commands/sandbox/get/index.d.ts +11 -0
- package/dist/commands/sandbox/get/index.js +61 -0
- package/dist/commands/sandbox/impersonate/index.d.ts +5 -0
- package/dist/commands/sandbox/impersonate/index.js +5 -0
- package/dist/commands/sandbox/license/get/index.d.ts +13 -0
- package/dist/commands/sandbox/license/get/index.js +76 -0
- package/dist/commands/sandbox/license/set/index.d.ts +14 -0
- package/dist/commands/sandbox/license/set/index.js +93 -0
- package/dist/commands/sandbox/pull/index.d.ts +17 -0
- package/dist/commands/sandbox/pull/index.js +180 -0
- package/dist/commands/sandbox/push/index.d.ts +18 -0
- package/dist/commands/sandbox/push/index.js +141 -0
- package/dist/commands/sandbox/reset/index.d.ts +12 -0
- package/dist/commands/sandbox/reset/index.js +69 -0
- package/dist/commands/sandbox/review/index.d.ts +13 -0
- package/dist/commands/sandbox/review/index.js +92 -0
- package/dist/commands/sandbox/unit_test/list/index.d.ts +13 -0
- package/dist/commands/sandbox/unit_test/list/index.js +89 -0
- package/dist/commands/sandbox/unit_test/run/index.d.ts +14 -0
- package/dist/commands/sandbox/unit_test/run/index.js +77 -0
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +13 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +167 -0
- package/dist/commands/sandbox/workflow_test/delete/index.d.ts +17 -0
- package/dist/commands/sandbox/workflow_test/delete/index.js +59 -0
- package/dist/commands/sandbox/workflow_test/get/index.d.ts +17 -0
- package/dist/commands/sandbox/workflow_test/get/index.js +58 -0
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +12 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +82 -0
- package/dist/commands/sandbox/workflow_test/run/index.d.ts +17 -0
- package/dist/commands/sandbox/workflow_test/run/index.js +75 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +12 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +153 -0
- package/dist/commands/tenant/create/index.d.ts +2 -2
- package/dist/commands/tenant/create/index.js +23 -11
- package/dist/commands/tenant/get/index.js +2 -2
- package/dist/commands/tenant/list/index.js +2 -2
- package/dist/commands/tenant/push/index.js +0 -34
- package/dist/commands/tenant/unit_test/list/index.d.ts +15 -0
- package/dist/commands/tenant/unit_test/list/index.js +140 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +16 -0
- package/dist/commands/tenant/unit_test/run/index.js +128 -0
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +15 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +215 -0
- package/dist/commands/tenant/workflow_test/delete/index.d.ts +19 -0
- package/dist/commands/tenant/workflow_test/delete/index.js +110 -0
- package/dist/commands/tenant/workflow_test/get/index.d.ts +19 -0
- package/dist/commands/tenant/workflow_test/get/index.js +112 -0
- package/dist/commands/tenant/workflow_test/list/index.d.ts +14 -0
- package/dist/commands/tenant/workflow_test/list/index.js +133 -0
- package/dist/commands/tenant/workflow_test/run/index.d.ts +19 -0
- package/dist/commands/tenant/workflow_test/run/index.js +126 -0
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +14 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +201 -0
- package/dist/commands/workspace/edit/index.d.ts +1 -0
- package/dist/commands/workspace/edit/index.js +16 -6
- package/dist/commands/workspace/get/index.js +9 -7
- package/dist/commands/workspace/list/index.d.ts +1 -0
- package/dist/commands/workspace/list/index.js +14 -7
- package/dist/commands/workspace/push/index.js +30 -2
- package/dist/help.d.ts +2 -1
- package/dist/help.js +39 -1
- package/oclif.manifest.json +4701 -2272
- package/package.json +17 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
export default class SandboxWorkflowTestRunAll extends BaseCommand {
|
|
4
|
+
static description = 'Run all workflow tests for a sandbox environment';
|
|
5
|
+
static examples = [
|
|
6
|
+
`$ xano sandbox workflow-test run-all
|
|
7
|
+
Running 3 workflow tests...
|
|
8
|
+
|
|
9
|
+
PASS my-test (0.25s)
|
|
10
|
+
FAIL data-check (0.10s)
|
|
11
|
+
Error: assertion failed
|
|
12
|
+
|
|
13
|
+
Results: 2 passed, 1 failed
|
|
14
|
+
`,
|
|
15
|
+
`$ xano sandbox workflow-test run-all -o json`,
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
branch: Flags.string({
|
|
20
|
+
char: 'b',
|
|
21
|
+
description: 'Filter by branch name',
|
|
22
|
+
required: false,
|
|
23
|
+
}),
|
|
24
|
+
output: Flags.string({
|
|
25
|
+
char: 'o',
|
|
26
|
+
default: 'summary',
|
|
27
|
+
description: 'Output format',
|
|
28
|
+
options: ['summary', 'json'],
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { flags } = await this.parse(SandboxWorkflowTestRunAll);
|
|
34
|
+
const { profile } = this.resolveProfile(flags);
|
|
35
|
+
const baseUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test`;
|
|
36
|
+
try {
|
|
37
|
+
// Step 1: List all workflow tests
|
|
38
|
+
const listParams = new URLSearchParams();
|
|
39
|
+
listParams.set('per_page', '10000');
|
|
40
|
+
if (flags.branch)
|
|
41
|
+
listParams.set('branch', flags.branch);
|
|
42
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
43
|
+
headers: {
|
|
44
|
+
accept: 'application/json',
|
|
45
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
46
|
+
},
|
|
47
|
+
method: 'GET',
|
|
48
|
+
}, flags.verbose, profile.access_token);
|
|
49
|
+
if (!listResponse.ok) {
|
|
50
|
+
const errorText = await listResponse.text();
|
|
51
|
+
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
52
|
+
}
|
|
53
|
+
const data = (await listResponse.json());
|
|
54
|
+
let tests;
|
|
55
|
+
if (Array.isArray(data)) {
|
|
56
|
+
tests = data;
|
|
57
|
+
}
|
|
58
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
59
|
+
tests = data.items;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.error('Unexpected API response format');
|
|
63
|
+
}
|
|
64
|
+
if (tests.length === 0) {
|
|
65
|
+
this.log('No workflow tests found');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (flags.output === 'summary') {
|
|
69
|
+
this.log(`Running ${tests.length} workflow test${tests.length === 1 ? '' : 's'}...\n`);
|
|
70
|
+
}
|
|
71
|
+
// Step 2: Run each test
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const test of tests) {
|
|
74
|
+
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
75
|
+
try {
|
|
76
|
+
const runResponse = await this.verboseFetch(runUrl, {
|
|
77
|
+
headers: {
|
|
78
|
+
accept: 'application/json',
|
|
79
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
method: 'POST',
|
|
83
|
+
}, flags.verbose, profile.access_token);
|
|
84
|
+
if (!runResponse.ok) {
|
|
85
|
+
const errorText = await runResponse.text();
|
|
86
|
+
results.push({
|
|
87
|
+
message: `API error ${runResponse.status}: ${errorText}`,
|
|
88
|
+
name: test.name,
|
|
89
|
+
status: 'fail',
|
|
90
|
+
});
|
|
91
|
+
if (flags.output === 'summary') {
|
|
92
|
+
this.log(`FAIL ${test.name}`);
|
|
93
|
+
this.log(` Error: API error ${runResponse.status}`);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const runResult = (await runResponse.json());
|
|
98
|
+
const passed = runResult.status === 'ok';
|
|
99
|
+
results.push({
|
|
100
|
+
message: runResult.message,
|
|
101
|
+
name: test.name,
|
|
102
|
+
status: passed ? 'pass' : 'fail',
|
|
103
|
+
timing: runResult.timing,
|
|
104
|
+
});
|
|
105
|
+
if (flags.output === 'summary') {
|
|
106
|
+
const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
|
|
107
|
+
if (passed) {
|
|
108
|
+
this.log(`PASS ${test.name}${timing}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.log(`FAIL ${test.name}${timing}`);
|
|
112
|
+
if (runResult.message) {
|
|
113
|
+
this.log(` Error: ${runResult.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
results.push({
|
|
121
|
+
message,
|
|
122
|
+
name: test.name,
|
|
123
|
+
status: 'fail',
|
|
124
|
+
});
|
|
125
|
+
if (flags.output === 'summary') {
|
|
126
|
+
this.log(`FAIL ${test.name}`);
|
|
127
|
+
this.log(` Error: ${message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Step 3: Summary
|
|
132
|
+
const passed = results.filter((r) => r.status === 'pass').length;
|
|
133
|
+
const failed = results.filter((r) => r.status === 'fail').length;
|
|
134
|
+
if (flags.output === 'json') {
|
|
135
|
+
this.log(JSON.stringify({ failed, passed, results }, null, 2));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
139
|
+
}
|
|
140
|
+
if (failed > 0) {
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof Error) {
|
|
146
|
+
this.error(`Failed to run workflow tests: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.error(`Failed to run workflow tests: ${String(error)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -9,11 +9,11 @@ export default class TenantCreate extends BaseCommand {
|
|
|
9
9
|
cluster_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
ephemeral: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
12
|
ingress: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
license: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
license: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
14
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
15
|
platform_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
17
|
tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
18
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
19
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -16,7 +16,8 @@ export default class TenantCreate extends BaseCommand {
|
|
|
16
16
|
`$ xano tenant create "Production"
|
|
17
17
|
Created tenant: Production (production) - ID: 42
|
|
18
18
|
`,
|
|
19
|
-
`$ xano tenant create "Staging" --description "Staging env" --cluster_id 1 --platform_id 1 --
|
|
19
|
+
`$ xano tenant create "Staging" --description "Staging env" --cluster_id 1 --platform_id 1 --type tier2 -o json`,
|
|
20
|
+
`$ xano tenant create "Staging" --type tier2 --cluster_id 1 --license ./license.yaml`,
|
|
20
21
|
];
|
|
21
22
|
static flags = {
|
|
22
23
|
...BaseCommand.baseFlags,
|
|
@@ -33,19 +34,14 @@ Created tenant: Production (production) - ID: 42
|
|
|
33
34
|
description: 'Custom domain for the tenant',
|
|
34
35
|
required: false,
|
|
35
36
|
}),
|
|
36
|
-
ephemeral: Flags.boolean({
|
|
37
|
-
default: false,
|
|
38
|
-
description: 'Mark tenant as ephemeral (allows push operations)',
|
|
39
|
-
}),
|
|
40
37
|
ingress: Flags.boolean({
|
|
41
38
|
allowNo: true,
|
|
42
39
|
default: true,
|
|
43
40
|
description: 'Enable ingress',
|
|
44
41
|
}),
|
|
45
42
|
license: Flags.string({
|
|
46
|
-
|
|
47
|
-
description: '
|
|
48
|
-
options: ['tier1', 'tier2', 'tier3'],
|
|
43
|
+
char: 'l',
|
|
44
|
+
description: 'Path to a license override file to apply during creation',
|
|
49
45
|
required: false,
|
|
50
46
|
}),
|
|
51
47
|
output: Flags.string({
|
|
@@ -59,6 +55,12 @@ Created tenant: Production (production) - ID: 42
|
|
|
59
55
|
description: 'Platform ID to use',
|
|
60
56
|
required: false,
|
|
61
57
|
}),
|
|
58
|
+
type: Flags.string({
|
|
59
|
+
default: 'tier1',
|
|
60
|
+
description: 'Tenant type',
|
|
61
|
+
options: ['tier1', 'tier2', 'tier3'],
|
|
62
|
+
required: false,
|
|
63
|
+
}),
|
|
62
64
|
tasks: Flags.boolean({
|
|
63
65
|
allowNo: true,
|
|
64
66
|
default: true,
|
|
@@ -91,9 +93,8 @@ Created tenant: Production (production) - ID: 42
|
|
|
91
93
|
}
|
|
92
94
|
const body = {
|
|
93
95
|
display: args.display,
|
|
94
|
-
ephemeral: flags.ephemeral,
|
|
95
96
|
ingress: flags.ingress,
|
|
96
|
-
license: flags.
|
|
97
|
+
license: flags.type,
|
|
97
98
|
tag: [],
|
|
98
99
|
tasks: flags.tasks,
|
|
99
100
|
};
|
|
@@ -107,7 +108,16 @@ Created tenant: Production (production) - ID: 42
|
|
|
107
108
|
body.platform_id = flags.platform_id;
|
|
108
109
|
if (flags.domain)
|
|
109
110
|
body.domain = flags.domain;
|
|
110
|
-
if (flags.license
|
|
111
|
+
if (flags.license) {
|
|
112
|
+
const licensePath = path.resolve(flags.license);
|
|
113
|
+
if (!fs.existsSync(licensePath)) {
|
|
114
|
+
this.error(`License file not found: ${licensePath}`);
|
|
115
|
+
}
|
|
116
|
+
const licenseContent = fs.readFileSync(licensePath, 'utf8');
|
|
117
|
+
body.license_overrides = yaml.load(licenseContent);
|
|
118
|
+
}
|
|
119
|
+
const effectiveType = flags.cluster_id ? 'tier3' : flags.type;
|
|
120
|
+
if (effectiveType === 'tier2' || effectiveType === 'tier3') {
|
|
111
121
|
this.warn('This may take a few minutes. Please be patient.');
|
|
112
122
|
}
|
|
113
123
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant`;
|
|
@@ -134,6 +144,8 @@ Created tenant: Production (production) - ID: 42
|
|
|
134
144
|
if (tenant.state) {
|
|
135
145
|
this.log(` State: ${tenant.state}`);
|
|
136
146
|
}
|
|
147
|
+
if (flags.license)
|
|
148
|
+
this.log(` License: applied`);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
catch (error) {
|
|
@@ -99,8 +99,8 @@ Tenant: My Tenant (my-tenant)
|
|
|
99
99
|
this.log(` Tasks: ${tenant.tasks}`);
|
|
100
100
|
if (tenant.ingress !== undefined)
|
|
101
101
|
this.log(` Ingress: ${tenant.ingress}`);
|
|
102
|
-
if (tenant.
|
|
103
|
-
this.log(`
|
|
102
|
+
if (tenant.type)
|
|
103
|
+
this.log(` Type: ${tenant.type}`);
|
|
104
104
|
if (tenant.deployed_at) {
|
|
105
105
|
const d = new Date(tenant.deployed_at);
|
|
106
106
|
const deployedDate = Number.isNaN(d.getTime()) ? tenant.deployed_at : d.toISOString().split('T')[0];
|
|
@@ -92,8 +92,8 @@ Tenants in workspace 5:
|
|
|
92
92
|
for (const tenant of tenants) {
|
|
93
93
|
const state = tenant.state ? ` [${tenant.state}]` : '';
|
|
94
94
|
const license = tenant.license ? ` - ${tenant.license}` : '';
|
|
95
|
-
const
|
|
96
|
-
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${
|
|
95
|
+
const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
|
|
96
|
+
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
|
|
97
97
|
if (tenant.cluster?.name)
|
|
98
98
|
this.log(` Cluster: ${tenant.cluster.name}`);
|
|
99
99
|
const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
|
|
@@ -100,40 +100,6 @@ Truncate all table records before importing
|
|
|
100
100
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
101
101
|
}
|
|
102
102
|
const tenantName = flags.tenant;
|
|
103
|
-
// Fetch tenant details and verify it's ephemeral
|
|
104
|
-
const tenantApiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
|
|
105
|
-
try {
|
|
106
|
-
const tenantResponse = await this.verboseFetch(tenantApiUrl, {
|
|
107
|
-
headers: {
|
|
108
|
-
accept: 'application/json',
|
|
109
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
110
|
-
},
|
|
111
|
-
method: 'GET',
|
|
112
|
-
}, flags.verbose, profile.access_token);
|
|
113
|
-
if (!tenantResponse.ok) {
|
|
114
|
-
const errorText = await tenantResponse.text();
|
|
115
|
-
this.error(`Failed to fetch tenant '${tenantName}' (${tenantResponse.status}): ${errorText}`);
|
|
116
|
-
}
|
|
117
|
-
const tenantData = (await tenantResponse.json());
|
|
118
|
-
if (!tenantData.ephemeral) {
|
|
119
|
-
this.error(`Tenant '${tenantName}' is not ephemeral. Push is only allowed for ephemeral tenants.\n` +
|
|
120
|
-
`Create an ephemeral tenant with: xano tenant create "name" --ephemeral`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
if (error instanceof Error && error.message.includes('is not ephemeral')) {
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
if (error instanceof Error && error.message.includes('Failed to fetch tenant')) {
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
if (error instanceof Error) {
|
|
131
|
-
this.error(`Failed to verify tenant: ${error.message}`);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
this.error(`Failed to verify tenant: ${String(error)}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
103
|
// Resolve the input directory
|
|
138
104
|
const inputDir = path.resolve(args.directory);
|
|
139
105
|
if (!fs.existsSync(inputDir)) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantUnitTestList extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
export default class TenantUnitTestList extends BaseCommand {
|
|
4
|
+
static description = 'List all unit tests for a tenant';
|
|
5
|
+
static examples = [
|
|
6
|
+
`$ xano tenant unit-test list -t my-tenant
|
|
7
|
+
Unit tests for tenant my-tenant:
|
|
8
|
+
- my-test (ID: abc-123) [function: math]
|
|
9
|
+
`,
|
|
10
|
+
`$ xano tenant unit-test list -t my-tenant -w 5 -o json`,
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
...BaseCommand.baseFlags,
|
|
14
|
+
branch: Flags.string({
|
|
15
|
+
char: 'b',
|
|
16
|
+
description: 'Filter by branch name',
|
|
17
|
+
required: false,
|
|
18
|
+
}),
|
|
19
|
+
'obj-type': Flags.string({
|
|
20
|
+
description: 'Filter by object type',
|
|
21
|
+
options: ['function', 'query', 'middleware'],
|
|
22
|
+
required: false,
|
|
23
|
+
}),
|
|
24
|
+
output: Flags.string({
|
|
25
|
+
char: 'o',
|
|
26
|
+
default: 'summary',
|
|
27
|
+
description: 'Output format',
|
|
28
|
+
options: ['summary', 'json'],
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
tenant: Flags.string({
|
|
32
|
+
char: 't',
|
|
33
|
+
description: 'Tenant name',
|
|
34
|
+
required: true,
|
|
35
|
+
}),
|
|
36
|
+
workspace: Flags.string({
|
|
37
|
+
char: 'w',
|
|
38
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { flags } = await this.parse(TenantUnitTestList);
|
|
44
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
45
|
+
const credentials = this.loadCredentialsFile();
|
|
46
|
+
if (!credentials || !(profileName in credentials.profiles)) {
|
|
47
|
+
this.error(`Profile '${profileName}' not found.\nCreate a profile using 'xano profile create'`);
|
|
48
|
+
}
|
|
49
|
+
const profile = credentials.profiles[profileName];
|
|
50
|
+
if (!profile.instance_origin) {
|
|
51
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
52
|
+
}
|
|
53
|
+
if (!profile.access_token) {
|
|
54
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
55
|
+
}
|
|
56
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
57
|
+
if (!workspaceId) {
|
|
58
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
59
|
+
}
|
|
60
|
+
// Resolve tenant to get its workspace
|
|
61
|
+
const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
|
|
62
|
+
let tenantWorkspaceId;
|
|
63
|
+
try {
|
|
64
|
+
const tenantResponse = await this.verboseFetch(tenantUrl, {
|
|
65
|
+
headers: {
|
|
66
|
+
accept: 'application/json',
|
|
67
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
68
|
+
},
|
|
69
|
+
method: 'GET',
|
|
70
|
+
}, flags.verbose, profile.access_token);
|
|
71
|
+
if (!tenantResponse.ok) {
|
|
72
|
+
const errorText = await tenantResponse.text();
|
|
73
|
+
this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
|
|
74
|
+
}
|
|
75
|
+
const tenant = (await tenantResponse.json());
|
|
76
|
+
tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
this.error(`Failed to resolve tenant: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.error(`Failed to resolve tenant: ${String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
params.set('per_page', '10000');
|
|
88
|
+
if (flags.branch)
|
|
89
|
+
params.set('branch', flags.branch);
|
|
90
|
+
if (flags['obj-type'])
|
|
91
|
+
params.set('obj_type', flags['obj-type']);
|
|
92
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/unit_test?${params}`;
|
|
93
|
+
try {
|
|
94
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
95
|
+
headers: {
|
|
96
|
+
accept: 'application/json',
|
|
97
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
98
|
+
},
|
|
99
|
+
method: 'GET',
|
|
100
|
+
}, flags.verbose, profile.access_token);
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const errorText = await response.text();
|
|
103
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
104
|
+
}
|
|
105
|
+
const data = (await response.json());
|
|
106
|
+
let tests;
|
|
107
|
+
if (Array.isArray(data)) {
|
|
108
|
+
tests = data;
|
|
109
|
+
}
|
|
110
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
111
|
+
tests = data.items;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.error('Unexpected API response format');
|
|
115
|
+
}
|
|
116
|
+
if (flags.output === 'json') {
|
|
117
|
+
this.log(JSON.stringify(tests, null, 2));
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
if (tests.length === 0) {
|
|
121
|
+
this.log('No unit tests found');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.log(`Unit tests for tenant ${flags.tenant}:`);
|
|
125
|
+
for (const test of tests) {
|
|
126
|
+
this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
this.error(`Failed to list unit tests: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.error(`Failed to list unit tests: ${String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantUnitTestRun extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
unit_test_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
export default class TenantUnitTestRun extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
unit_test_id: Args.string({
|
|
6
|
+
description: 'ID of the unit test to run',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Run a unit test for a tenant';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano tenant unit-test run abc-123 -t my-tenant
|
|
13
|
+
Running unit test abc-123...
|
|
14
|
+
Result: PASS
|
|
15
|
+
`,
|
|
16
|
+
`$ xano tenant unit-test run abc-123 -t my-tenant -o json`,
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...BaseCommand.baseFlags,
|
|
20
|
+
output: Flags.string({
|
|
21
|
+
char: 'o',
|
|
22
|
+
default: 'summary',
|
|
23
|
+
description: 'Output format',
|
|
24
|
+
options: ['summary', 'json'],
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
tenant: Flags.string({
|
|
28
|
+
char: 't',
|
|
29
|
+
description: 'Tenant name',
|
|
30
|
+
required: true,
|
|
31
|
+
}),
|
|
32
|
+
workspace: Flags.string({
|
|
33
|
+
char: 'w',
|
|
34
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
async run() {
|
|
39
|
+
const { args, flags } = await this.parse(TenantUnitTestRun);
|
|
40
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
41
|
+
const credentials = this.loadCredentialsFile();
|
|
42
|
+
if (!credentials || !(profileName in credentials.profiles)) {
|
|
43
|
+
this.error(`Profile '${profileName}' not found.\nCreate a profile using 'xano profile create'`);
|
|
44
|
+
}
|
|
45
|
+
const profile = credentials.profiles[profileName];
|
|
46
|
+
if (!profile.instance_origin) {
|
|
47
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
48
|
+
}
|
|
49
|
+
if (!profile.access_token) {
|
|
50
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
51
|
+
}
|
|
52
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
53
|
+
if (!workspaceId) {
|
|
54
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
55
|
+
}
|
|
56
|
+
// Resolve tenant to get its workspace
|
|
57
|
+
const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
|
|
58
|
+
let tenantWorkspaceId;
|
|
59
|
+
try {
|
|
60
|
+
const tenantResponse = await this.verboseFetch(tenantUrl, {
|
|
61
|
+
headers: {
|
|
62
|
+
accept: 'application/json',
|
|
63
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
64
|
+
},
|
|
65
|
+
method: 'GET',
|
|
66
|
+
}, flags.verbose, profile.access_token);
|
|
67
|
+
if (!tenantResponse.ok) {
|
|
68
|
+
const errorText = await tenantResponse.text();
|
|
69
|
+
this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
|
|
70
|
+
}
|
|
71
|
+
const tenant = (await tenantResponse.json());
|
|
72
|
+
tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof Error) {
|
|
76
|
+
this.error(`Failed to resolve tenant: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.error(`Failed to resolve tenant: ${String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
|
|
83
|
+
try {
|
|
84
|
+
if (flags.output === 'summary') {
|
|
85
|
+
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
86
|
+
}
|
|
87
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
88
|
+
headers: {
|
|
89
|
+
accept: 'application/json',
|
|
90
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
method: 'POST',
|
|
94
|
+
}, flags.verbose, profile.access_token);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorText = await response.text();
|
|
97
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
98
|
+
}
|
|
99
|
+
const result = (await response.json());
|
|
100
|
+
if (flags.output === 'json') {
|
|
101
|
+
this.log(JSON.stringify(result, null, 2));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
if (result.status === 'ok') {
|
|
105
|
+
this.log('Result: PASS');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.log('Result: FAIL');
|
|
109
|
+
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
110
|
+
for (const expect of failedExpects) {
|
|
111
|
+
if (expect.message) {
|
|
112
|
+
this.log(` Error: ${expect.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
this.error(`Failed to run unit test: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.error(`Failed to run unit test: ${String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantUnitTestRunAll extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|