@effect-app/vue-components 4.0.0-beta.21 → 4.0.0-beta.211

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 (196) hide show
  1. package/README.md +36 -8
  2. package/dist/reset.css +52 -0
  3. package/dist/types/components/CommandButton.vue.d.ts +6 -4
  4. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  5. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  6. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
  7. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  9. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
  10. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
  12. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  13. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  14. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  15. package/dist/types/components/OmegaForm/index.d.ts +13 -3
  16. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  17. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  18. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
  19. package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
  20. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  21. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  22. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  23. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  24. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  25. package/dist/types/components/OmegaForm/types.d.ts +281 -0
  26. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +7 -213
  27. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  28. package/dist/types/index.d.ts +0 -1
  29. package/dist/types/utils/index.d.ts +6 -6
  30. package/dist/vue-components.es.js +29 -45
  31. package/dist/vue-components10.es.js +5 -0
  32. package/dist/vue-components11.es.js +20 -0
  33. package/dist/vue-components12.es.js +49 -0
  34. package/dist/vue-components13.es.js +128 -0
  35. package/dist/vue-components14.es.js +65 -0
  36. package/dist/vue-components15.es.js +60 -0
  37. package/dist/vue-components16.es.js +22 -0
  38. package/dist/vue-components17.es.js +5 -0
  39. package/dist/vue-components18.es.js +80 -0
  40. package/dist/vue-components19.es.js +92 -0
  41. package/dist/vue-components2.es.js +11 -0
  42. package/dist/vue-components20.es.js +73 -0
  43. package/dist/vue-components21.es.js +12 -0
  44. package/dist/vue-components22.es.js +56 -0
  45. package/dist/vue-components23.es.js +5 -0
  46. package/dist/vue-components24.es.js +44 -0
  47. package/dist/vue-components25.es.js +5 -0
  48. package/dist/vue-components26.es.js +84 -0
  49. package/dist/vue-components28.es.js +8 -0
  50. package/dist/vue-components29.es.js +9 -0
  51. package/dist/vue-components3.es.js +86 -0
  52. package/dist/vue-components30.es.js +269 -0
  53. package/dist/vue-components32.es.js +8 -0
  54. package/dist/vue-components33.es.js +73 -0
  55. package/dist/vue-components34.es.js +5 -0
  56. package/dist/vue-components35.es.js +52 -0
  57. package/dist/vue-components36.es.js +5 -0
  58. package/dist/vue-components37.es.js +24 -0
  59. package/dist/vue-components38.es.js +5 -0
  60. package/dist/vue-components39.es.js +59 -0
  61. package/dist/vue-components4.es.js +5 -0
  62. package/dist/vue-components40.es.js +5 -0
  63. package/dist/vue-components41.es.js +12 -0
  64. package/dist/vue-components42.es.js +22 -0
  65. package/dist/vue-components44.es.js +9 -0
  66. package/dist/vue-components45.es.js +4 -0
  67. package/dist/vue-components46.es.js +38 -0
  68. package/dist/vue-components47.es.js +27 -0
  69. package/dist/vue-components48.es.js +28 -0
  70. package/dist/vue-components49.es.js +7 -0
  71. package/dist/vue-components5.es.js +24 -0
  72. package/dist/vue-components50.es.js +18 -0
  73. package/dist/vue-components51.es.js +36 -0
  74. package/dist/vue-components52.es.js +18 -0
  75. package/dist/vue-components53.es.js +21 -0
  76. package/dist/vue-components54.es.js +30 -0
  77. package/dist/vue-components55.es.js +7 -0
  78. package/dist/vue-components56.es.js +9 -0
  79. package/dist/vue-components57.es.js +38 -0
  80. package/dist/vue-components58.es.js +25 -0
  81. package/dist/vue-components59.es.js +128 -0
  82. package/dist/vue-components6.es.js +13 -0
  83. package/dist/vue-components60.es.js +24 -0
  84. package/dist/vue-components61.es.js +21 -0
  85. package/dist/vue-components62.es.js +9 -0
  86. package/dist/vue-components63.es.js +19 -0
  87. package/dist/vue-components64.es.js +5 -0
  88. package/dist/vue-components65.es.js +29 -0
  89. package/dist/vue-components66.es.js +5 -0
  90. package/dist/vue-components67.es.js +29 -0
  91. package/dist/vue-components68.es.js +6 -0
  92. package/dist/vue-components69.es.js +18 -0
  93. package/dist/vue-components7.es.js +13 -0
  94. package/dist/vue-components70.es.js +40 -0
  95. package/dist/vue-components71.es.js +81 -0
  96. package/dist/vue-components72.es.js +33 -0
  97. package/dist/vue-components73.es.js +19 -0
  98. package/dist/vue-components74.es.js +48 -0
  99. package/dist/vue-components8.es.js +35 -0
  100. package/dist/vue-components9.es.js +47 -0
  101. package/package.json +35 -31
  102. package/src/components/CommandButton.vue +55 -7
  103. package/src/components/OmegaForm/OmegaArray.vue +2 -4
  104. package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
  105. package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
  106. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  107. package/src/components/OmegaForm/OmegaInput.vue +7 -36
  108. package/src/components/OmegaForm/OmegaInputVuetify.vue +5 -2
  109. package/src/components/OmegaForm/OmegaInternalInput.vue +18 -10
  110. package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
  111. package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +1 -1
  112. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  113. package/src/components/OmegaForm/blockDialog.ts +18 -6
  114. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
  115. package/src/components/OmegaForm/errors.ts +136 -0
  116. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  117. package/src/components/OmegaForm/hocs.ts +19 -0
  118. package/src/components/OmegaForm/index.ts +16 -4
  119. package/src/components/OmegaForm/inputs.ts +22 -0
  120. package/src/components/OmegaForm/meta/checks.ts +81 -0
  121. package/src/components/OmegaForm/meta/createMeta.ts +138 -0
  122. package/src/components/OmegaForm/meta/defaults.ts +132 -0
  123. package/src/components/OmegaForm/meta/redacted.ts +66 -0
  124. package/src/components/OmegaForm/meta/types.ts +78 -0
  125. package/src/components/OmegaForm/meta/walker.ts +248 -0
  126. package/src/components/OmegaForm/persistency.ts +247 -0
  127. package/src/components/OmegaForm/submit.ts +128 -0
  128. package/src/components/OmegaForm/types.ts +751 -0
  129. package/src/components/OmegaForm/useOmegaForm.ts +58 -893
  130. package/src/components/OmegaForm/validation/localized.ts +202 -0
  131. package/src/index.ts +0 -1
  132. package/src/reset.css +52 -0
  133. package/src/utils/index.ts +10 -7
  134. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -157
  135. package/dist/types/constants/index.d.ts +0 -1
  136. package/dist/vue-components.es10.js +0 -239
  137. package/dist/vue-components.es11.js +0 -32
  138. package/dist/vue-components.es12.js +0 -481
  139. package/dist/vue-components.es13.js +0 -49
  140. package/dist/vue-components.es14.js +0 -4
  141. package/dist/vue-components.es15.js +0 -4
  142. package/dist/vue-components.es16.js +0 -6
  143. package/dist/vue-components.es17.js +0 -13
  144. package/dist/vue-components.es18.js +0 -57
  145. package/dist/vue-components.es19.js +0 -56
  146. package/dist/vue-components.es2.js +0 -31
  147. package/dist/vue-components.es20.js +0 -8
  148. package/dist/vue-components.es21.js +0 -8
  149. package/dist/vue-components.es22.js +0 -5
  150. package/dist/vue-components.es23.js +0 -5
  151. package/dist/vue-components.es24.js +0 -4
  152. package/dist/vue-components.es25.js +0 -4
  153. package/dist/vue-components.es26.js +0 -4
  154. package/dist/vue-components.es27.js +0 -4
  155. package/dist/vue-components.es28.js +0 -19
  156. package/dist/vue-components.es29.js +0 -13
  157. package/dist/vue-components.es3.js +0 -17
  158. package/dist/vue-components.es30.js +0 -31
  159. package/dist/vue-components.es31.js +0 -6
  160. package/dist/vue-components.es32.js +0 -4
  161. package/dist/vue-components.es33.js +0 -4
  162. package/dist/vue-components.es34.js +0 -113
  163. package/dist/vue-components.es36.js +0 -9
  164. package/dist/vue-components.es37.js +0 -34
  165. package/dist/vue-components.es39.js +0 -194
  166. package/dist/vue-components.es4.js +0 -52
  167. package/dist/vue-components.es41.js +0 -6
  168. package/dist/vue-components.es42.js +0 -25
  169. package/dist/vue-components.es43.js +0 -7
  170. package/dist/vue-components.es44.js +0 -23
  171. package/dist/vue-components.es45.js +0 -32
  172. package/dist/vue-components.es46.js +0 -24
  173. package/dist/vue-components.es47.js +0 -14
  174. package/dist/vue-components.es48.js +0 -7
  175. package/dist/vue-components.es49.js +0 -21
  176. package/dist/vue-components.es5.js +0 -52
  177. package/dist/vue-components.es50.js +0 -11
  178. package/dist/vue-components.es51.js +0 -33
  179. package/dist/vue-components.es52.js +0 -50
  180. package/dist/vue-components.es53.js +0 -28
  181. package/dist/vue-components.es54.js +0 -13
  182. package/dist/vue-components.es55.js +0 -67
  183. package/dist/vue-components.es56.js +0 -58
  184. package/dist/vue-components.es57.js +0 -19
  185. package/dist/vue-components.es58.js +0 -35
  186. package/dist/vue-components.es59.js +0 -31
  187. package/dist/vue-components.es6.js +0 -69
  188. package/dist/vue-components.es60.js +0 -44
  189. package/dist/vue-components.es61.js +0 -4
  190. package/dist/vue-components.es62.js +0 -46
  191. package/dist/vue-components.es63.js +0 -4
  192. package/dist/vue-components.es7.js +0 -83
  193. package/dist/vue-components.es8.js +0 -63
  194. package/dist/vue-components.es9.js +0 -21
  195. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1184
  196. package/src/constants/index.ts +0 -1
@@ -0,0 +1,136 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { type Component, computed, type ComputedRef, type ConcreteComponent, h, onUnmounted, type Ref, ref, watch } from "vue"
4
+ import { useIntl } from "../../utils"
5
+ import type { OmegaError } from "./types"
6
+ import type { OF } from "./useOmegaForm"
7
+
8
+ export const useErrorLabel = (form: OF<any, any>) => {
9
+ const { formatMessage } = useIntl()
10
+ const humanize = (str: string) => {
11
+ return str
12
+ .replace(/([A-Z])/g, " $1") // Add space before capital letters
13
+ .replace(/^./, (char) => char.toUpperCase()) // Capitalize the first letter
14
+ .trim() // Remove leading/trailing spaces
15
+ }
16
+ const fallback = (propsName: string) =>
17
+ formatMessage
18
+ ? formatMessage({ id: `general.fields.${propsName}`, defaultMessage: humanize(propsName) })
19
+ : humanize(propsName)
20
+ const i18n = (propsName: string) =>
21
+ form.i18nNamespace
22
+ ? formatMessage({ id: `${form.i18nNamespace}.fields.${propsName}`, defaultMessage: fallback(propsName) })
23
+ : fallback(propsName)
24
+
25
+ return i18n
26
+ }
27
+
28
+ export const eHoc = (errorProps: {
29
+ form: OF<any, any>
30
+ fieldMap: Ref<Map<string, { id: string; label: string }>>
31
+ }) => {
32
+ return function FormHoc<P>(
33
+ WrappedComponent: Component<P>
34
+ ): ConcreteComponent<P> {
35
+ return {
36
+ setup() {
37
+ const { fieldMap, form } = errorProps
38
+ const generalErrors = form.useStore((state) => state.errors)
39
+ const fieldMeta = form.useStore((state) => state.fieldMeta)
40
+ const errorMap = form.useStore((state) => state.errorMap)
41
+
42
+ const errorLabel = useErrorLabel(form)
43
+
44
+ const errors = computed(() => {
45
+ // Collect errors from fieldMeta (field-level errors for registered fields)
46
+ const fieldErrors = Object.entries(fieldMeta.value).reduce<OmegaError[]>((acc, [key, m]) => {
47
+ const fieldErrors = (m as { errors?: Array<{ message?: string }> } | undefined)?.errors ?? []
48
+ if (!fieldErrors.length) {
49
+ return acc
50
+ }
51
+
52
+ const fieldInfo = fieldMap.value.get(key)
53
+ if (!fieldInfo) {
54
+ return acc
55
+ }
56
+
57
+ acc.push({
58
+ label: fieldInfo.label,
59
+ inputId: fieldInfo.id,
60
+ errors: [fieldErrors[0]?.message].filter(Boolean) as string[]
61
+ })
62
+
63
+ return acc
64
+ }, [])
65
+
66
+ // Collect errors from errorMap.onDynamic / errorMap.onSubmit ONLY for fields that are NOT registered
67
+ // (registered fields already have their errors in fieldMeta).
68
+ // Our localized standard schema writes to onDynamic via validationLogic: revalidateLogic();
69
+ // caller-provided validators.onSubmit (via tanstackFormOptions spread) writes to onSubmit.
70
+ const submitErrors: OmegaError[] = []
71
+ const submitIssueMaps = [errorMap.value.onDynamic, errorMap.value.onSubmit].filter(
72
+ Boolean
73
+ ) as unknown as Array<
74
+ Record<string, unknown>
75
+ >
76
+ const seenPaths = new Set<string>()
77
+ for (const issuesByPath of submitIssueMaps) {
78
+ for (const [_, issues] of Object.entries(issuesByPath)) {
79
+ if (Array.isArray(issues) && issues.length) {
80
+ for (const issue of issues) {
81
+ const issAny: any = issue
82
+ if (issAny?.path && Array.isArray(issAny.path) && issAny.path.length) {
83
+ const fieldPath = issAny.path.join(".")
84
+ if (!fieldMap.value.has(fieldPath) && !seenPaths.has(fieldPath)) {
85
+ seenPaths.add(fieldPath)
86
+ submitErrors.push({
87
+ label: errorLabel(fieldPath),
88
+ inputId: fieldPath,
89
+ errors: [issAny.message].filter(Boolean)
90
+ })
91
+ break
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Combine both error sources (no need to check for duplicates since they're mutually exclusive)
100
+ return [...fieldErrors, ...submitErrors]
101
+ })
102
+
103
+ return {
104
+ generalErrors,
105
+ errors
106
+ }
107
+ },
108
+ render({ errors, generalErrors }: any) {
109
+ return h(WrappedComponent, {
110
+ errors,
111
+ generalErrors,
112
+ ...this.$attrs
113
+ }, this.$slots)
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ export const makeFieldMap = () => {
120
+ const fieldMap = ref(new Map<string, { label: string; id: string }>())
121
+ const registerField = (field: ComputedRef<{ name: string; label: string; id: string }>) => {
122
+ watch(field, (f) => {
123
+ fieldMap.value.set(f.name, { label: f.label, id: f.id })
124
+ }, { immediate: true })
125
+ onUnmounted(() => {
126
+ // Only delete if we still own this entry (id matches)
127
+ // This prevents old components from deleting entries registered by new components
128
+ // during re-mount transitions (e.g., when :key changes)
129
+ const currentEntry = fieldMap.value.get(field.value.name)
130
+ if (currentEntry?.id === field.value.id) {
131
+ fieldMap.value.delete(field.value.name)
132
+ }
133
+ })
134
+ }
135
+ return { fieldMap, registerField }
136
+ }
@@ -1,6 +1,6 @@
1
1
  import { useStore } from "@tanstack/vue-form"
2
2
  import { computed, type Ref } from "vue"
3
- import type { OmegaFormApi, OmegaFormState } from "./OmegaFormStuff"
3
+ import type { OmegaFormApi, OmegaFormState } from "./types"
4
4
 
5
5
  export function getOmegaStore<
6
6
  To,
@@ -0,0 +1,19 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { type Component, type ConcreteComponent, h } from "vue"
4
+ import type { OF } from "./useOmegaForm"
5
+
6
+ export const fHoc = (form: OF<any, any>) => {
7
+ return function FormHoc<P>(
8
+ WrappedComponent: Component<P>
9
+ ): ConcreteComponent<P> {
10
+ return {
11
+ render() {
12
+ return h(WrappedComponent, {
13
+ form,
14
+ ...this.$attrs
15
+ }, this.$slots)
16
+ }
17
+ }
18
+ }
19
+ }
@@ -1,5 +1,17 @@
1
- export * from "./OmegaFormStuff"
2
- export { type OmegaConfig, type OmegaFormReturn, useOmegaForm } from "./useOmegaForm"
1
+ import type { S } from "effect-app"
2
+
3
+ export { getInputType, type SupportedInputs } from "./inputs"
4
+ export { createMeta, generateMetaFromSchema, isNullableOrUndefined, metadataFromAst } from "./meta/createMeta"
5
+ export type { CreateMeta, FilterItems } from "./meta/createMeta"
6
+ export { defaultsValueFromSchema } from "./meta/defaults"
7
+ export { toFormSchema } from "./meta/redacted"
8
+ export type { BaseFieldMeta, BooleanFieldMeta, DateFieldMeta, FieldMeta, MetaRecord, MultipleFieldMeta, NestedKeyOf, NumberFieldMeta, SelectFieldMeta, StringFieldMeta, UnknownFieldMeta } from "./meta/types"
9
+ export { deepMerge } from "./persistency"
10
+ export type { BaseProps, DefaultTypeProps, FieldPath, FieldPath_, FieldValidators, FormComponent, FormProps, FormType, OmegaArrayProps, OmegaAutoGenMeta, OmegaError, OmegaFormApi, OmegaFormParams, OmegaFormState, OmegaInputProps, OmegaInputPropsBase, PrefixFromDepth, TypeOverride, TypesWithOptions } from "./types"
11
+ export { makeStandardSchemaV1Hooks, toLocalizedStandardSchemaV1 } from "./validation/localized"
12
+
13
+ export { FormErrors, OmegaFormKey, useErrorLabel, useOmegaForm } from "./useOmegaForm"
14
+ export type { defaultValuesPriorityUnion, OF, OmegaConfig, OmegaFormReturn, Policies } from "./useOmegaForm"
3
15
 
4
16
  export { type ExtractTagValue, type ExtractUnionBranch, type InputProps, type MergedInputProps, type TaggedUnionOption, type TaggedUnionOptionsArray, type TaggedUnionProps } from "./InputProps"
5
17
  export { default as OmegaInput } from "./OmegaInput.vue"
@@ -9,6 +21,6 @@ export { default as OmegaTaggedUnionInternal } from "./OmegaTaggedUnionInternal.
9
21
 
10
22
  export { useOnClose, usePreventClose } from "./blockDialog"
11
23
 
12
- export { getInputType } from "./OmegaFormStuff"
13
-
14
24
  export { createUseFormWithCustomInput } from "./createUseFormWithCustomInput"
25
+
26
+ export const duplicateSchema = <From, To>(schema: S.Codec<To, From>) => schema
@@ -0,0 +1,22 @@
1
+ const supportedInputs = [
2
+ "button",
3
+ "checkbox",
4
+ "color",
5
+ "date",
6
+ "email",
7
+ "number",
8
+ "password",
9
+ "radio",
10
+ "range",
11
+ "search",
12
+ "submit",
13
+ "tel",
14
+ "text",
15
+ "time",
16
+ "url"
17
+ ] as const
18
+
19
+ export type SupportedInputs = typeof supportedInputs[number]
20
+
21
+ export const getInputType = (input: string): SupportedInputs =>
22
+ (supportedInputs as readonly string[]).includes(input) ? input as SupportedInputs : "text"
@@ -0,0 +1,81 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { type Record, S } from "effect-app"
3
+ import type { FieldMeta } from "./types"
4
+
5
+ export const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
6
+ const checks = property.checks ?? []
7
+
8
+ return checks.flatMap((check) => {
9
+ if (check._tag === "FilterGroup") {
10
+ return check.checks.flatMap((inner) => {
11
+ const meta = inner.annotations?.meta
12
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
13
+ })
14
+ }
15
+
16
+ const meta = check.annotations?.meta
17
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
18
+ })
19
+ }
20
+
21
+ export const getFieldMetadataFromAst = (property: S.AST.AST) => {
22
+ const base: Partial<FieldMeta> & Record<string, unknown> = {
23
+ description: S.AST.resolveDescription(property)
24
+ }
25
+ const checks = getCheckMetas(property)
26
+
27
+ if (S.AST.isString(property)) {
28
+ base.type = "string"
29
+ for (const check of checks) {
30
+ switch (check._tag) {
31
+ case "isMinLength":
32
+ base.minLength = check.minLength
33
+ break
34
+ case "isMaxLength":
35
+ base.maxLength = check.maxLength
36
+ break
37
+ }
38
+ }
39
+
40
+ const format = property.annotations?.["format"]
41
+ if (format === "email") {
42
+ base.format = "email"
43
+ }
44
+ } else if (S.AST.isNumber(property)) {
45
+ base.type = "number"
46
+ for (const check of checks) {
47
+ switch (check._tag) {
48
+ case "isInt":
49
+ base.refinement = "int"
50
+ break
51
+ case "isGreaterThanOrEqualTo":
52
+ base.minimum = check.minimum
53
+ break
54
+ case "isLessThanOrEqualTo":
55
+ base.maximum = check.maximum
56
+ break
57
+ case "isBetween":
58
+ base.minimum = check.minimum
59
+ base.maximum = check.maximum
60
+ break
61
+ case "isGreaterThan":
62
+ base.exclusiveMinimum = check.exclusiveMinimum
63
+ break
64
+ case "isLessThan":
65
+ base.exclusiveMaximum = check.exclusiveMaximum
66
+ break
67
+ }
68
+ }
69
+ } else if (S.AST.isBoolean(property)) {
70
+ base.type = "boolean"
71
+ } else if (
72
+ S.AST.isDeclaration(property)
73
+ && (property.annotations as any)?.typeConstructor?._tag === "Date"
74
+ ) {
75
+ base.type = "date"
76
+ } else {
77
+ base.type = "unknown"
78
+ }
79
+
80
+ return base
81
+ }
@@ -0,0 +1,138 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { type Effect, type Record, S } from "effect-app"
3
+ import { getTransformationFrom } from "../../../utils"
4
+ import type { FieldMeta, MetaRecord } from "./types"
5
+ import { classifyAndWalkUnion, leafMetaForAst, type ParentMeta, type WalkerContext, walkStruct } from "./walker"
6
+
7
+ export type FilterItems = {
8
+ items: readonly [string, ...string[]]
9
+ message:
10
+ | string
11
+ | Effect.Effect<string>
12
+ | { readonly message: string | Effect.Effect<string> }
13
+ }
14
+
15
+ export type CreateMeta =
16
+ & {
17
+ parent?: string
18
+ meta?: Record<string, any>
19
+ nullableOrUndefined?: false | "undefined" | "null"
20
+ }
21
+ & (
22
+ | {
23
+ propertySignatures: readonly S.AST.PropertySignature[]
24
+ property?: never
25
+ }
26
+ | {
27
+ propertySignatures?: never
28
+ property: S.AST.AST
29
+ }
30
+ )
31
+
32
+ export const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
33
+ let current = getTransformationFrom(property)
34
+
35
+ while (S.AST.isDeclaration(current) && current.typeParameters.length > 0) {
36
+ current = getTransformationFrom(current.typeParameters[0])
37
+ }
38
+
39
+ return current
40
+ }
41
+
42
+ export const isNullableOrUndefined = (property: false | S.AST.AST | undefined) => {
43
+ if (!property || !S.AST.isUnion(property)) return false
44
+ if (property.types.find((_) => S.AST.isUndefined(_))) {
45
+ return "undefined"
46
+ }
47
+ if (property.types.find((_) => S.AST.isNull(_))) return "null"
48
+ return false
49
+ }
50
+
51
+ export const createMeta = <T = any>(
52
+ { meta = {}, parent = "", property, propertySignatures }: CreateMeta,
53
+ acc: Partial<MetaRecord<T>> = {}
54
+ ): MetaRecord<T> | FieldMeta => {
55
+ const ctx: WalkerContext<T> = { acc, unionMeta: {} }
56
+
57
+ if (propertySignatures) {
58
+ const parentMeta: ParentMeta = {
59
+ required: meta.required !== false,
60
+ nullableOrUndefined: meta.nullableOrUndefined ?? false
61
+ }
62
+ walkStruct(propertySignatures, parent, parentMeta, ctx)
63
+ return acc
64
+ }
65
+
66
+ if (property) {
67
+ const nullableOrUndefined = isNullableOrUndefined(property)
68
+ const unwrapped = unwrapDeclaration(property)
69
+ const required = !Object.hasOwnProperty.call(meta, "required")
70
+ ? !nullableOrUndefined
71
+ : (meta.required as boolean)
72
+
73
+ const parentMeta: ParentMeta = {
74
+ required,
75
+ nullableOrUndefined: (meta.nullableOrUndefined ?? nullableOrUndefined) as false | "null" | "undefined"
76
+ }
77
+
78
+ if (S.AST.isObjects(unwrapped)) {
79
+ walkStruct(unwrapped.propertySignatures, parent, parentMeta, ctx)
80
+ return acc
81
+ }
82
+
83
+ if (S.AST.isUnion(unwrapped)) {
84
+ // For property-mode, return a FieldMeta by running through classifyAndWalkUnion
85
+ // and then pulling out the result at `parent` key
86
+ const leafCtx: WalkerContext<T> = { acc: {}, unionMeta: {} }
87
+ classifyAndWalkUnion(unwrapped, parent, parentMeta, leafCtx)
88
+ const result = (leafCtx.acc as any)[parent]
89
+ if (result) return result as FieldMeta
90
+ }
91
+
92
+ return leafMetaForAst(unwrapped, parentMeta)
93
+ }
94
+
95
+ return acc
96
+ }
97
+
98
+ export const metadataFromAst = <From, To>(
99
+ schema: S.Codec<To, From>
100
+ ): {
101
+ meta: MetaRecord<To>
102
+ defaultValues: Record<string, any>
103
+ unionMeta: Record<string, MetaRecord<To>>
104
+ } => {
105
+ const ast = unwrapDeclaration(schema.ast)
106
+ const newMeta: Partial<MetaRecord<To>> = {}
107
+ const defaultValues: Record<string, any> = {}
108
+ const unionMeta: Record<string, MetaRecord<To>> = {}
109
+
110
+ const ctx: WalkerContext<To> = { acc: newMeta, unionMeta }
111
+
112
+ if (S.AST.isUnion(ast)) {
113
+ // Root-level discriminated union
114
+ classifyAndWalkUnion(ast, "", { required: true, nullableOrUndefined: false }, ctx)
115
+
116
+ return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
117
+ }
118
+
119
+ if (S.AST.isObjects(ast)) {
120
+ walkStruct(ast.propertySignatures, "", { required: true, nullableOrUndefined: false }, ctx)
121
+
122
+ return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
123
+ }
124
+
125
+ return { meta: newMeta as MetaRecord<To>, defaultValues, unionMeta }
126
+ }
127
+
128
+ export const generateMetaFromSchema = <From, To>(
129
+ schema: S.Codec<To, From>
130
+ ): {
131
+ schema: S.Codec<To, From>
132
+ meta: MetaRecord<To>
133
+ unionMeta: Record<string, MetaRecord<To>>
134
+ } => {
135
+ const { meta, unionMeta } = metadataFromAst(schema)
136
+
137
+ return { schema, meta, unionMeta }
138
+ }
@@ -0,0 +1,132 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Effect, Option, S } from "effect-app"
3
+ import { isNullableOrUndefined, unwrapDeclaration } from "./createMeta"
4
+
5
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
6
+ if (!link?.transformation?.decode?.run) return undefined
7
+ try {
8
+ const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
9
+ return Option.isSome(result) ? result.value : undefined
10
+ } catch {
11
+ return undefined
12
+ }
13
+ }
14
+
15
+ const getDefaultFromAst = (property: S.AST.AST) => {
16
+ // 1. Check withConstructorDefault (stored in context.defaultValue)
17
+ const constructorLink = property.context?.defaultValue?.[0]
18
+ const constructorDefault = extractDefaultFromLink(constructorLink)
19
+ if (constructorDefault !== undefined) return constructorDefault
20
+
21
+ // 2. Check withDecodingDefault (stored in encoding)
22
+ const encodingLink = property.encoding?.[0]
23
+ if (encodingLink && property.context?.isOptional) {
24
+ return extractDefaultFromLink(encodingLink)
25
+ }
26
+
27
+ return undefined
28
+ }
29
+
30
+ type SchemaWithMembers = {
31
+ members: readonly S.Schema<any>[]
32
+ }
33
+
34
+ function hasMembers(schema: any): schema is SchemaWithMembers {
35
+ return schema && "members" in schema && Array.isArray(schema.members)
36
+ }
37
+
38
+ // Internal implementation with WeakSet tracking
39
+ export const defaultsValueFromSchema = (
40
+ schema: S.Schema<any>,
41
+ record: Record<string, any> = {}
42
+ ): any => {
43
+ const ast = schema.ast
44
+ const defaultValue = getDefaultFromAst(ast)
45
+
46
+ if (defaultValue !== undefined) {
47
+ return defaultValue
48
+ }
49
+
50
+ if (isNullableOrUndefined(schema.ast) === "null") {
51
+ return null
52
+ }
53
+ if (isNullableOrUndefined(schema.ast) === "undefined") {
54
+ return undefined
55
+ }
56
+
57
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, Class, etc.)
58
+ const objectsAst = S.AST.isObjects(ast)
59
+ ? ast
60
+ : S.AST.isDeclaration(ast)
61
+ ? unwrapDeclaration(ast)
62
+ : undefined
63
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
64
+ const result: Record<string, any> = {}
65
+
66
+ for (const prop of objectsAst.propertySignatures) {
67
+ const key = prop.name.toString()
68
+ const propType = prop.type
69
+
70
+ const propDefault = getDefaultFromAst(propType)
71
+ if (propDefault !== undefined) {
72
+ result[key] = propDefault
73
+ continue
74
+ }
75
+
76
+ const propSchema = S.make(propType)
77
+ const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
78
+
79
+ if (propValue !== undefined) {
80
+ result[key] = propValue
81
+ } else if (isNullableOrUndefined(propType) === "undefined") {
82
+ result[key] = undefined
83
+ }
84
+ }
85
+
86
+ return { ...result, ...record }
87
+ }
88
+
89
+ // Handle unions via AST or schema-level .members
90
+ const unionTypes = S.AST.isUnion(ast)
91
+ ? ast.types
92
+ : hasMembers(schema)
93
+ ? schema.members.map((m) => m.ast)
94
+ : undefined
95
+ if (unionTypes) {
96
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
97
+
98
+ for (const memberAstRaw of unionTypes) {
99
+ const memberAst = unwrapDeclaration(memberAstRaw)
100
+ if (!S.AST.isObjects(memberAst)) continue
101
+
102
+ for (const prop of memberAst.propertySignatures) {
103
+ const key = prop.name.toString()
104
+ const fieldDefault = getDefaultFromAst(prop.type)
105
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key].ast) : undefined
106
+
107
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
108
+ mergedFields[key] = { ast: prop.type }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (Object.keys(mergedFields).length === 0) {
114
+ return Object.keys(record).length > 0 ? record : undefined
115
+ }
116
+
117
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
118
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
119
+ return acc
120
+ }, record)
121
+ }
122
+
123
+ if (Object.keys(record).length === 0) {
124
+ if (S.AST.isString(ast)) {
125
+ return ""
126
+ }
127
+
128
+ if (S.AST.isBoolean(ast)) {
129
+ return false
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,66 @@
1
+ import { S } from "effect-app"
2
+
3
+ /*
4
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
5
+ * These need to be swapped to S.RedactedFromValue for form usage
6
+ * because S.Redacted expects Redacted objects, not plain strings.
7
+ */
8
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
9
+ S.AST.isDeclaration(ast)
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Effect Schema AST annotations are loosely typed
11
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
12
+ && !ast.encoding
13
+
14
+ /*
15
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
16
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
17
+ * Redacted<A> on both encoded and type sides, so form inputs (which
18
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
19
+ * plain values on the encoded side and wraps them in Redacted on decode.
20
+ */
21
+ export const toFormSchema = <From, To>(
22
+ schema: S.Codec<To, From>
23
+ ): S.Codec<To, From> => {
24
+ const ast = schema.ast
25
+ const objAst = S.AST.isObjects(ast)
26
+ ? ast
27
+ : S.AST.isDeclaration(ast)
28
+ ? S.AST.toEncoded(ast)
29
+ : null
30
+
31
+ if (!objAst || !("propertySignatures" in objAst)) return schema
32
+
33
+ let hasRedacted = false
34
+ const props: Record<string, S.Struct.Fields[string]> = {}
35
+
36
+ for (const p of objAst.propertySignatures) {
37
+ if (isRedactedWithoutEncoding(p.type)) {
38
+ hasRedacted = true
39
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0])
40
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
41
+ } else if (S.AST.isUnion(p.type)) {
42
+ const types = p.type.types
43
+ const redactedType = types.find(isRedactedWithoutEncoding)
44
+ if (redactedType) {
45
+ hasRedacted = true
46
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0])
47
+ const hasNull = types.some(S.AST.isNull)
48
+ const hasUndefined = types.some(S.AST.isUndefined)
49
+ const base = S.RedactedFromValue(innerSchema)
50
+ props[p.name as string] = hasNull && hasUndefined
51
+ ? S.NullishOr(base)
52
+ : hasNull
53
+ ? S.NullOr(base)
54
+ : hasUndefined
55
+ ? S.UndefinedOr(base)
56
+ : base
57
+ } else {
58
+ props[p.name as string] = S.make(p.type)
59
+ }
60
+ } else {
61
+ props[p.name as string] = S.make(p.type)
62
+ }
63
+ }
64
+
65
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From> : schema
66
+ }