@norbix.ai/cli 0.3.0 → 1.0.0
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 +23 -0
- package/dist/base.js +14 -3
- package/dist/lib/filesIntegrationTest.js +2 -16
- package/dist/lib/gatewayError.js +58 -0
- package/dist/lib/publicFiles.js +12 -12
- package/oclif.manifest.json +445 -445
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -125,6 +125,29 @@ pipe into `jq`:
|
|
|
125
125
|
norbix db find orders --json | jq '.list.items[] | ._id'
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
A failed call prints one line: the error code, the message, and the HTTP
|
|
131
|
+
status.
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
$ norbix files info nbin_1 a/b.txt
|
|
135
|
+
› Error: CM-ERRORS-FILES-016: File not found: "a/b.txt" does not exist in
|
|
136
|
+
› Local (nbin_1). (HTTP 404)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The code and the message are the gateway's own — they come out of
|
|
140
|
+
`responseStatus.errors[]`, where the gateway puts them. Before this, every
|
|
141
|
+
failure read "Request failed with status 404" with no code.
|
|
142
|
+
|
|
143
|
+
Exit codes do not change: `1` for a failed call, `2` for a check that ran and
|
|
144
|
+
came back negative (`norbix files integrations test`).
|
|
145
|
+
|
|
146
|
+
**A refusal the gateway answers with HTTP 200** and
|
|
147
|
+
`responseStatus.isSuccess = false` now exits non-zero as well, wherever it did
|
|
148
|
+
not before — `norbix files publish` / `unpublish` were the two commands that
|
|
149
|
+
used to treat it as a success.
|
|
150
|
+
|
|
128
151
|
## Development
|
|
129
152
|
|
|
130
153
|
```sh
|
package/dist/base.js
CHANGED
|
@@ -157,12 +157,23 @@ export class BaseCommand extends Command {
|
|
|
157
157
|
this.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* One line for a failed call: `<errorCode>: <message> (HTTP <status>)`.
|
|
162
|
+
*
|
|
163
|
+
* The code comes first because it is the part a reader can search for and a
|
|
164
|
+
* script can match on. It is the gateway's own code — the SDK reads it out
|
|
165
|
+
* of `responseStatus.errors[]`, where the gateway puts it (10b-files slice
|
|
166
|
+
* ERRORS, issue #66). The exit codes do not change.
|
|
167
|
+
*/
|
|
160
168
|
async catch(error) {
|
|
161
169
|
if (error instanceof NorbixError) {
|
|
162
|
-
const status = error
|
|
163
|
-
const
|
|
170
|
+
const { code, status } = error;
|
|
171
|
+
const prefix = code ? `${code}: ` : '';
|
|
172
|
+
// The SDK's last-resort message already ends in "(HTTP <status>)" —
|
|
173
|
+
// do not say it twice.
|
|
174
|
+
const suffix = status && !error.message.includes(`(HTTP ${status})`) ? ` (HTTP ${status})` : '';
|
|
164
175
|
const hint = status === 401 ? '\nYour session may have expired. Run `norbix login` again.' : '';
|
|
165
|
-
return this.error(`${error.message}${suffix}${hint}`);
|
|
176
|
+
return this.error(`${prefix}${error.message}${suffix}${hint}`);
|
|
166
177
|
}
|
|
167
178
|
return super.catch(error);
|
|
168
179
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { gatewayError } from './gatewayError.js';
|
|
2
2
|
/** The API version the SDK talks by default. Kept in one place. */
|
|
3
3
|
const API_VERSION = 'v2';
|
|
4
4
|
export async function callTestFilesIntegration(ctx, filesIntegrationId) {
|
|
@@ -32,23 +32,9 @@ export async function callTestFilesIntegration(ctx, filesIntegrationId) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
if (!response.ok)
|
|
35
|
-
throw
|
|
35
|
+
throw gatewayError(response.status, raw, url);
|
|
36
36
|
return (raw && typeof raw === 'object' ? raw : {});
|
|
37
37
|
}
|
|
38
|
-
/** Same mapping as the SDK transport: 401/403 → auth, 400 → validation, else NorbixError. */
|
|
39
|
-
function errorFrom(status, raw, url) {
|
|
40
|
-
const body = raw && typeof raw === 'object' ? raw : undefined;
|
|
41
|
-
const payload = body?.responseStatus ?? body;
|
|
42
|
-
const message = payload?.message ??
|
|
43
|
-
body?.responseStatus?.errors?.find((e) => e.message)?.message ??
|
|
44
|
-
`Request failed with status ${status}`;
|
|
45
|
-
const opts = { code: payload?.errorCode, message, raw, status, url };
|
|
46
|
-
if (status === 401 || status === 403)
|
|
47
|
-
return new NorbixAuthError(opts);
|
|
48
|
-
if (status === 400)
|
|
49
|
-
return new NorbixValidationError(opts);
|
|
50
|
-
return new NorbixError(opts);
|
|
51
|
-
}
|
|
52
38
|
/** `Failed` / `NotTested` / `NOT_TESTED` → `FAILED` / `NOT_TESTED`. */
|
|
53
39
|
export function normaliseResult(result) {
|
|
54
40
|
if (!result)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NorbixAuthError, NorbixError, NorbixValidationError } from '@norbix.ai/ts';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/** The `responseStatus` block of a body, whatever the casing of the key. */
|
|
6
|
+
function responseStatusOf(raw) {
|
|
7
|
+
if (!isRecord(raw))
|
|
8
|
+
return undefined;
|
|
9
|
+
for (const key of Object.keys(raw)) {
|
|
10
|
+
if (key.toLowerCase() === 'responsestatus') {
|
|
11
|
+
const value = raw[key];
|
|
12
|
+
return isRecord(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function text(value) {
|
|
18
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function itemsOf(value) {
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
return [];
|
|
23
|
+
return value.filter((entry) => isRecord(entry)).map((entry) => entry);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True when the gateway said the call failed inside the body.
|
|
27
|
+
*
|
|
28
|
+
* A business refusal — an unknown id, a rule that says no — comes back as HTTP
|
|
29
|
+
* 200 with `responseStatus.isSuccess = false` (10b-files, issue #67).
|
|
30
|
+
*/
|
|
31
|
+
export function saysItFailed(raw) {
|
|
32
|
+
const status = responseStatusOf(raw);
|
|
33
|
+
return isRecord(status) && status.isSuccess === false;
|
|
34
|
+
}
|
|
35
|
+
/** The message and the error code a gateway answer carries. */
|
|
36
|
+
export function readGatewayError(raw, status) {
|
|
37
|
+
const responseStatus = responseStatusOf(raw);
|
|
38
|
+
// `source` is responseStatus when the body has one, the body itself when it
|
|
39
|
+
// has none — so the top-level fields are read only in the second case.
|
|
40
|
+
const source = responseStatus ?? (isRecord(raw) ? raw : undefined);
|
|
41
|
+
const errors = itemsOf(source?.errors);
|
|
42
|
+
const first = errors.find((e) => e.message !== undefined || e.errorCode !== undefined);
|
|
43
|
+
return {
|
|
44
|
+
code: first?.errorCode ?? text(source?.errorCode),
|
|
45
|
+
errors,
|
|
46
|
+
message: first?.message ?? text(source?.message) ?? `Request failed (HTTP ${status})`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Build the right NorbixError for a status and an already-parsed body. */
|
|
50
|
+
export function gatewayError(status, raw, url) {
|
|
51
|
+
const { code, message } = readGatewayError(raw, status);
|
|
52
|
+
const opts = { code, message, raw, status, url };
|
|
53
|
+
if (status === 401 || status === 403)
|
|
54
|
+
return new NorbixAuthError(opts);
|
|
55
|
+
if (status === 400)
|
|
56
|
+
return new NorbixValidationError(opts);
|
|
57
|
+
return new NorbixError(opts);
|
|
58
|
+
}
|
package/dist/lib/publicFiles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { gatewayError, saysItFailed } from './gatewayError.js';
|
|
2
2
|
const ROUTES = {
|
|
3
3
|
makeFilePrivate: 'files/item/private',
|
|
4
4
|
makeFilePublic: 'files/item/public',
|
|
@@ -27,23 +27,23 @@ export async function callPublicFiles(ctx, operation, body) {
|
|
|
27
27
|
const url = `${base}/${HUB_VERSION}/${ROUTES[operation]}`;
|
|
28
28
|
const response = await fetch(url, { body: JSON.stringify(body), headers, method: 'POST' });
|
|
29
29
|
const text = await response.text();
|
|
30
|
-
let
|
|
30
|
+
let raw;
|
|
31
31
|
if (text) {
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
raw = JSON.parse(text);
|
|
34
34
|
}
|
|
35
35
|
catch {
|
|
36
|
-
|
|
36
|
+
raw = text;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
if (!response.ok)
|
|
40
|
-
throw
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
39
|
+
if (!response.ok)
|
|
40
|
+
throw gatewayError(response.status, raw, url);
|
|
41
|
+
// A 2xx does not mean the call worked: the gateway answers a business
|
|
42
|
+
// refusal with HTTP 200 and responseStatus.isSuccess = false, and the
|
|
43
|
+
// command must exit non-zero for it (10b-files, issue #67).
|
|
44
|
+
if (saysItFailed(raw))
|
|
45
|
+
throw gatewayError(response.status, raw, url);
|
|
46
|
+
return (raw && typeof raw === 'object' ? raw : {});
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
49
|
* The address anyone can open. The gateway builds the same one and puts it on
|