@thomasfosterau/effect-forms-svelte 0.1.0 → 0.3.1
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 +137 -23
- package/dist/ClientValidate.d.ts +3 -3
- package/dist/ClientValidate.js +1 -2
- package/dist/ClientValidate.js.map +1 -1
- package/dist/EnhanceCore.d.ts +41 -10
- package/dist/EnhanceCore.js +107 -40
- package/dist/EnhanceCore.js.map +1 -1
- package/dist/EntityForm.d.ts +17 -18
- package/dist/EntityForm.js +28 -28
- package/dist/EntityForm.js.map +1 -1
- package/dist/FormStore.svelte.d.ts +25 -18
- package/dist/FormStore.svelte.js +95 -62
- package/dist/FormStore.svelte.js.map +1 -1
- package/dist/Listeners.d.ts +6 -6
- package/dist/Listeners.js +3 -2
- package/dist/Listeners.js.map +1 -1
- package/dist/SvelteKit.d.ts +6 -4
- package/dist/SvelteKit.js +5 -2
- package/dist/SvelteKit.js.map +1 -1
- package/dist/Tainted.d.ts +7 -7
- package/dist/Tainted.js +27 -39
- package/dist/Tainted.js.map +1 -1
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/kit/ActionRedirect.d.ts +22 -0
- package/dist/kit/ActionRedirect.js +54 -0
- package/dist/kit/ActionRedirect.js.map +1 -0
- package/dist/kit/CommandFailure.d.ts +52 -0
- package/dist/kit/CommandFailure.js +81 -0
- package/dist/kit/CommandFailure.js.map +1 -0
- package/dist/kit/Enhance.d.ts +30 -0
- package/dist/kit/Enhance.js +57 -0
- package/dist/kit/Enhance.js.map +1 -0
- package/dist/kit/FormAction.d.ts +132 -0
- package/dist/kit/FormAction.js +170 -0
- package/dist/kit/FormAction.js.map +1 -0
- package/dist/kit/FormStateReaders.d.ts +58 -0
- package/dist/kit/FormStateReaders.js +64 -0
- package/dist/kit/FormStateReaders.js.map +1 -0
- package/dist/kit/Pointer.d.ts +23 -0
- package/dist/kit/Pointer.js +38 -0
- package/dist/kit/Pointer.js.map +1 -0
- package/dist/kit/index.d.ts +7 -0
- package/dist/kit/index.js +7 -0
- package/package.json +23 -4
package/dist/Tainted.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { Issues } from "@thomasfosterau/effect-forms";
|
|
|
2
2
|
//#region src/Tainted.d.ts
|
|
3
3
|
type FormPath = Issues.FormPath;
|
|
4
4
|
/** A nested tree mirroring the form data, `true` where a leaf has changed. */
|
|
5
|
-
interface TaintedFields {
|
|
5
|
+
export interface TaintedFields {
|
|
6
6
|
readonly [key: string]: boolean | TaintedFields | undefined;
|
|
7
7
|
}
|
|
8
8
|
/** Structural deep equality for plain JSON-ish values. */
|
|
9
|
-
declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
9
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
10
10
|
/**
|
|
11
11
|
* Compare `current` against `original`, returning the nested tainted tree.
|
|
12
12
|
* A changed scalar leaf is `true`; an unchanged leaf is omitted; nested objects
|
|
@@ -14,11 +14,11 @@ declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
|
14
14
|
* row taints only that index — `taintedAt(["items", 2, "name"])` stays precise.
|
|
15
15
|
* Keys present in either side are considered.
|
|
16
16
|
*/
|
|
17
|
-
declare function computeTainted(original: unknown, current: unknown): TaintedFields;
|
|
17
|
+
export declare function computeTainted(original: unknown, current: unknown): TaintedFields;
|
|
18
18
|
/** Whether anything in the tree is tainted. */
|
|
19
|
-
declare function isAnyTainted(tainted: TaintedFields): boolean;
|
|
19
|
+
export declare function isAnyTainted(tainted: TaintedFields): boolean;
|
|
20
20
|
/** Whether the field at `path` (or anything beneath it) is tainted. */
|
|
21
|
-
declare function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean;
|
|
21
|
+
export declare function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean;
|
|
22
22
|
/**
|
|
23
23
|
* Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s
|
|
24
24
|
* that actually changed, in depth-first order — the per-field-changed events
|
|
@@ -26,7 +26,7 @@ declare function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean;
|
|
|
26
26
|
* server error at exactly the field that moved (as opposed to `isAnyTainted`,
|
|
27
27
|
* which only says *something* changed).
|
|
28
28
|
*/
|
|
29
|
-
declare function taintedLeafPaths(tainted: TaintedFields): FormPath[];
|
|
29
|
+
export declare function taintedLeafPaths(tainted: TaintedFields): FormPath[];
|
|
30
30
|
//#endregion
|
|
31
|
-
export {
|
|
31
|
+
export {};
|
|
32
32
|
//# sourceMappingURL=Tainted.d.ts.map
|
package/dist/Tainted.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Predicate from "effect/Predicate";
|
|
3
|
+
import * as Record from "effect/Record";
|
|
1
4
|
//#region src/Tainted.ts
|
|
2
|
-
|
|
5
|
+
/** A non-null object or array, indexable by its own string keys. */
|
|
6
|
+
const isObject = (value) => Predicate.isObjectOrArray(value);
|
|
7
|
+
/** The union of both containers' own enumerable keys, `a`'s first. */
|
|
8
|
+
const unionKeys = (a, b) => Arr.union(Record.keys(a), Record.keys(b));
|
|
3
9
|
/** Structural deep equality for plain JSON-ish values. */
|
|
4
10
|
function deepEqual(a, b) {
|
|
5
11
|
if (a === b) return true;
|
|
6
12
|
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
7
|
-
if (isObject(a) && isObject(b))
|
|
8
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
9
|
-
for (const key of keys) if (!deepEqual(a[key], b[key])) return false;
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
13
|
+
if (isObject(a) && isObject(b)) return Arr.every(unionKeys(a, b), (key) => deepEqual(a[key], b[key]));
|
|
12
14
|
return false;
|
|
13
15
|
}
|
|
14
16
|
/** Whether two containers are the same kind (both arrays, or both plain objects). */
|
|
@@ -22,37 +24,27 @@ const sameKind = (a, b) => Array.isArray(a) === Array.isArray(b);
|
|
|
22
24
|
*/
|
|
23
25
|
function computeTainted(original, current) {
|
|
24
26
|
if (!isObject(original) || !isObject(current) || !sameKind(original, current)) return {};
|
|
25
|
-
|
|
26
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(original), ...Object.keys(current)]);
|
|
27
|
-
for (const key of keys) {
|
|
27
|
+
return Record.fromEntries(Arr.flatMap(unionKeys(original, current), (key) => {
|
|
28
28
|
const before = original[key];
|
|
29
29
|
const after = current[key];
|
|
30
30
|
if (isObject(before) && isObject(after) && sameKind(before, after)) {
|
|
31
31
|
const nested = computeTainted(before, after);
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
return Record.isEmptyReadonlyRecord(nested) ? [] : [[key, nested]];
|
|
33
|
+
}
|
|
34
|
+
return deepEqual(before, after) ? [] : [[key, true]];
|
|
35
|
+
}));
|
|
36
36
|
}
|
|
37
37
|
/** Whether anything in the tree is tainted. */
|
|
38
38
|
function isAnyTainted(tainted) {
|
|
39
|
-
|
|
40
|
-
const value = tainted[key];
|
|
41
|
-
if (value === true) return true;
|
|
42
|
-
if (isObject(value) && isAnyTainted(value)) return true;
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
39
|
+
return Arr.some(Record.values(tainted), (value) => value === true || isObject(value) && isAnyTainted(value));
|
|
45
40
|
}
|
|
46
41
|
/** Whether the field at `path` (or anything beneath it) is tainted. */
|
|
47
42
|
function isTaintedAt(tainted, path) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (node === void 0) return false;
|
|
54
|
-
if (typeof node === "boolean") return node;
|
|
55
|
-
return isAnyTainted(node);
|
|
43
|
+
const go = (node, rest) => Arr.match(rest, {
|
|
44
|
+
onEmpty: () => Predicate.isUndefined(node) ? false : Predicate.isBoolean(node) ? node : isAnyTainted(node),
|
|
45
|
+
onNonEmpty: (segments) => Predicate.isUndefined(node) || Predicate.isBoolean(node) ? node === true : go(node[String(Arr.headNonEmpty(segments))], Arr.tailNonEmpty(segments))
|
|
46
|
+
});
|
|
47
|
+
return go(tainted, path);
|
|
56
48
|
}
|
|
57
49
|
/**
|
|
58
50
|
* Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s
|
|
@@ -62,18 +54,14 @@ function isTaintedAt(tainted, path) {
|
|
|
62
54
|
* which only says *something* changed).
|
|
63
55
|
*/
|
|
64
56
|
function taintedLeafPaths(tainted) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
walk(tainted, []);
|
|
76
|
-
return out;
|
|
57
|
+
const walk = (node, path) => Arr.flatMap(Record.toEntries(node), ([key, value]) => {
|
|
58
|
+
if (Predicate.isUndefined(value)) return [];
|
|
59
|
+
const segment = /^\d+$/.test(key) ? Number(key) : key;
|
|
60
|
+
if (value === true) return [[...path, segment]];
|
|
61
|
+
if (isObject(value)) return walk(value, [...path, segment]);
|
|
62
|
+
return [];
|
|
63
|
+
});
|
|
64
|
+
return walk(tainted, []);
|
|
77
65
|
}
|
|
78
66
|
//#endregion
|
|
79
67
|
export { computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths };
|
package/dist/Tainted.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tainted.js","names":[],"sources":["../src/Tainted.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"Tainted.js","names":[],"sources":["../src/Tainted.ts"],"sourcesContent":["/**\n * Tainted-field tracking — which fields the user has changed since the form\n * was seeded.\n *\n * A `tainted` store lets a UI show \"unsaved changes\",\n * guard navigation, or submit only what changed. This is the pure half: a deep\n * diff of the current data against the original snapshot producing a nested\n * boolean tree mirroring the data's shape. The reactive store\n * (`superForm.svelte.ts`) re-derives it as the data changes.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/Tainted\n */\n\nimport type { Issues } from \"@thomasfosterau/effect-forms\";\nimport * as Arr from \"effect/Array\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Record from \"effect/Record\";\n\ntype FormPath = Issues.FormPath;\n\n/** A nested tree mirroring the form data, `true` where a leaf has changed. */\nexport interface TaintedFields {\n readonly [key: string]: boolean | TaintedFields | undefined;\n}\n\n/** An object or array read through its string keys. */\ntype Indexable = { readonly [key: string]: unknown };\n\n/** A non-null object or array, indexable by its own string keys. */\nconst isObject = (value: unknown): value is Indexable => Predicate.isObjectOrArray(value);\n\n/** The union of both containers' own enumerable keys, `a`'s first. */\nconst unionKeys = (a: Indexable, b: Indexable): Array<string> =>\n Arr.union(Record.keys(a), Record.keys(b));\n\n/** Structural deep equality for plain JSON-ish values. */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n return Arr.every(unionKeys(a, b), (key) => deepEqual(a[key], b[key]));\n }\n return false;\n}\n\n/** Whether two containers are the same kind (both arrays, or both plain objects). */\nconst sameKind = (a: unknown, b: unknown): boolean => Array.isArray(a) === Array.isArray(b);\n\n/**\n * Compare `current` against `original`, returning the nested tainted tree.\n * A changed scalar leaf is `true`; an unchanged leaf is omitted; nested objects\n * **and arrays** recurse (array elements keyed by index), so a single changed\n * row taints only that index — `taintedAt([\"items\", 2, \"name\"])` stays precise.\n * Keys present in either side are considered.\n */\nexport function computeTainted(original: unknown, current: unknown): TaintedFields {\n if (!isObject(original) || !isObject(current) || !sameKind(original, current)) return {};\n\n return Record.fromEntries(\n Arr.flatMap(\n unionKeys(original, current),\n (key): Array<readonly [string, boolean | TaintedFields]> => {\n const before = original[key];\n const after = current[key];\n if (isObject(before) && isObject(after) && sameKind(before, after)) {\n const nested = computeTainted(before, after);\n return Record.isEmptyReadonlyRecord(nested) ? [] : [[key, nested]];\n }\n return deepEqual(before, after) ? [] : [[key, true]];\n },\n ),\n );\n}\n\n/** Whether anything in the tree is tainted. */\nexport function isAnyTainted(tainted: TaintedFields): boolean {\n return Arr.some(\n Record.values(tainted),\n (value) => value === true || (isObject(value) && isAnyTainted(value)),\n );\n}\n\n/** Whether the field at `path` (or anything beneath it) is tainted. */\nexport function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean {\n const go = (node: boolean | TaintedFields | undefined, rest: FormPath): boolean =>\n Arr.match(rest, {\n onEmpty: () =>\n Predicate.isUndefined(node) ? false : Predicate.isBoolean(node) ? node : isAnyTainted(node),\n onNonEmpty: (segments) =>\n Predicate.isUndefined(node) || Predicate.isBoolean(node)\n ? node === true\n : go(node[String(Arr.headNonEmpty(segments))], Arr.tailNonEmpty(segments)),\n });\n return go(tainted, path);\n}\n\n/**\n * Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s\n * that actually changed, in depth-first order — the per-field-changed events\n * a reactive store needs to run per-field validators / clear a superseded\n * server error at exactly the field that moved (as opposed to `isAnyTainted`,\n * which only says *something* changed).\n */\nexport function taintedLeafPaths(tainted: TaintedFields): FormPath[] {\n const walk = (node: TaintedFields, path: Array<string | number>): FormPath[] =>\n Arr.flatMap(Record.toEntries(node), ([key, value]): FormPath[] => {\n if (Predicate.isUndefined(value)) return [];\n const segment: string | number = /^\\d+$/.test(key) ? Number(key) : key;\n if (value === true) return [[...path, segment]];\n if (isObject(value)) return walk(value, [...path, segment]);\n return [];\n });\n\n return walk(tainted, []);\n}\n"],"mappings":";;;;;AA8BA,MAAM,YAAY,UAAuC,UAAU,gBAAgB,KAAK;;AAGxF,MAAM,aAAa,GAAc,MAC/B,IAAI,MAAM,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC;;AAG1C,SAAgB,UAAU,GAAY,GAAqB;CACzD,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GACrC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,MAAM,MAAM,UAAU,MAAM,EAAE,EAAE,CAAC;CAE5E,IAAI,SAAS,CAAC,KAAK,SAAS,CAAC,GAC3B,OAAO,IAAI,MAAM,UAAU,GAAG,CAAC,IAAI,QAAQ,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC;CAEtE,OAAO;AACT;;AAGA,MAAM,YAAY,GAAY,MAAwB,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC;;;;;;;;AAS1F,SAAgB,eAAe,UAAmB,SAAiC;CACjF,IAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,SAAS,OAAO,KAAK,CAAC,SAAS,UAAU,OAAO,GAAG,OAAO,CAAC;CAEvF,OAAO,OAAO,YACZ,IAAI,QACF,UAAU,UAAU,OAAO,IAC1B,QAA2D;EAC1D,MAAM,SAAS,SAAS;EACxB,MAAM,QAAQ,QAAQ;EACtB,IAAI,SAAS,MAAM,KAAK,SAAS,KAAK,KAAK,SAAS,QAAQ,KAAK,GAAG;GAClE,MAAM,SAAS,eAAe,QAAQ,KAAK;GAC3C,OAAO,OAAO,sBAAsB,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,MAAM,CAAC;EACnE;EACA,OAAO,UAAU,QAAQ,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;CACrD,CACF,CACF;AACF;;AAGA,SAAgB,aAAa,SAAiC;CAC5D,OAAO,IAAI,KACT,OAAO,OAAO,OAAO,IACpB,UAAU,UAAU,QAAS,SAAS,KAAK,KAAK,aAAa,KAAK,CACrE;AACF;;AAGA,SAAgB,YAAY,SAAwB,MAAyB;CAC3E,MAAM,MAAM,MAA2C,SACrD,IAAI,MAAM,MAAM;EACd,eACE,UAAU,YAAY,IAAI,IAAI,QAAQ,UAAU,UAAU,IAAI,IAAI,OAAO,aAAa,IAAI;EAC5F,aAAa,aACX,UAAU,YAAY,IAAI,KAAK,UAAU,UAAU,IAAI,IACnD,SAAS,OACT,GAAG,KAAK,OAAO,IAAI,aAAa,QAAQ,CAAC,IAAI,IAAI,aAAa,QAAQ,CAAC;CAC/E,CAAC;CACH,OAAO,GAAG,SAAS,IAAI;AACzB;;;;;;;;AASA,SAAgB,iBAAiB,SAAoC;CACnE,MAAM,QAAQ,MAAqB,SACjC,IAAI,QAAQ,OAAO,UAAU,IAAI,IAAI,CAAC,KAAK,WAAuB;EAChE,IAAI,UAAU,YAAY,KAAK,GAAG,OAAO,CAAC;EAC1C,MAAM,UAA2B,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;EACnE,IAAI,UAAU,MAAM,OAAO,CAAC,CAAC,GAAG,MAAM,OAAO,CAAC;EAC9C,IAAI,SAAS,KAAK,GAAG,OAAO,KAAK,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EAC1D,OAAO,CAAC;CACV,CAAC;CAEH,OAAO,KAAK,SAAS,CAAC,CAAC;AACzB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
//#region src/kit/ActionRedirect.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A user-fixable failure's message; `None` takes the hard-error path.
|
|
5
|
+
*
|
|
6
|
+
* - a `JsonapiError` / `ErrorResponse`-tagged cause at 400 / 409 / 422 → its
|
|
7
|
+
* first error object's `detail`, else `title`, else `fallback` (an
|
|
8
|
+
* `ErrorResponse` with no document → its own message);
|
|
9
|
+
* - an `HttpError`-tagged 400 → its message (or `fallback`);
|
|
10
|
+
* - anything else → `None`.
|
|
11
|
+
*
|
|
12
|
+
* `Some("")` is a deliberate *present* answer: an error object carrying a
|
|
13
|
+
* blank `detail` redirects with an empty parameter rather than escalating.
|
|
14
|
+
*/
|
|
15
|
+
export declare const userFixableMessage: (cause: unknown, fallback: string) => Option.Option<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Append the failure's query parameters to a same-origin return target,
|
|
18
|
+
* answering the path and query only (never an origin).
|
|
19
|
+
*/
|
|
20
|
+
export declare const withFailureParams: (target: string, params: Readonly<Record<string, string>>) => string;
|
|
21
|
+
//#endregion
|
|
22
|
+
//# sourceMappingURL=ActionRedirect.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { detailOf, jsonapiFailure } from "./CommandFailure.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
4
|
+
import * as Record from "effect/Record";
|
|
5
|
+
import * as HashSet from "effect/HashSet";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
//#region src/kit/ActionRedirect.ts
|
|
8
|
+
/**
|
|
9
|
+
* Failure routing for **action-only** routes: a route with no page of its
|
|
10
|
+
* own (a restore, a confirm) always answers a submission with a redirect, so
|
|
11
|
+
* a *user-fixable* command failure rides back to the submitting page as
|
|
12
|
+
* query parameters instead of a `fail()` (which would try to render the
|
|
13
|
+
* action route's nonexistent page for a no-JS submitter). Everything else —
|
|
14
|
+
* auth denials, infrastructure faults — stays a hard error at the caller.
|
|
15
|
+
*
|
|
16
|
+
* @copyright Thomas Foster
|
|
17
|
+
* @module effect-forms-svelte/kit/ActionRedirect
|
|
18
|
+
*/
|
|
19
|
+
/** The statuses whose failure the submitter can act on (the form layer's set). */
|
|
20
|
+
const USER_FIXABLE_STATUSES = HashSet.make(400, 409, 422);
|
|
21
|
+
/**
|
|
22
|
+
* A user-fixable failure's message; `None` takes the hard-error path.
|
|
23
|
+
*
|
|
24
|
+
* - a `JsonapiError` / `ErrorResponse`-tagged cause at 400 / 409 / 422 → its
|
|
25
|
+
* first error object's `detail`, else `title`, else `fallback` (an
|
|
26
|
+
* `ErrorResponse` with no document → its own message);
|
|
27
|
+
* - an `HttpError`-tagged 400 → its message (or `fallback`);
|
|
28
|
+
* - anything else → `None`.
|
|
29
|
+
*
|
|
30
|
+
* `Some("")` is a deliberate *present* answer: an error object carrying a
|
|
31
|
+
* blank `detail` redirects with an empty parameter rather than escalating.
|
|
32
|
+
*/
|
|
33
|
+
const userFixableMessage = (cause, fallback) => {
|
|
34
|
+
if (!Predicate.isObjectOrArray(cause)) return Option.none();
|
|
35
|
+
if (Predicate.isTagged(cause, "HttpError")) {
|
|
36
|
+
if (!Predicate.hasProperty(cause, "status") || cause.status !== 400) return Option.none();
|
|
37
|
+
return Option.some(Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) ? cause.message : fallback);
|
|
38
|
+
}
|
|
39
|
+
if (!Predicate.isTagged(cause, "JsonapiError") && !Predicate.isTagged(cause, "ErrorResponse")) return Option.none();
|
|
40
|
+
return jsonapiFailure(cause).pipe(Option.filter((failure) => HashSet.has(USER_FIXABLE_STATUSES, failure.status)), Option.map((failure) => Predicate.isTagged(failure, "Routed") ? detailOf(failure.errors[0], fallback) : failure.message));
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Append the failure's query parameters to a same-origin return target,
|
|
44
|
+
* answering the path and query only (never an origin).
|
|
45
|
+
*/
|
|
46
|
+
const withFailureParams = (target, params) => {
|
|
47
|
+
const url = new URL(target, "http://localhost");
|
|
48
|
+
Arr.forEach(Record.toEntries(params), ([key, value]) => url.searchParams.set(key, value));
|
|
49
|
+
return url.pathname + url.search;
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { userFixableMessage, withFailureParams };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=ActionRedirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionRedirect.js","names":[],"sources":["../../src/kit/ActionRedirect.ts"],"sourcesContent":["/**\n * Failure routing for **action-only** routes: a route with no page of its\n * own (a restore, a confirm) always answers a submission with a redirect, so\n * a *user-fixable* command failure rides back to the submitting page as\n * query parameters instead of a `fail()` (which would try to render the\n * action route's nonexistent page for a no-JS submitter). Everything else —\n * auth denials, infrastructure faults — stays a hard error at the caller.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/kit/ActionRedirect\n */\n\nimport * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Record from \"effect/Record\";\nimport { detailOf, jsonapiFailure } from \"./CommandFailure.ts\";\n\n/** The statuses whose failure the submitter can act on (the form layer's set). */\nconst USER_FIXABLE_STATUSES: HashSet.HashSet<number> = HashSet.make(400, 409, 422);\n\n/**\n * A user-fixable failure's message; `None` takes the hard-error path.\n *\n * - a `JsonapiError` / `ErrorResponse`-tagged cause at 400 / 409 / 422 → its\n * first error object's `detail`, else `title`, else `fallback` (an\n * `ErrorResponse` with no document → its own message);\n * - an `HttpError`-tagged 400 → its message (or `fallback`);\n * - anything else → `None`.\n *\n * `Some(\"\")` is a deliberate *present* answer: an error object carrying a\n * blank `detail` redirects with an empty parameter rather than escalating.\n */\nexport const userFixableMessage = (cause: unknown, fallback: string): Option.Option<string> => {\n if (!Predicate.isObjectOrArray(cause)) return Option.none();\n if (Predicate.isTagged(cause, \"HttpError\")) {\n if (!Predicate.hasProperty(cause, \"status\") || cause.status !== 400) return Option.none();\n return Option.some(\n Predicate.hasProperty(cause, \"message\") && Predicate.isString(cause.message)\n ? cause.message\n : fallback,\n );\n }\n if (!Predicate.isTagged(cause, \"JsonapiError\") && !Predicate.isTagged(cause, \"ErrorResponse\")) {\n return Option.none();\n }\n return jsonapiFailure(cause).pipe(\n Option.filter((failure) => HashSet.has(USER_FIXABLE_STATUSES, failure.status)),\n Option.map((failure) =>\n Predicate.isTagged(failure, \"Routed\")\n ? detailOf(failure.errors[0], fallback)\n : failure.message,\n ),\n );\n};\n\n/**\n * Append the failure's query parameters to a same-origin return target,\n * answering the path and query only (never an origin).\n */\nexport const withFailureParams = (\n target: string,\n params: Readonly<Record<string, string>>,\n): string => {\n const url = new URL(target, \"http://localhost\");\n Arr.forEach(Record.toEntries(params), ([key, value]) => url.searchParams.set(key, value));\n return url.pathname + url.search;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,MAAM,wBAAiD,QAAQ,KAAK,KAAK,KAAK,GAAG;;;;;;;;;;;;;AAcjF,MAAa,sBAAsB,OAAgB,aAA4C;CAC7F,IAAI,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO,OAAO,KAAK;CAC1D,IAAI,UAAU,SAAS,OAAO,WAAW,GAAG;EAC1C,IAAI,CAAC,UAAU,YAAY,OAAO,QAAQ,KAAK,MAAM,WAAW,KAAK,OAAO,OAAO,KAAK;EACxF,OAAO,OAAO,KACZ,UAAU,YAAY,OAAO,SAAS,KAAK,UAAU,SAAS,MAAM,OAAO,IACvE,MAAM,UACN,QACN;CACF;CACA,IAAI,CAAC,UAAU,SAAS,OAAO,cAAc,KAAK,CAAC,UAAU,SAAS,OAAO,eAAe,GAC1F,OAAO,OAAO,KAAK;CAErB,OAAO,eAAe,KAAK,CAAC,CAAC,KAC3B,OAAO,QAAQ,YAAY,QAAQ,IAAI,uBAAuB,QAAQ,MAAM,CAAC,GAC7E,OAAO,KAAK,YACV,UAAU,SAAS,SAAS,QAAQ,IAChC,SAAS,QAAQ,OAAO,IAAI,QAAQ,IACpC,QAAQ,OACd,CACF;AACF;;;;;AAMA,MAAa,qBACX,QACA,WACW;CACX,MAAM,MAAM,IAAI,IAAI,QAAQ,kBAAkB;CAC9C,IAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,CAAC,KAAK,WAAW,IAAI,aAAa,IAAI,KAAK,KAAK,CAAC;CACxF,OAAO,IAAI,WAAW,IAAI;AAC5B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import { Document } from "@thomasfosterau/effect-jsonapi";
|
|
3
|
+
declare namespace CommandFailure_d_exports {
|
|
4
|
+
export { Classify, CommandFailure, DEFAULT_MESSAGE, ErrorObject, MessageFailure, RoutedFailure, detailOf, jsonapiFailure, message, routed };
|
|
5
|
+
}
|
|
6
|
+
/** One JSON:API error object: `@thomasfosterau/effect-jsonapi`'s `Document.ErrorObject`. */
|
|
7
|
+
export type ErrorObject = typeof Document.ErrorObject.Type;
|
|
8
|
+
/** A failure carrying JSON:API error objects, each routed to a field (or the root) by its pointer. */
|
|
9
|
+
export interface RoutedFailure {
|
|
10
|
+
readonly _tag: "Routed";
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly errors: ReadonlyArray<ErrorObject>;
|
|
13
|
+
}
|
|
14
|
+
/** A failure with no error objects to route: one whole-form message. */
|
|
15
|
+
export interface MessageFailure {
|
|
16
|
+
readonly _tag: "Message";
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a form action needs to know about a recognised command failure. An
|
|
22
|
+
* unrecognised failure is not a `CommandFailure` at all: the classifier
|
|
23
|
+
* answers `None` and the action treats it as unexpected.
|
|
24
|
+
*/
|
|
25
|
+
export type CommandFailure = RoutedFailure | MessageFailure;
|
|
26
|
+
/** Construct a {@link RoutedFailure}. */
|
|
27
|
+
export declare const routed: (status: number, errors: ReadonlyArray<ErrorObject>) => CommandFailure;
|
|
28
|
+
/** Construct a {@link MessageFailure}. */
|
|
29
|
+
export declare const message: (status: number, message: string) => CommandFailure;
|
|
30
|
+
/** A classifier: `Some` for a failure the form can show, `None` for an unexpected one. */
|
|
31
|
+
export type Classify = (cause: unknown) => Option.Option<CommandFailure>;
|
|
32
|
+
/** The fallback message for an error object with neither `detail` nor `title`. */
|
|
33
|
+
export declare const DEFAULT_MESSAGE = "Request could not be completed";
|
|
34
|
+
/** An error object's human message: its `detail`, else its `title`, else `fallback`. */
|
|
35
|
+
export declare const detailOf: (error: ErrorObject | undefined, fallback?: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The default classifier:
|
|
38
|
+
*
|
|
39
|
+
* - a `JsonapiError`-tagged cause (`{ status, errors }`) → its error objects,
|
|
40
|
+
* at its own status;
|
|
41
|
+
* - an `ErrorResponse`-tagged cause (effect-jsonapi's client-side decoded
|
|
42
|
+
* error) → its document's error objects, or its message when the response
|
|
43
|
+
* carried no document;
|
|
44
|
+
* - an `Error` carrying a numeric `status` (a transport's status error, e.g.
|
|
45
|
+
* a 413 oversize upload the transport did not map to a document) → its
|
|
46
|
+
* message, at its own status;
|
|
47
|
+
* - anything else → `None` (unexpected).
|
|
48
|
+
*/
|
|
49
|
+
export declare const jsonapiFailure: Classify;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CommandFailure_d_exports };
|
|
52
|
+
//# sourceMappingURL=CommandFailure.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
//#region src/kit/CommandFailure.ts
|
|
6
|
+
/**
|
|
7
|
+
* Classifying what a form action's command failed with.
|
|
8
|
+
*
|
|
9
|
+
* A command (an API client call, an Effect program) fails with whatever its
|
|
10
|
+
* transport raises. The form layer only needs to know two things about it:
|
|
11
|
+
* which JSON:API error objects it carried (so each can be routed onto a
|
|
12
|
+
* field by its `source.pointer`), or, failing that, a status and a single
|
|
13
|
+
* message for the whole form. {@link CommandFailure} is that view, and
|
|
14
|
+
* {@link jsonapiFailure} is the default classifier.
|
|
15
|
+
*
|
|
16
|
+
* The error-object shape is `@thomasfosterau/effect-jsonapi`'s
|
|
17
|
+
* `Document.ErrorObject` — an optional peer imported for its type only, so
|
|
18
|
+
* nothing here depends on it at runtime and no application type leaks in.
|
|
19
|
+
* Causes are recognised structurally by their `_tag`, so an application's
|
|
20
|
+
* own `JsonapiError` with the same shape is recognised too.
|
|
21
|
+
*
|
|
22
|
+
* @copyright Thomas Foster
|
|
23
|
+
* @module effect-forms-svelte/kit/CommandFailure
|
|
24
|
+
*/
|
|
25
|
+
var CommandFailure_exports = /* @__PURE__ */ __exportAll({
|
|
26
|
+
DEFAULT_MESSAGE: () => DEFAULT_MESSAGE,
|
|
27
|
+
detailOf: () => detailOf,
|
|
28
|
+
jsonapiFailure: () => jsonapiFailure,
|
|
29
|
+
message: () => message,
|
|
30
|
+
routed: () => routed
|
|
31
|
+
});
|
|
32
|
+
/** Construct a {@link RoutedFailure}. */
|
|
33
|
+
const routed = (status, errors) => ({
|
|
34
|
+
_tag: "Routed",
|
|
35
|
+
status,
|
|
36
|
+
errors
|
|
37
|
+
});
|
|
38
|
+
/** Construct a {@link MessageFailure}. */
|
|
39
|
+
const message = (status, message) => ({
|
|
40
|
+
_tag: "Message",
|
|
41
|
+
status,
|
|
42
|
+
message
|
|
43
|
+
});
|
|
44
|
+
/** The fallback message for an error object with neither `detail` nor `title`. */
|
|
45
|
+
const DEFAULT_MESSAGE = "Request could not be completed";
|
|
46
|
+
/** An error object's human message: its `detail`, else its `title`, else `fallback`. */
|
|
47
|
+
const detailOf = (error, fallback = DEFAULT_MESSAGE) => error?.detail ?? error?.title ?? fallback;
|
|
48
|
+
const isStatus = (u) => Predicate.isNumber(u) && Number.isInteger(u);
|
|
49
|
+
/** The cause's `status`, when it carries an integer one. */
|
|
50
|
+
const statusOf = (cause) => Predicate.hasProperty(cause, "status") && isStatus(cause.status) ? Option.some(cause.status) : Option.none();
|
|
51
|
+
const fromJsonapiError = (cause) => Option.flatMap(statusOf(cause), (status) => Predicate.hasProperty(cause, "errors") && Array.isArray(cause.errors) ? Option.some(routed(status, cause.errors)) : Option.none());
|
|
52
|
+
const fromErrorResponse = (cause) => Option.map(statusOf(cause), (status) => {
|
|
53
|
+
const errors = Predicate.hasProperty(cause, "document") && Option.isOption(cause.document) ? Option.flatMap(cause.document, (document) => Predicate.hasProperty(document, "errors") && Array.isArray(document.errors) && Arr.isReadonlyArrayNonEmpty(document.errors) ? Option.some(document.errors) : Option.none()) : Option.none();
|
|
54
|
+
return Option.match(errors, {
|
|
55
|
+
onSome: (errors) => routed(status, errors),
|
|
56
|
+
onNone: () => message(status, Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) ? cause.message : DEFAULT_MESSAGE)
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* The default classifier:
|
|
61
|
+
*
|
|
62
|
+
* - a `JsonapiError`-tagged cause (`{ status, errors }`) → its error objects,
|
|
63
|
+
* at its own status;
|
|
64
|
+
* - an `ErrorResponse`-tagged cause (effect-jsonapi's client-side decoded
|
|
65
|
+
* error) → its document's error objects, or its message when the response
|
|
66
|
+
* carried no document;
|
|
67
|
+
* - an `Error` carrying a numeric `status` (a transport's status error, e.g.
|
|
68
|
+
* a 413 oversize upload the transport did not map to a document) → its
|
|
69
|
+
* message, at its own status;
|
|
70
|
+
* - anything else → `None` (unexpected).
|
|
71
|
+
*/
|
|
72
|
+
const jsonapiFailure = (cause) => {
|
|
73
|
+
if (!Predicate.isObjectOrArray(cause)) return Option.none();
|
|
74
|
+
if (Predicate.isTagged(cause, "JsonapiError")) return fromJsonapiError(cause);
|
|
75
|
+
if (Predicate.isTagged(cause, "ErrorResponse")) return fromErrorResponse(cause);
|
|
76
|
+
return Predicate.isError(cause) ? Option.map(statusOf(cause), (status) => message(status, cause.message)) : Option.none();
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { CommandFailure_exports, DEFAULT_MESSAGE, detailOf, jsonapiFailure, message, routed };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=CommandFailure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandFailure.js","names":[],"sources":["../../src/kit/CommandFailure.ts"],"sourcesContent":["/**\n * Classifying what a form action's command failed with.\n *\n * A command (an API client call, an Effect program) fails with whatever its\n * transport raises. The form layer only needs to know two things about it:\n * which JSON:API error objects it carried (so each can be routed onto a\n * field by its `source.pointer`), or, failing that, a status and a single\n * message for the whole form. {@link CommandFailure} is that view, and\n * {@link jsonapiFailure} is the default classifier.\n *\n * The error-object shape is `@thomasfosterau/effect-jsonapi`'s\n * `Document.ErrorObject` — an optional peer imported for its type only, so\n * nothing here depends on it at runtime and no application type leaks in.\n * Causes are recognised structurally by their `_tag`, so an application's\n * own `JsonapiError` with the same shape is recognised too.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/kit/CommandFailure\n */\n\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Predicate from \"effect/Predicate\";\nimport type { Document } from \"@thomasfosterau/effect-jsonapi\";\n\n/** One JSON:API error object: `@thomasfosterau/effect-jsonapi`'s `Document.ErrorObject`. */\nexport type ErrorObject = typeof Document.ErrorObject.Type;\n\n/** A failure carrying JSON:API error objects, each routed to a field (or the root) by its pointer. */\nexport interface RoutedFailure {\n readonly _tag: \"Routed\";\n readonly status: number;\n readonly errors: ReadonlyArray<ErrorObject>;\n}\n\n/** A failure with no error objects to route: one whole-form message. */\nexport interface MessageFailure {\n readonly _tag: \"Message\";\n readonly status: number;\n readonly message: string;\n}\n\n/**\n * What a form action needs to know about a recognised command failure. An\n * unrecognised failure is not a `CommandFailure` at all: the classifier\n * answers `None` and the action treats it as unexpected.\n */\nexport type CommandFailure = RoutedFailure | MessageFailure;\n\n/** Construct a {@link RoutedFailure}. */\nexport const routed = (status: number, errors: ReadonlyArray<ErrorObject>): CommandFailure => ({\n _tag: \"Routed\",\n status,\n errors,\n});\n\n/** Construct a {@link MessageFailure}. */\nexport const message = (status: number, message: string): CommandFailure => ({\n _tag: \"Message\",\n status,\n message,\n});\n\n/** A classifier: `Some` for a failure the form can show, `None` for an unexpected one. */\nexport type Classify = (cause: unknown) => Option.Option<CommandFailure>;\n\n/** The fallback message for an error object with neither `detail` nor `title`. */\nexport const DEFAULT_MESSAGE = \"Request could not be completed\";\n\n/** An error object's human message: its `detail`, else its `title`, else `fallback`. */\nexport const detailOf = (error: ErrorObject | undefined, fallback = DEFAULT_MESSAGE): string =>\n error?.detail ?? error?.title ?? fallback;\n\nconst isStatus = (u: unknown): u is number => Predicate.isNumber(u) && Number.isInteger(u);\n\n/** A cause recognised by its `_tag` as effect-jsonapi's (or an application's) `JsonapiError`. */\ninterface JsonapiErrorLike {\n readonly _tag: \"JsonapiError\";\n}\n\n/** A cause recognised by its `_tag` as effect-jsonapi's client-side `ErrorResponse`. */\ninterface ErrorResponseLike {\n readonly _tag: \"ErrorResponse\";\n}\n\n/** The cause's `status`, when it carries an integer one. */\nconst statusOf = (cause: JsonapiErrorLike | ErrorResponseLike | Error): Option.Option<number> =>\n Predicate.hasProperty(cause, \"status\") && isStatus(cause.status)\n ? Option.some(cause.status)\n : Option.none();\n\n// `ServerErrors.JsonapiError` (effect-jsonapi) and the applications' own\n// `JsonapiError`: `{ _tag: \"JsonapiError\", status, errors }`.\nconst fromJsonapiError = (cause: JsonapiErrorLike): Option.Option<CommandFailure> =>\n Option.flatMap(statusOf(cause), (status) =>\n Predicate.hasProperty(cause, \"errors\") && Array.isArray(cause.errors)\n ? // SAFETY: a `JsonapiError`-tagged cause carries JSON:API error objects in\n // `errors` by that tag's contract (effect-jsonapi's `ServerErrors.JsonapiError`\n // and the applications' own); causes are recognised structurally, not decoded.\n Option.some(routed(status, cause.errors as ReadonlyArray<ErrorObject>))\n : Option.none(),\n );\n\n// `Client.ErrorResponse` (effect-jsonapi): `{ _tag: \"ErrorResponse\", status,\n// message, document: Option<ErrorDocument> }`. A decoded document routes its\n// error objects; a response with none is its message alone.\nconst fromErrorResponse = (cause: ErrorResponseLike): Option.Option<CommandFailure> =>\n Option.map(statusOf(cause), (status) => {\n const errors =\n Predicate.hasProperty(cause, \"document\") && Option.isOption(cause.document)\n ? Option.flatMap(cause.document, (document) =>\n Predicate.hasProperty(document, \"errors\") &&\n Array.isArray(document.errors) &&\n Arr.isReadonlyArrayNonEmpty(document.errors)\n ? // SAFETY: an `ErrorResponse`'s `document` is effect-jsonapi's decoded\n // `ErrorDocument`, whose `errors` are JSON:API error objects.\n Option.some(document.errors as ReadonlyArray<ErrorObject>)\n : Option.none(),\n )\n : Option.none<ReadonlyArray<ErrorObject>>();\n return Option.match(errors, {\n onSome: (errors) => routed(status, errors),\n onNone: () =>\n message(\n status,\n Predicate.hasProperty(cause, \"message\") && Predicate.isString(cause.message)\n ? cause.message\n : DEFAULT_MESSAGE,\n ),\n });\n });\n\n/**\n * The default classifier:\n *\n * - a `JsonapiError`-tagged cause (`{ status, errors }`) → its error objects,\n * at its own status;\n * - an `ErrorResponse`-tagged cause (effect-jsonapi's client-side decoded\n * error) → its document's error objects, or its message when the response\n * carried no document;\n * - an `Error` carrying a numeric `status` (a transport's status error, e.g.\n * a 413 oversize upload the transport did not map to a document) → its\n * message, at its own status;\n * - anything else → `None` (unexpected).\n */\nexport const jsonapiFailure: Classify = (cause) => {\n if (!Predicate.isObjectOrArray(cause)) return Option.none();\n if (Predicate.isTagged(cause, \"JsonapiError\")) return fromJsonapiError(cause);\n if (Predicate.isTagged(cause, \"ErrorResponse\")) return fromErrorResponse(cause);\n return Predicate.isError(cause)\n ? Option.map(statusOf(cause), (status) => message(status, cause.message))\n : Option.none();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,UAAU,QAAgB,YAAwD;CAC7F,MAAM;CACN;CACA;AACF;;AAGA,MAAa,WAAW,QAAgB,aAAqC;CAC3E,MAAM;CACN;CACA;AACF;;AAMA,MAAa,kBAAkB;;AAG/B,MAAa,YAAY,OAAgC,WAAW,oBAClE,OAAO,UAAU,OAAO,SAAS;AAEnC,MAAM,YAAY,MAA4B,UAAU,SAAS,CAAC,KAAK,OAAO,UAAU,CAAC;;AAazF,MAAM,YAAY,UAChB,UAAU,YAAY,OAAO,QAAQ,KAAK,SAAS,MAAM,MAAM,IAC3D,OAAO,KAAK,MAAM,MAAM,IACxB,OAAO,KAAK;AAIlB,MAAM,oBAAoB,UACxB,OAAO,QAAQ,SAAS,KAAK,IAAI,WAC/B,UAAU,YAAY,OAAO,QAAQ,KAAK,MAAM,QAAQ,MAAM,MAAM,IAIhE,OAAO,KAAK,OAAO,QAAQ,MAAM,MAAoC,CAAC,IACtE,OAAO,KAAK,CAClB;AAKF,MAAM,qBAAqB,UACzB,OAAO,IAAI,SAAS,KAAK,IAAI,WAAW;CACtC,MAAM,SACJ,UAAU,YAAY,OAAO,UAAU,KAAK,OAAO,SAAS,MAAM,QAAQ,IACtE,OAAO,QAAQ,MAAM,WAAW,aAC9B,UAAU,YAAY,UAAU,QAAQ,KACxC,MAAM,QAAQ,SAAS,MAAM,KAC7B,IAAI,wBAAwB,SAAS,MAAM,IAGvC,OAAO,KAAK,SAAS,MAAoC,IACzD,OAAO,KAAK,CAClB,IACA,OAAO,KAAiC;CAC9C,OAAO,OAAO,MAAM,QAAQ;EAC1B,SAAS,WAAW,OAAO,QAAQ,MAAM;EACzC,cACE,QACE,QACA,UAAU,YAAY,OAAO,SAAS,KAAK,UAAU,SAAS,MAAM,OAAO,IACvE,MAAM,UACN,eACN;CACJ,CAAC;AACH,CAAC;;;;;;;;;;;;;;AAeH,MAAa,kBAA4B,UAAU;CACjD,IAAI,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO,OAAO,KAAK;CAC1D,IAAI,UAAU,SAAS,OAAO,cAAc,GAAG,OAAO,iBAAiB,KAAK;CAC5E,IAAI,UAAU,SAAS,OAAO,eAAe,GAAG,OAAO,kBAAkB,KAAK;CAC9E,OAAO,UAAU,QAAQ,KAAK,IAC1B,OAAO,IAAI,SAAS,KAAK,IAAI,WAAW,QAAQ,QAAQ,MAAM,OAAO,CAAC,IACtE,OAAO,KAAK;AAClB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FormStore } from "../FormStore.svelte.js";
|
|
2
|
+
import { Attachment } from "svelte/attachments";
|
|
3
|
+
//#region src/kit/Enhance.d.ts
|
|
4
|
+
/** The spreadable `<form>` props: the POST method plus the enhance attachment under its symbol key. */
|
|
5
|
+
export interface EnhancedFormProps {
|
|
6
|
+
readonly method: "post";
|
|
7
|
+
readonly [attachmentKey: symbol]: Attachment<HTMLFormElement>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Spreadable `<form>` props wiring SvelteKit's native `enhance` to the store:
|
|
11
|
+
* `method="post"` plus a Svelte attachment that flips `submitting`, applies a
|
|
12
|
+
* validation failure back to the store, and otherwise defers to SvelteKit's
|
|
13
|
+
* default `update` (with `reset: false`) for success / redirect / error, which
|
|
14
|
+
* applies the result exactly once.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <script lang="ts">
|
|
19
|
+
* import { createForm } from "@thomasfosterau/effect-forms-svelte";
|
|
20
|
+
* import { enhanceForm } from "@thomasfosterau/effect-forms-svelte/kit";
|
|
21
|
+
* let { data } = $props();
|
|
22
|
+
* const form = createForm(data.form);
|
|
23
|
+
* </script>
|
|
24
|
+
*
|
|
25
|
+
* <form {...enhanceForm(form)}>…</form>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function enhanceForm(form: FormStore<any, any, any>): EnhancedFormProps;
|
|
29
|
+
//#endregion
|
|
30
|
+
//# sourceMappingURL=Enhance.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { applyActionResult } from "../SvelteKit.js";
|
|
2
|
+
import { createAttachmentKey } from "svelte/attachments";
|
|
3
|
+
import { enhance } from "$app/forms";
|
|
4
|
+
//#region src/kit/Enhance.ts
|
|
5
|
+
/**
|
|
6
|
+
* SvelteKit `$app/forms` glue for the form store. {@link enhanceForm}
|
|
7
|
+
* returns spreadable `<form>` props — `<form {...enhanceForm(form)}>` — that
|
|
8
|
+
* post to the page's derived action ({@link formAction}) through SvelteKit's
|
|
9
|
+
* native `enhance`.
|
|
10
|
+
*
|
|
11
|
+
* `$app/forms` is a SvelteKit virtual module: it resolves inside a SvelteKit
|
|
12
|
+
* build (which compiles this package, as it does every `svelte`-condition
|
|
13
|
+
* dependency), never from Node on its own. That is why this lives in the
|
|
14
|
+
* `/kit` entry and nowhere the framework-neutral bindings can reach.
|
|
15
|
+
*
|
|
16
|
+
* @copyright Thomas Foster
|
|
17
|
+
* @module effect-forms-svelte/kit/Enhance
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Spreadable `<form>` props wiring SvelteKit's native `enhance` to the store:
|
|
21
|
+
* `method="post"` plus a Svelte attachment that flips `submitting`, applies a
|
|
22
|
+
* validation failure back to the store, and otherwise defers to SvelteKit's
|
|
23
|
+
* default `update` (with `reset: false`) for success / redirect / error, which
|
|
24
|
+
* applies the result exactly once.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```svelte
|
|
28
|
+
* <script lang="ts">
|
|
29
|
+
* import { createForm } from "@thomasfosterau/effect-forms-svelte";
|
|
30
|
+
* import { enhanceForm } from "@thomasfosterau/effect-forms-svelte/kit";
|
|
31
|
+
* let { data } = $props();
|
|
32
|
+
* const form = createForm(data.form);
|
|
33
|
+
* <\/script>
|
|
34
|
+
*
|
|
35
|
+
* <form {...enhanceForm(form)}>…</form>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function enhanceForm(form) {
|
|
39
|
+
return {
|
|
40
|
+
method: "post",
|
|
41
|
+
[createAttachmentKey()]: (node) => {
|
|
42
|
+
const enhanced = enhance(node, () => {
|
|
43
|
+
form.submitting = true;
|
|
44
|
+
return async ({ result, update }) => {
|
|
45
|
+
form.submitting = false;
|
|
46
|
+
if (applyActionResult(form, result)) return;
|
|
47
|
+
await update({ reset: false });
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return () => enhanced.destroy();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { enhanceForm };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=Enhance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Enhance.js","names":[],"sources":["../../src/kit/Enhance.ts"],"sourcesContent":["/**\n * SvelteKit `$app/forms` glue for the form store. {@link enhanceForm}\n * returns spreadable `<form>` props — `<form {...enhanceForm(form)}>` — that\n * post to the page's derived action ({@link formAction}) through SvelteKit's\n * native `enhance`.\n *\n * `$app/forms` is a SvelteKit virtual module: it resolves inside a SvelteKit\n * build (which compiles this package, as it does every `svelte`-condition\n * dependency), never from Node on its own. That is why this lives in the\n * `/kit` entry and nowhere the framework-neutral bindings can reach.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/kit/Enhance\n */\n\n/// <reference types=\"@sveltejs/kit\" />\n\nimport { enhance } from \"$app/forms\";\nimport { type Attachment, createAttachmentKey } from \"svelte/attachments\";\nimport type { FormStore } from \"../FormStore.svelte.ts\";\nimport { applyActionResult } from \"../SvelteKit.ts\";\n\n/** The spreadable `<form>` props: the POST method plus the enhance attachment under its symbol key. */\nexport interface EnhancedFormProps {\n readonly method: \"post\";\n readonly [attachmentKey: symbol]: Attachment<HTMLFormElement>;\n}\n\n/**\n * Spreadable `<form>` props wiring SvelteKit's native `enhance` to the store:\n * `method=\"post\"` plus a Svelte attachment that flips `submitting`, applies a\n * validation failure back to the store, and otherwise defers to SvelteKit's\n * default `update` (with `reset: false`) for success / redirect / error, which\n * applies the result exactly once.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { createForm } from \"@thomasfosterau/effect-forms-svelte\";\n * import { enhanceForm } from \"@thomasfosterau/effect-forms-svelte/kit\";\n * let { data } = $props();\n * const form = createForm(data.form);\n * </script>\n *\n * <form {...enhanceForm(form)}>…</form>\n * ```\n */\nexport function enhanceForm(\n // oxlint-disable-next-line typescript/no-explicit-any -- accepts any concrete store\n form: FormStore<any, any, any>,\n): EnhancedFormProps {\n return {\n method: \"post\",\n [createAttachmentKey()]: (node: HTMLFormElement) => {\n const enhanced = enhance(node, () => {\n form.submitting = true;\n return async ({ result, update }) => {\n form.submitting = false;\n if (applyActionResult(form, result)) return;\n await update({ reset: false });\n };\n });\n return () => enhanced.destroy();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAgB,YAEd,MACmB;CACnB,OAAO;EACL,QAAQ;GACP,oBAAoB,KAAK,SAA0B;GAClD,MAAM,WAAW,QAAQ,YAAY;IACnC,KAAK,aAAa;IAClB,OAAO,OAAO,EAAE,QAAQ,aAAa;KACnC,KAAK,aAAa;KAClB,IAAI,kBAAkB,MAAM,MAAM,GAAG;KACrC,MAAM,OAAO,EAAE,OAAO,MAAM,CAAC;IAC/B;GACF,CAAC;GACD,aAAa,SAAS,QAAQ;EAChC;CACF;AACF"}
|