@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 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.status;
163
- const suffix = status ? ` (HTTP ${status})` : '';
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 { NorbixAuthError, NorbixError, NorbixValidationError } from '@norbix.ai/ts';
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 errorFrom(response.status, raw, url);
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
+ }
@@ -1,4 +1,4 @@
1
- import { NorbixError } from '@norbix.ai/ts';
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 payload = {};
30
+ let raw;
31
31
  if (text) {
32
32
  try {
33
- payload = JSON.parse(text);
33
+ raw = JSON.parse(text);
34
34
  }
35
35
  catch {
36
- payload = {};
36
+ raw = text;
37
37
  }
38
38
  }
39
- if (!response.ok) {
40
- throw new NorbixError({
41
- code: payload.status,
42
- message: payload.message ?? text ?? 'Request failed',
43
- status: response.status,
44
- });
45
- }
46
- return payload;
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