@thomasfosterau/effect-forms-svelte 0.1.0 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +135 -21
  2. package/dist/ClientValidate.d.ts +2 -3
  3. package/dist/ClientValidate.js +1 -2
  4. package/dist/ClientValidate.js.map +1 -1
  5. package/dist/EnhanceCore.d.ts +7 -8
  6. package/dist/EnhanceCore.js +2 -3
  7. package/dist/EnhanceCore.js.map +1 -1
  8. package/dist/EntityForm.d.ts +17 -18
  9. package/dist/EntityForm.js.map +1 -1
  10. package/dist/FormStore.svelte.d.ts +15 -16
  11. package/dist/FormStore.svelte.js +2 -3
  12. package/dist/FormStore.svelte.js.map +1 -1
  13. package/dist/Listeners.d.ts +5 -6
  14. package/dist/Listeners.js.map +1 -1
  15. package/dist/SvelteKit.d.ts +5 -4
  16. package/dist/SvelteKit.js +3 -1
  17. package/dist/SvelteKit.js.map +1 -1
  18. package/dist/Tainted.d.ts +6 -7
  19. package/dist/Tainted.js.map +1 -1
  20. package/dist/_virtual/_rolldown/runtime.js +13 -0
  21. package/dist/kit/ActionRedirect.d.ts +22 -0
  22. package/dist/kit/ActionRedirect.js +56 -0
  23. package/dist/kit/ActionRedirect.js.map +1 -0
  24. package/dist/kit/CommandFailure.d.ts +52 -0
  25. package/dist/kit/CommandFailure.js +82 -0
  26. package/dist/kit/CommandFailure.js.map +1 -0
  27. package/dist/kit/Enhance.d.ts +30 -0
  28. package/dist/kit/Enhance.js +57 -0
  29. package/dist/kit/Enhance.js.map +1 -0
  30. package/dist/kit/FormAction.d.ts +132 -0
  31. package/dist/kit/FormAction.js +164 -0
  32. package/dist/kit/FormAction.js.map +1 -0
  33. package/dist/kit/FormStateReaders.d.ts +58 -0
  34. package/dist/kit/FormStateReaders.js +63 -0
  35. package/dist/kit/FormStateReaders.js.map +1 -0
  36. package/dist/kit/Pointer.d.ts +23 -0
  37. package/dist/kit/Pointer.js +38 -0
  38. package/dist/kit/Pointer.js.map +1 -0
  39. package/dist/kit/index.d.ts +7 -0
  40. package/dist/kit/index.js +7 -0
  41. package/package.json +22 -3
package/dist/SvelteKit.js CHANGED
@@ -4,7 +4,9 @@ const isFormActionData = (data) => typeof data === "object" && data !== null &&
4
4
  * Apply a derived form action's result to `form`. On a validation `failure`
5
5
  * carrying `{ form }`, repopulate the store's errors + posted values and return
6
6
  * `true` (consumed). Otherwise return `false` — the caller should defer to the
7
- * framework (`applyAction`) for success / redirect / error.
7
+ * framework for success / redirect / error: inside SvelteKit's `enhance`
8
+ * callback that is `update`, which applies the result itself, so don't also
9
+ * call `applyAction`.
8
10
  */
9
11
  function applyActionResult(form, result) {
10
12
  if (result.type !== "failure" || !isFormActionData(result.data)) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"SvelteKit.js","names":[],"sources":["../src/SvelteKit.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * SvelteKit interop for the form store. The framework's `$app/forms` `enhance`\n * / `applyAction` are app-build virtuals (not resolvable from a library), so\n * the SvelteKit-specific glue stays in the consuming app; this module owns the\n * reusable half — applying a derived form action's result back to the store.\n *\n * @module effect-forms-svelte/SvelteKit\n */\n\nimport type { Errors, Result, State } from \"@thomasfosterau/effect-forms\";\nimport type { FormStore } from \"./FormStore.svelte.ts\";\n\ntype FormActionData<A = unknown, Message = unknown> = Result.FormActionData<A, Message>;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\ntype FormErrors = Errors.FormErrors;\n\n/**\n * The minimal view of SvelteKit's `ActionResult` that {@link applyActionResult}\n * inspects — a failure carries the derived action's {@link FormActionData}.\n */\nexport interface ServerActionResult {\n readonly type: \"success\" | \"failure\" | \"redirect\" | \"error\";\n readonly data?: FormActionData | Record<string, unknown> | undefined;\n}\n\nconst isFormActionData = (data: unknown): data is FormActionData & { form: FormState<unknown> } =>\n typeof data === \"object\" &&\n data !== null &&\n \"form\" in data &&\n Boolean((data as { form?: unknown }).form);\n\n/**\n * Apply a derived form action's result to `form`. On a validation `failure`\n * carrying `{ form }`, repopulate the store's errors + posted values and return\n * `true` (consumed). Otherwise return `false` — the caller should defer to the\n * framework (`applyAction`) for success / redirect / error.\n */\nexport function applyActionResult(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any concrete store\n form: FormStore<any, any, any>,\n result: ServerActionResult,\n): boolean {\n if (result.type !== \"failure\" || !isFormActionData(result.data)) return false;\n const state = result.data.form;\n form.setServerState(state.data, state.errors as FormErrors);\n return true;\n}\n"],"mappings":";AA2BA,MAAM,oBAAoB,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,QAAS,KAA4B,IAAI;;;;;;;AAQ3C,SAAgB,kBAEd,MACA,QACS;CACT,IAAI,OAAO,SAAS,aAAa,CAAC,iBAAiB,OAAO,IAAI,GAAG,OAAO;CACxE,MAAM,QAAQ,OAAO,KAAK;CAC1B,KAAK,eAAe,MAAM,MAAM,MAAM,MAAoB;CAC1D,OAAO;AACT"}
1
+ {"version":3,"file":"SvelteKit.js","names":[],"sources":["../src/SvelteKit.ts"],"sourcesContent":["/**\n * SvelteKit interop for the form store that needs no SvelteKit import:\n * applying a derived form action's result back to the store. The glue that\n * imports `@sveltejs/kit` or the `$app/forms` virtual lives in the `/kit`\n * entry.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/SvelteKit\n */\n\nimport type { Errors, Result, State } from \"@thomasfosterau/effect-forms\";\nimport type { FormStore } from \"./FormStore.svelte.ts\";\n\ntype FormActionData<A = unknown, Message = unknown> = Result.FormActionData<A, Message>;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\ntype FormErrors = Errors.FormErrors;\n\n/**\n * The minimal view of SvelteKit's `ActionResult` that {@link applyActionResult}\n * inspects — a failure carries the derived action's {@link FormActionData}.\n */\nexport interface ServerActionResult {\n readonly type: \"success\" | \"failure\" | \"redirect\" | \"error\";\n readonly data?: FormActionData | Record<string, unknown> | undefined;\n}\n\nconst isFormActionData = (data: unknown): data is FormActionData & { form: FormState<unknown> } =>\n typeof data === \"object\" &&\n data !== null &&\n \"form\" in data &&\n Boolean((data as { form?: unknown }).form);\n\n/**\n * Apply a derived form action's result to `form`. On a validation `failure`\n * carrying `{ form }`, repopulate the store's errors + posted values and return\n * `true` (consumed). Otherwise return `false` — the caller should defer to the\n * framework for success / redirect / error: inside SvelteKit's `enhance`\n * callback that is `update`, which applies the result itself, so don't also\n * call `applyAction`.\n */\nexport function applyActionResult(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any concrete store\n form: FormStore<any, any, any>,\n result: ServerActionResult,\n): boolean {\n if (result.type !== \"failure\" || !isFormActionData(result.data)) return false;\n const state = result.data.form;\n form.setServerState(state.data, state.errors as FormErrors);\n return true;\n}\n"],"mappings":";AA0BA,MAAM,oBAAoB,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,QAAS,KAA4B,IAAI;;;;;;;;;AAU3C,SAAgB,kBAEd,MACA,QACS;CACT,IAAI,OAAO,SAAS,aAAa,CAAC,iBAAiB,OAAO,IAAI,GAAG,OAAO;CACxE,MAAM,QAAQ,OAAO,KAAK;CAC1B,KAAK,eAAe,MAAM,MAAM,MAAM,MAAoB;CAC1D,OAAO;AACT"}
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,6 @@ 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 { TaintedFields, computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths };
32
31
  //# sourceMappingURL=Tainted.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Tainted.js","names":[],"sources":["../src/Tainted.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\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 * @module effect-forms-svelte/Tainted\n */\n\nimport type { Issues } from \"@thomasfosterau/effect-forms\";\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\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\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 const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (!deepEqual(a[key], b[key])) return false;\n }\n return true;\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 const out: Record<string, boolean | TaintedFields> = {};\n const keys = new Set([...Object.keys(original), ...Object.keys(current)]);\n for (const key of keys) {\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 if (Object.keys(nested).length > 0) out[key] = nested;\n } else if (!deepEqual(before, after)) {\n out[key] = true;\n }\n }\n return out;\n}\n\n/** Whether anything in the tree is tainted. */\nexport function isAnyTainted(tainted: TaintedFields): boolean {\n for (const key of Object.keys(tainted)) {\n const value = tainted[key];\n if (value === true) return true;\n if (isObject(value) && isAnyTainted(value)) return true;\n }\n return false;\n}\n\n/** Whether the field at `path` (or anything beneath it) is tainted. */\nexport function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean {\n let node: boolean | TaintedFields | undefined = tainted;\n for (const segment of path) {\n if (node === undefined || typeof node === \"boolean\") return node === true;\n node = node[String(segment)];\n }\n if (node === undefined) return false;\n if (typeof node === \"boolean\") return node;\n return isAnyTainted(node);\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 out: FormPath[] = [];\n\n const walk = (node: TaintedFields, path: Array<string | number>): void => {\n for (const key of Object.keys(node)) {\n const value = node[key];\n if (value === undefined) continue;\n const segment: string | number = /^\\d+$/.test(key) ? Number(key) : key;\n if (value === true) out.push([...path, segment]);\n else if (typeof value === \"object\") walk(value, [...path, segment]);\n }\n };\n\n walk(tainted, []);\n return out;\n}\n"],"mappings":";AAwBA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU;;AAGzC,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,GAAG;EAC9B,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC;EAC3D,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO;EAEzC,OAAO;CACT;CACA,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,MAAM,MAA+C,CAAC;CACtD,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,QAAQ,GAAG,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC;CACxE,KAAK,MAAM,OAAO,MAAM;EACtB,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,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,IAAI,OAAO;EACjD,OAAO,IAAI,CAAC,UAAU,QAAQ,KAAK,GACjC,IAAI,OAAO;CAEf;CACA,OAAO;AACT;;AAGA,SAAgB,aAAa,SAAiC;CAC5D,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG;EACtC,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,SAAS,KAAK,KAAK,aAAa,KAAK,GAAG,OAAO;CACrD;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,SAAwB,MAAyB;CAC3E,IAAI,OAA4C;CAChD,KAAK,MAAM,WAAW,MAAM;EAC1B,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,WAAW,OAAO,SAAS;EACrE,OAAO,KAAK,OAAO,OAAO;CAC5B;CACA,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,IAAI,OAAO,SAAS,WAAW,OAAO;CACtC,OAAO,aAAa,IAAI;AAC1B;;;;;;;;AASA,SAAgB,iBAAiB,SAAoC;CACnE,MAAM,MAAkB,CAAC;CAEzB,MAAM,QAAQ,MAAqB,SAAuC;EACxE,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG;GACnC,MAAM,QAAQ,KAAK;GACnB,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,UAA2B,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;GACnE,IAAI,UAAU,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC;QAC1C,IAAI,OAAO,UAAU,UAAU,KAAK,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EACpE;CACF;CAEA,KAAK,SAAS,CAAC,CAAC;CAChB,OAAO;AACT"}
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\";\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\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\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 const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (!deepEqual(a[key], b[key])) return false;\n }\n return true;\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 const out: Record<string, boolean | TaintedFields> = {};\n const keys = new Set([...Object.keys(original), ...Object.keys(current)]);\n for (const key of keys) {\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 if (Object.keys(nested).length > 0) out[key] = nested;\n } else if (!deepEqual(before, after)) {\n out[key] = true;\n }\n }\n return out;\n}\n\n/** Whether anything in the tree is tainted. */\nexport function isAnyTainted(tainted: TaintedFields): boolean {\n for (const key of Object.keys(tainted)) {\n const value = tainted[key];\n if (value === true) return true;\n if (isObject(value) && isAnyTainted(value)) return true;\n }\n return false;\n}\n\n/** Whether the field at `path` (or anything beneath it) is tainted. */\nexport function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean {\n let node: boolean | TaintedFields | undefined = tainted;\n for (const segment of path) {\n if (node === undefined || typeof node === \"boolean\") return node === true;\n node = node[String(segment)];\n }\n if (node === undefined) return false;\n if (typeof node === \"boolean\") return node;\n return isAnyTainted(node);\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 out: FormPath[] = [];\n\n const walk = (node: TaintedFields, path: Array<string | number>): void => {\n for (const key of Object.keys(node)) {\n const value = node[key];\n if (value === undefined) continue;\n const segment: string | number = /^\\d+$/.test(key) ? Number(key) : key;\n if (value === true) out.push([...path, segment]);\n else if (typeof value === \"object\") walk(value, [...path, segment]);\n }\n };\n\n walk(tainted, []);\n return out;\n}\n"],"mappings":";AAuBA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU;;AAGzC,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,GAAG;EAC9B,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC;EAC3D,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO;EAEzC,OAAO;CACT;CACA,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,MAAM,MAA+C,CAAC;CACtD,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,QAAQ,GAAG,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC;CACxE,KAAK,MAAM,OAAO,MAAM;EACtB,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,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,IAAI,OAAO;EACjD,OAAO,IAAI,CAAC,UAAU,QAAQ,KAAK,GACjC,IAAI,OAAO;CAEf;CACA,OAAO;AACT;;AAGA,SAAgB,aAAa,SAAiC;CAC5D,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG;EACtC,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,SAAS,KAAK,KAAK,aAAa,KAAK,GAAG,OAAO;CACrD;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,SAAwB,MAAyB;CAC3E,IAAI,OAA4C;CAChD,KAAK,MAAM,WAAW,MAAM;EAC1B,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,WAAW,OAAO,SAAS;EACrE,OAAO,KAAK,OAAO,OAAO;CAC5B;CACA,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,IAAI,OAAO,SAAS,WAAW,OAAO;CACtC,OAAO,aAAa,IAAI;AAC1B;;;;;;;;AASA,SAAgB,iBAAiB,SAAoC;CACnE,MAAM,MAAkB,CAAC;CAEzB,MAAM,QAAQ,MAAqB,SAAuC;EACxE,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG;GACnC,MAAM,QAAQ,KAAK;GACnB,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,UAA2B,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;GACnE,IAAI,UAAU,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC;QAC1C,IAAI,OAAO,UAAU,UAAU,KAAK,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EACpE;CACF;CAEA,KAAK,SAAS,CAAC,CAAC;CAChB,OAAO;AACT"}
@@ -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,56 @@
1
+ import { detailOf, jsonapiFailure } from "./CommandFailure.js";
2
+ import * as Option from "effect/Option";
3
+ import * as Predicate from "effect/Predicate";
4
+ //#region src/kit/ActionRedirect.ts
5
+ /**
6
+ * Failure routing for **action-only** routes: a route with no page of its
7
+ * own (a restore, a confirm) always answers a submission with a redirect, so
8
+ * a *user-fixable* command failure rides back to the submitting page as
9
+ * query parameters instead of a `fail()` (which would try to render the
10
+ * action route's nonexistent page for a no-JS submitter). Everything else —
11
+ * auth denials, infrastructure faults — stays a hard error at the caller.
12
+ *
13
+ * @copyright Thomas Foster
14
+ * @module effect-forms-svelte/kit/ActionRedirect
15
+ */
16
+ /** The statuses whose failure the submitter can act on (the form layer's set). */
17
+ const USER_FIXABLE_STATUSES = /* @__PURE__ */ new Set([
18
+ 400,
19
+ 409,
20
+ 422
21
+ ]);
22
+ /**
23
+ * A user-fixable failure's message; `None` takes the hard-error path.
24
+ *
25
+ * - a `JsonapiError` / `ErrorResponse`-tagged cause at 400 / 409 / 422 → its
26
+ * first error object's `detail`, else `title`, else `fallback` (an
27
+ * `ErrorResponse` with no document → its own message);
28
+ * - an `HttpError`-tagged 400 → its message (or `fallback`);
29
+ * - anything else → `None`.
30
+ *
31
+ * `Some("")` is a deliberate *present* answer: an error object carrying a
32
+ * blank `detail` redirects with an empty parameter rather than escalating.
33
+ */
34
+ const userFixableMessage = (cause, fallback) => {
35
+ if (!Predicate.isObjectOrArray(cause)) return Option.none();
36
+ const tagged = cause;
37
+ if (tagged._tag === "HttpError") {
38
+ if (tagged.status !== 400) return Option.none();
39
+ return Option.some(Predicate.isString(tagged.message) ? tagged.message : fallback);
40
+ }
41
+ if (tagged._tag !== "JsonapiError" && tagged._tag !== "ErrorResponse") return Option.none();
42
+ return jsonapiFailure(cause).pipe(Option.filter((failure) => USER_FIXABLE_STATUSES.has(failure.status)), Option.map((failure) => failure._tag === "Routed" ? detailOf(failure.errors[0], fallback) : failure.message));
43
+ };
44
+ /**
45
+ * Append the failure's query parameters to a same-origin return target,
46
+ * answering the path and query only (never an origin).
47
+ */
48
+ const withFailureParams = (target, params) => {
49
+ const url = new URL(target, "http://localhost");
50
+ for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
51
+ return url.pathname + url.search;
52
+ };
53
+ //#endregion
54
+ export { userFixableMessage, withFailureParams };
55
+
56
+ //# 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 Option from \"effect/Option\";\nimport * as Predicate from \"effect/Predicate\";\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: ReadonlySet<number> = new Set([400, 409, 422]);\n\ninterface TaggedLike {\n readonly _tag?: unknown;\n readonly status?: unknown;\n readonly message?: unknown;\n}\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 const tagged = cause as TaggedLike;\n if (tagged._tag === \"HttpError\") {\n if (tagged.status !== 400) return Option.none();\n return Option.some(Predicate.isString(tagged.message) ? tagged.message : fallback);\n }\n if (tagged._tag !== \"JsonapiError\" && tagged._tag !== \"ErrorResponse\") return Option.none();\n return jsonapiFailure(cause).pipe(\n Option.filter((failure) => USER_FIXABLE_STATUSES.has(failure.status)),\n Option.map((failure) =>\n failure._tag === \"Routed\" ? detailOf(failure.errors[0], fallback) : 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 for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);\n return url.pathname + url.search;\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,wCAA6C,IAAI,IAAI;CAAC;CAAK;CAAK;AAAG,CAAC;;;;;;;;;;;;;AAoB1E,MAAa,sBAAsB,OAAgB,aAA4C;CAC7F,IAAI,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO,OAAO,KAAK;CAC1D,MAAM,SAAS;CACf,IAAI,OAAO,SAAS,aAAa;EAC/B,IAAI,OAAO,WAAW,KAAK,OAAO,OAAO,KAAK;EAC9C,OAAO,OAAO,KAAK,UAAU,SAAS,OAAO,OAAO,IAAI,OAAO,UAAU,QAAQ;CACnF;CACA,IAAI,OAAO,SAAS,kBAAkB,OAAO,SAAS,iBAAiB,OAAO,OAAO,KAAK;CAC1F,OAAO,eAAe,KAAK,CAAC,CAAC,KAC3B,OAAO,QAAQ,YAAY,sBAAsB,IAAI,QAAQ,MAAM,CAAC,GACpE,OAAO,KAAK,YACV,QAAQ,SAAS,WAAW,SAAS,QAAQ,OAAO,IAAI,QAAQ,IAAI,QAAQ,OAC9E,CACF;AACF;;;;;AAMA,MAAa,qBACX,QACA,WACW;CACX,MAAM,MAAM,IAAI,IAAI,QAAQ,kBAAkB;CAC9C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,IAAI,aAAa,IAAI,KAAK,KAAK;CAClF,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,82 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import * as Option from "effect/Option";
3
+ import * as Predicate from "effect/Predicate";
4
+ //#region src/kit/CommandFailure.ts
5
+ /**
6
+ * Classifying what a form action's command failed with.
7
+ *
8
+ * A command (an API client call, an Effect program) fails with whatever its
9
+ * transport raises. The form layer only needs to know two things about it:
10
+ * which JSON:API error objects it carried (so each can be routed onto a
11
+ * field by its `source.pointer`), or, failing that, a status and a single
12
+ * message for the whole form. {@link CommandFailure} is that view, and
13
+ * {@link jsonapiFailure} is the default classifier.
14
+ *
15
+ * The error-object shape is `@thomasfosterau/effect-jsonapi`'s
16
+ * `Document.ErrorObject` — an optional peer imported for its type only, so
17
+ * nothing here depends on it at runtime and no application type leaks in.
18
+ * Causes are recognised structurally by their `_tag`, so an application's
19
+ * own `JsonapiError` with the same shape is recognised too.
20
+ *
21
+ * @copyright Thomas Foster
22
+ * @module effect-forms-svelte/kit/CommandFailure
23
+ */
24
+ var CommandFailure_exports = /* @__PURE__ */ __exportAll({
25
+ DEFAULT_MESSAGE: () => DEFAULT_MESSAGE,
26
+ detailOf: () => detailOf,
27
+ jsonapiFailure: () => jsonapiFailure,
28
+ message: () => message,
29
+ routed: () => routed
30
+ });
31
+ /** Construct a {@link RoutedFailure}. */
32
+ const routed = (status, errors) => ({
33
+ _tag: "Routed",
34
+ status,
35
+ errors
36
+ });
37
+ /** Construct a {@link MessageFailure}. */
38
+ const message = (status, message) => ({
39
+ _tag: "Message",
40
+ status,
41
+ message
42
+ });
43
+ /** The fallback message for an error object with neither `detail` nor `title`. */
44
+ const DEFAULT_MESSAGE = "Request could not be completed";
45
+ /** An error object's human message: its `detail`, else its `title`, else `fallback`. */
46
+ const detailOf = (error, fallback = DEFAULT_MESSAGE) => error?.detail ?? error?.title ?? fallback;
47
+ const isStatus = (u) => Predicate.isNumber(u) && Number.isInteger(u);
48
+ const fromJsonapiError = (cause) => isStatus(cause.status) && Array.isArray(cause.errors) ? Option.some(routed(cause.status, cause.errors)) : Option.none();
49
+ const fromErrorResponse = (cause) => {
50
+ if (!isStatus(cause.status)) return Option.none();
51
+ const errors = Option.isOption(cause.document) ? Option.flatMap(cause.document, (document) => Predicate.hasProperty(document, "errors") && Array.isArray(document.errors) && document.errors.length > 0 ? Option.some(document.errors) : Option.none()) : Option.none();
52
+ const status = cause.status;
53
+ return Option.some(Option.match(errors, {
54
+ onSome: (errors) => routed(status, errors),
55
+ onNone: () => message(status, Predicate.isString(cause.message) ? cause.message : DEFAULT_MESSAGE)
56
+ }));
57
+ };
58
+ /**
59
+ * The default classifier:
60
+ *
61
+ * - a `JsonapiError`-tagged cause (`{ status, errors }`) → its error objects,
62
+ * at its own status;
63
+ * - an `ErrorResponse`-tagged cause (effect-jsonapi's client-side decoded
64
+ * error) → its document's error objects, or its message when the response
65
+ * carried no document;
66
+ * - an `Error` carrying a numeric `status` (a transport's status error, e.g.
67
+ * a 413 oversize upload the transport did not map to a document) → its
68
+ * message, at its own status;
69
+ * - anything else → `None` (unexpected).
70
+ */
71
+ const jsonapiFailure = (cause) => {
72
+ if (!Predicate.isObjectOrArray(cause)) return Option.none();
73
+ const tagged = cause;
74
+ if (tagged._tag === "JsonapiError") return fromJsonapiError(tagged);
75
+ if (tagged._tag === "ErrorResponse") return fromErrorResponse(tagged);
76
+ if (Predicate.isError(cause) && isStatus(tagged.status)) return Option.some(message(tagged.status, cause.message));
77
+ return Option.none();
78
+ };
79
+ //#endregion
80
+ export { CommandFailure_exports, DEFAULT_MESSAGE, detailOf, jsonapiFailure, message, routed };
81
+
82
+ //# 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 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\ninterface TaggedLike {\n readonly _tag?: unknown;\n readonly status?: unknown;\n readonly message?: unknown;\n readonly errors?: unknown;\n readonly document?: unknown;\n}\n\nconst isStatus = (u: unknown): u is number => Predicate.isNumber(u) && Number.isInteger(u);\n\n// `ServerErrors.JsonapiError` (effect-jsonapi) and the applications' own\n// `JsonapiError`: `{ _tag: \"JsonapiError\", status, errors }`.\nconst fromJsonapiError = (cause: TaggedLike): Option.Option<CommandFailure> =>\n isStatus(cause.status) && Array.isArray(cause.errors)\n ? Option.some(routed(cause.status, cause.errors as ReadonlyArray<ErrorObject>))\n : Option.none();\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: TaggedLike): Option.Option<CommandFailure> => {\n if (!isStatus(cause.status)) return Option.none();\n const errors = Option.isOption(cause.document)\n ? Option.flatMap(cause.document as Option.Option<unknown>, (document) =>\n Predicate.hasProperty(document, \"errors\") &&\n Array.isArray(document.errors) &&\n document.errors.length > 0\n ? Option.some(document.errors as ReadonlyArray<ErrorObject>)\n : Option.none(),\n )\n : Option.none<ReadonlyArray<ErrorObject>>();\n const status = cause.status;\n return Option.some(\n Option.match(errors, {\n onSome: (errors) => routed(status, errors),\n onNone: () =>\n message(status, Predicate.isString(cause.message) ? cause.message : 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 const tagged = cause as TaggedLike;\n if (tagged._tag === \"JsonapiError\") return fromJsonapiError(tagged);\n if (tagged._tag === \"ErrorResponse\") return fromErrorResponse(tagged);\n if (Predicate.isError(cause) && isStatus(tagged.status)) {\n return Option.some(message(tagged.status, cause.message));\n }\n return Option.none();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,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;AAUnC,MAAM,YAAY,MAA4B,UAAU,SAAS,CAAC,KAAK,OAAO,UAAU,CAAC;AAIzF,MAAM,oBAAoB,UACxB,SAAS,MAAM,MAAM,KAAK,MAAM,QAAQ,MAAM,MAAM,IAChD,OAAO,KAAK,OAAO,MAAM,QAAQ,MAAM,MAAoC,CAAC,IAC5E,OAAO,KAAK;AAKlB,MAAM,qBAAqB,UAAqD;CAC9E,IAAI,CAAC,SAAS,MAAM,MAAM,GAAG,OAAO,OAAO,KAAK;CAChD,MAAM,SAAS,OAAO,SAAS,MAAM,QAAQ,IACzC,OAAO,QAAQ,MAAM,WAAqC,aACxD,UAAU,YAAY,UAAU,QAAQ,KACxC,MAAM,QAAQ,SAAS,MAAM,KAC7B,SAAS,OAAO,SAAS,IACrB,OAAO,KAAK,SAAS,MAAoC,IACzD,OAAO,KAAK,CAClB,IACA,OAAO,KAAiC;CAC5C,MAAM,SAAS,MAAM;CACrB,OAAO,OAAO,KACZ,OAAO,MAAM,QAAQ;EACnB,SAAS,WAAW,OAAO,QAAQ,MAAM;EACzC,cACE,QAAQ,QAAQ,UAAU,SAAS,MAAM,OAAO,IAAI,MAAM,UAAU,eAAe;CACvF,CAAC,CACH;AACF;;;;;;;;;;;;;;AAeA,MAAa,kBAA4B,UAAU;CACjD,IAAI,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO,OAAO,KAAK;CAC1D,MAAM,SAAS;CACf,IAAI,OAAO,SAAS,gBAAgB,OAAO,iBAAiB,MAAM;CAClE,IAAI,OAAO,SAAS,iBAAiB,OAAO,kBAAkB,MAAM;CACpE,IAAI,UAAU,QAAQ,KAAK,KAAK,SAAS,OAAO,MAAM,GACpD,OAAO,OAAO,KAAK,QAAQ,OAAO,QAAQ,MAAM,OAAO,CAAC;CAE1D,OAAO,OAAO,KAAK;AACrB"}
@@ -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"}
@@ -0,0 +1,132 @@
1
+ import { Classify } from "./CommandFailure.js";
2
+ import { Form, Result, State } from "@thomasfosterau/effect-forms";
3
+ import * as Schema from "effect/Schema";
4
+ import * as Effect from "effect/Effect";
5
+ import { RequestEvent } from "@sveltejs/kit";
6
+ //#region src/kit/FormAction.d.ts
7
+ type FormState<A, Message = unknown> = State.FormState<A, Message>;
8
+ type FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;
9
+ type FormActionData<A = unknown, Message = unknown> = Result.FormActionData<A, Message>;
10
+ /**
11
+ * A command: a Promise-returning function or one returning an `Effect` with
12
+ * no requirements (provide them first, e.g. through a `ManagedRuntime`).
13
+ */
14
+ type Command$1<I, O> = (input: I) => Promise<O> | Effect.Effect<O, unknown>;
15
+ /**
16
+ * Wrap a client-id create command so a 409 conflict on the caller's OWN
17
+ * submitted `id` resolves as success instead of a form error — the
18
+ * double-submit of a create whose id the client generated.
19
+ *
20
+ * On any 409 (a `JsonapiError` / `ErrorResponse`-tagged cause), re-fetches
21
+ * `input.id` via `fetchExisting` and returns that. This is deliberately
22
+ * unconditional on *why* the 409 fired: the re-fetch is the proof, not the
23
+ * status. A 409 from an unrelated precondition never left a row at
24
+ * `input.id`, so the re-fetch fails and the original conflict rethrows
25
+ * unchanged. An input with no `id`, or any other failure, rethrows as is.
26
+ *
27
+ * `input.id` is read structurally rather than through a constraint on
28
+ * `Input`: a constraint here would be used only contravariantly (as
29
+ * `create`'s parameter), which makes TypeScript infer `Input` from the
30
+ * constraint instead of from the command's own input type.
31
+ */
32
+ export declare function idempotentCreate<Input, Output>(create: (input: Input) => Promise<Output>, fetchExisting: (id: string) => Promise<Output>): (input: Input) => Promise<Output>;
33
+ /**
34
+ * Idempotent-retry wiring for a client-id create action: `clientId` gates the
35
+ * wrap to commands whose input actually carries a client-generated `id` (an
36
+ * entity form definition's own `clientId`, from `defineEntityForm`);
37
+ * `fetchExisting` re-fetches the submitted id (see {@link idempotentCreate}).
38
+ */
39
+ export interface IdempotentCreateOptions<O> {
40
+ readonly clientId: boolean;
41
+ readonly fetchExisting: (id: string) => Promise<O> | Effect.Effect<O, unknown>;
42
+ }
43
+ /** Options for {@link runFormAction} and {@link formAction}. */
44
+ export interface FormActionOptions<A, O> {
45
+ /** Retry a client-id create's own 409 as success (see {@link IdempotentCreateOptions}). */
46
+ readonly idempotent?: IdempotentCreateOptions<O> | undefined;
47
+ /**
48
+ * How a command failure is recognised. Defaults to
49
+ * {@link CommandFailure.jsonapiFailure}; compose with it to add an
50
+ * application's own failures.
51
+ */
52
+ readonly classify?: Classify | undefined;
53
+ /**
54
+ * Applied to every form state echoed back to the browser (an invalid
55
+ * submission, a routed command failure) — e.g. blanking password fields
56
+ * so a secret never rides a response body.
57
+ */
58
+ readonly redact?: ((state: FormState<A>) => FormState<A>) | undefined;
59
+ /**
60
+ * What happens to a command failure the classifier does not recognise:
61
+ * `"error"` (the default) answers an `Error` result at 500 carrying the
62
+ * cause's message; `"throw"` rethrows the cause, leaving it to SvelteKit's
63
+ * `handleError`.
64
+ */
65
+ readonly unrecognised?: "error" | "throw" | undefined;
66
+ }
67
+ /**
68
+ * Validate `request` against `form`, then:
69
+ *
70
+ * - invalid → a `Failure` carrying the form state (nested errors + posted
71
+ * values), status 400, and the command never runs;
72
+ * - valid → run the command (wrapped in the idempotent retry when
73
+ * `options.idempotent.clientId` is true); on success a `Redirect` (303) to
74
+ * `locationFor(output, input)`;
75
+ * - a recognised command failure → a `Failure` at the failure's own status,
76
+ * each JSON:API error routed onto its field (or the root);
77
+ * - an unrecognised one → an `Error` result at 500 (or a rethrow, per
78
+ * `options.unrecognised`).
79
+ *
80
+ * Posted values survive every failure, so the form re-renders what was typed.
81
+ *
82
+ * `locationFor` returns the redirect target as-is: `runFormAction` does not
83
+ * check it. {@link formAction} hands it to SvelteKit's `redirect`, which
84
+ * accepts only a same-origin location (see there).
85
+ */
86
+ export declare function runFormAction<S extends Schema.Top, O, Message = unknown>(form: Form.Form<S, Message>, request: Request, run: Command$1<S["Type"], O>, locationFor: (output: O, input: S["Type"]) => string, options?: FormActionOptions<S["Type"], O>): Promise<FormActionResult<S["Type"]>>;
87
+ /**
88
+ * A command for {@link formAction}: like {@link Command}, and also handed the
89
+ * `RequestEvent` (for `locals`, `cookies`, `fetch`).
90
+ */
91
+ export type ActionCommand<I, O> = (input: I, event: RequestEvent) => Promise<O> | Effect.Effect<O, unknown>;
92
+ /**
93
+ * Derive a SvelteKit page form **action** from `form` plus its command. The
94
+ * handler validates the submission, runs the command on success, and maps
95
+ * the outcome onto SvelteKit's action protocol:
96
+ *
97
+ * - success → `redirect(303, locationFor(output, input))`;
98
+ * - an invalid submission or a recognised command failure →
99
+ * `fail(status, { form })`, carrying the (redacted) state;
100
+ * - an unrecognised failure → `fail(500, { message })` (or a rethrow).
101
+ *
102
+ * Wire it into `export const actions`; the client `createForm` store applies
103
+ * the returned `form` via {@link enhanceForm} (or `applyActionResult`).
104
+ *
105
+ * `locationFor` must return a same-origin path, such as
106
+ * `/organisations/${id}`. SvelteKit 3's `redirect` rejects an external
107
+ * location unless it is passed `{ external }`, and `formAction` never passes
108
+ * it, so a `locationFor` that returns an absolute URL on another origin (or a
109
+ * protocol-relative `//host/...`) makes the action throw an `Error` instead of
110
+ * redirecting, after the command has already run. To send the user off-site,
111
+ * redirect to a same-origin route that performs the external redirect itself.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // src/routes/organisations/create/+page.server.ts
116
+ * import { formAction } from "@thomasfosterau/effect-forms-svelte/kit";
117
+ * import { organisationForm } from "#lib/forms";
118
+ * import { api } from "#lib/server/api";
119
+ *
120
+ * export const actions = {
121
+ * default: formAction(
122
+ * organisationForm,
123
+ * (input, { locals }) => api(locals).organisations.create(input),
124
+ * (id) => `/organisations/${id}`,
125
+ * ),
126
+ * };
127
+ * ```
128
+ */
129
+ export declare function formAction<S extends Schema.Top, O, Message = unknown>(form: Form.Form<S, Message>, run: ActionCommand<S["Type"], O>, locationFor: (output: O, input: S["Type"]) => string, options?: FormActionOptions<S["Type"], O>): (event: RequestEvent) => Promise<FormActionData<S["Type"], unknown> | import("@sveltejs/kit").ActionFailure<FormActionData<S["Type"], unknown>>>;
130
+ //#endregion
131
+ export { Command$1 as Command };
132
+ //# sourceMappingURL=FormAction.d.ts.map