@lotics/app-sdk 0.93.0 → 0.94.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/dist/src/hooks.d.ts +10 -5
- package/dist/src/rpc.js +41 -3
- package/docs/mutations.md +7 -4
- package/package.json +1 -1
package/dist/src/hooks.d.ts
CHANGED
|
@@ -334,11 +334,16 @@ export interface WorkflowResult<TData = unknown> {
|
|
|
334
334
|
files?: UploadedFile[];
|
|
335
335
|
data?: TData;
|
|
336
336
|
/**
|
|
337
|
-
* Per-input refusals the
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
337
|
+
* Per-input refusals, keyed by the INPUT name the alias declares — what a
|
|
338
|
+
* form wires straight onto the control that is wrong
|
|
339
|
+
* (`<FormField error={result.field_errors?.ly_do}>`), where `message` can
|
|
340
|
+
* only say it at the dialog's scope.
|
|
341
|
+
*
|
|
342
|
+
* Either half of the round trip fills it: the SERVER, when the payload does
|
|
343
|
+
* not match what the alias declares, and the workflow's own
|
|
344
|
+
* `return({ field_errors })`. One key, so a screen wires the control once
|
|
345
|
+
* rather than branching on which half refused. Absent when neither named a
|
|
346
|
+
* field — and an older server names none.
|
|
342
347
|
*/
|
|
343
348
|
field_errors?: Record<string, string>;
|
|
344
349
|
}
|
package/dist/src/rpc.js
CHANGED
|
@@ -568,6 +568,34 @@ export function transportErrorMessage(status, parsed) {
|
|
|
568
568
|
parsed.code.length > 0;
|
|
569
569
|
return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
|
|
570
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* A failed request, carrying the per-input refusals when the API named any.
|
|
573
|
+
*
|
|
574
|
+
* A body that does not match what an alias declares is answered 400 with
|
|
575
|
+
* top-level `field_errors` — one sentence per INPUT name, which is what a form
|
|
576
|
+
* puts on the control that is wrong, where the message can only say it at the
|
|
577
|
+
* screen's scope. It rides the error because the transport's own callers are
|
|
578
|
+
* what turn a failure into the result an app reads. Optional throughout: a
|
|
579
|
+
* server that predates them sends none.
|
|
580
|
+
*/
|
|
581
|
+
class ApiRefusal extends Error {
|
|
582
|
+
field_errors;
|
|
583
|
+
constructor(message, field_errors) {
|
|
584
|
+
super(message);
|
|
585
|
+
this.field_errors = field_errors;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/** The `field_errors` of an error body, when it carries a well-formed one. */
|
|
589
|
+
function readFieldErrors(parsed) {
|
|
590
|
+
if (parsed === null || typeof parsed !== "object" || !("field_errors" in parsed)) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
const raw = parsed.field_errors;
|
|
594
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
595
|
+
return undefined;
|
|
596
|
+
const named = Object.entries(raw).filter((entry) => typeof entry[1] === "string");
|
|
597
|
+
return named.length > 0 ? Object.fromEntries(named) : undefined;
|
|
598
|
+
}
|
|
571
599
|
/**
|
|
572
600
|
* The error a stream that never started should throw.
|
|
573
601
|
*
|
|
@@ -663,8 +691,9 @@ async function apiCall(method, path, body, opts) {
|
|
|
663
691
|
return new Promise(() => { });
|
|
664
692
|
}
|
|
665
693
|
// Never surface a non-JSON body (a gateway HTML error page) or a 5xx body as
|
|
666
|
-
// the message — emit a body-free, status-derived message instead.
|
|
667
|
-
|
|
694
|
+
// the message — emit a body-free, status-derived message instead. The
|
|
695
|
+
// per-input refusals ride along, for the caller that can place them.
|
|
696
|
+
throw new ApiRefusal(transportErrorMessage(res.status, parsed), readFieldErrors(parsed));
|
|
668
697
|
}
|
|
669
698
|
return parsed ?? (text ? text : {});
|
|
670
699
|
}
|
|
@@ -793,7 +822,16 @@ async function standaloneWorkflow(p) {
|
|
|
793
822
|
// rejection carrying a raw body) so an app reads `result.status === "error"`
|
|
794
823
|
// uniformly with a handled workflow error. `apiCall` already sanitized the
|
|
795
824
|
// message, so it never contains an HTML body.
|
|
796
|
-
|
|
825
|
+
//
|
|
826
|
+
// A payload the alias refuses is that same shape plus the inputs it named,
|
|
827
|
+
// which is the half a form can act on — the same key a workflow's own
|
|
828
|
+
// `return({ field_errors })` arrives under, so a screen wires one control
|
|
829
|
+
// once whichever half answered.
|
|
830
|
+
return {
|
|
831
|
+
status: "error",
|
|
832
|
+
message: err instanceof Error ? err.message : "The workflow failed to run.",
|
|
833
|
+
...(err instanceof ApiRefusal && err.field_errors ? { field_errors: err.field_errors } : {}),
|
|
834
|
+
};
|
|
797
835
|
}
|
|
798
836
|
}
|
|
799
837
|
async function standaloneAgentRuns(p) {
|
package/docs/mutations.md
CHANGED
|
@@ -65,7 +65,7 @@ Every call resolves to a `WorkflowResult<TData>` (type exported from the package
|
|
|
65
65
|
| `message` | `string?` | The workflow's `return({ message })` text, a validation/binding error, or a body-free transport message |
|
|
66
66
|
| `files` | `UploadedFile[]?` | Files generated during the run (auto-collected — below). Absent when the run generated none |
|
|
67
67
|
| `data` | `TData?` | Structured data from `return({ data })`. Absent when no return step ran |
|
|
68
|
-
| `field_errors` | `Record<string, string>?` | Per-input refusals
|
|
68
|
+
| `field_errors` | `Record<string, string>?` | Per-input refusals, keyed by the INPUT name the alias declares — from the server when the payload does not match what the alias declares, and from the workflow's own `return({ field_errors })`. Absent when neither named a field |
|
|
69
69
|
|
|
70
70
|
### The failure model: check `status`, never just try/catch
|
|
71
71
|
|
|
@@ -111,8 +111,10 @@ passes through unvalidated).
|
|
|
111
111
|
|
|
112
112
|
### Locating a refusal: `field_errors`
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
Two things refuse a call, and both address the input they refused. A payload that does not match
|
|
115
|
+
what the alias declares — a missing required input, a value of the wrong type — is refused before
|
|
116
|
+
the workflow runs, with one sentence per input name. A workflow that validates its own inputs
|
|
117
|
+
refuses the same way, with both halves — a message for the dialog and a map for the controls:
|
|
116
118
|
|
|
117
119
|
```ts
|
|
118
120
|
return({ status: "error", message: "Thiếu lý do.",
|
|
@@ -130,7 +132,8 @@ setErrs(result.field_errors ?? {});
|
|
|
130
132
|
```
|
|
131
133
|
|
|
132
134
|
Keep `message` too — it is what a refusal with no field to blame says, and the two are shown
|
|
133
|
-
together (a `Callout` at the dialog's scope plus the per-field text).
|
|
135
|
+
together (a `Callout` at the dialog's scope plus the per-field text). One reading covers both
|
|
136
|
+
refusals: the key is the same whichever half answered, so a form never branches on that.
|
|
134
137
|
|
|
135
138
|
### Generated files come back in `files[]`
|
|
136
139
|
|