@effect-app/vue-components 4.0.0-beta.20 → 4.0.0-beta.201
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 +36 -8
- package/dist/reset.css +52 -0
- package/dist/types/components/CommandButton.vue.d.ts +6 -4
- package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
- package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
- package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
- package/dist/types/components/OmegaForm/errors.d.ts +33 -0
- package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
- package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
- package/dist/types/components/OmegaForm/index.d.ts +13 -3
- package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
- package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
- package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
- package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
- package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
- package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
- package/dist/types/components/OmegaForm/submit.d.ts +60 -0
- package/dist/types/components/OmegaForm/types.d.ts +281 -0
- package/dist/types/components/OmegaForm/useOmegaForm.d.ts +7 -213
- package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/utils/index.d.ts +6 -6
- package/dist/vue-components.es.js +29 -45
- package/dist/vue-components10.es.js +5 -0
- package/dist/vue-components11.es.js +20 -0
- package/dist/vue-components12.es.js +49 -0
- package/dist/vue-components13.es.js +128 -0
- package/dist/vue-components14.es.js +65 -0
- package/dist/vue-components15.es.js +60 -0
- package/dist/vue-components16.es.js +22 -0
- package/dist/vue-components17.es.js +5 -0
- package/dist/vue-components18.es.js +80 -0
- package/dist/vue-components19.es.js +92 -0
- package/dist/vue-components2.es.js +11 -0
- package/dist/vue-components20.es.js +73 -0
- package/dist/vue-components21.es.js +12 -0
- package/dist/vue-components22.es.js +56 -0
- package/dist/vue-components23.es.js +5 -0
- package/dist/vue-components24.es.js +44 -0
- package/dist/vue-components25.es.js +5 -0
- package/dist/vue-components26.es.js +84 -0
- package/dist/vue-components28.es.js +8 -0
- package/dist/vue-components29.es.js +9 -0
- package/dist/vue-components3.es.js +86 -0
- package/dist/vue-components30.es.js +269 -0
- package/dist/vue-components32.es.js +8 -0
- package/dist/vue-components33.es.js +73 -0
- package/dist/vue-components34.es.js +5 -0
- package/dist/vue-components35.es.js +52 -0
- package/dist/vue-components36.es.js +5 -0
- package/dist/vue-components37.es.js +24 -0
- package/dist/vue-components38.es.js +5 -0
- package/dist/vue-components39.es.js +59 -0
- package/dist/vue-components4.es.js +5 -0
- package/dist/vue-components40.es.js +5 -0
- package/dist/vue-components41.es.js +12 -0
- package/dist/vue-components42.es.js +22 -0
- package/dist/vue-components44.es.js +9 -0
- package/dist/vue-components45.es.js +4 -0
- package/dist/vue-components46.es.js +38 -0
- package/dist/vue-components47.es.js +27 -0
- package/dist/vue-components48.es.js +28 -0
- package/dist/vue-components49.es.js +7 -0
- package/dist/vue-components5.es.js +24 -0
- package/dist/vue-components50.es.js +18 -0
- package/dist/vue-components51.es.js +36 -0
- package/dist/vue-components52.es.js +18 -0
- package/dist/vue-components53.es.js +21 -0
- package/dist/vue-components54.es.js +30 -0
- package/dist/vue-components55.es.js +7 -0
- package/dist/vue-components56.es.js +9 -0
- package/dist/vue-components57.es.js +38 -0
- package/dist/vue-components58.es.js +25 -0
- package/dist/vue-components59.es.js +128 -0
- package/dist/vue-components6.es.js +13 -0
- package/dist/vue-components60.es.js +24 -0
- package/dist/vue-components61.es.js +21 -0
- package/dist/vue-components62.es.js +9 -0
- package/dist/vue-components63.es.js +19 -0
- package/dist/vue-components64.es.js +5 -0
- package/dist/vue-components65.es.js +29 -0
- package/dist/vue-components66.es.js +5 -0
- package/dist/vue-components67.es.js +29 -0
- package/dist/vue-components68.es.js +6 -0
- package/dist/vue-components69.es.js +18 -0
- package/dist/vue-components7.es.js +13 -0
- package/dist/vue-components70.es.js +40 -0
- package/dist/vue-components71.es.js +81 -0
- package/dist/vue-components72.es.js +33 -0
- package/dist/vue-components73.es.js +19 -0
- package/dist/vue-components74.es.js +48 -0
- package/dist/vue-components8.es.js +35 -0
- package/dist/vue-components9.es.js +47 -0
- package/package.json +35 -31
- package/src/components/CommandButton.vue +55 -7
- package/src/components/OmegaForm/OmegaArray.vue +2 -4
- package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
- package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
- package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
- package/src/components/OmegaForm/OmegaInput.vue +7 -36
- package/src/components/OmegaForm/OmegaInputVuetify.vue +5 -2
- package/src/components/OmegaForm/OmegaInternalInput.vue +18 -10
- package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
- package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +1 -1
- package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
- package/src/components/OmegaForm/blockDialog.ts +18 -6
- package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
- package/src/components/OmegaForm/errors.ts +136 -0
- package/src/components/OmegaForm/getOmegaStore.ts +1 -1
- package/src/components/OmegaForm/hocs.ts +19 -0
- package/src/components/OmegaForm/index.ts +16 -4
- package/src/components/OmegaForm/inputs.ts +22 -0
- package/src/components/OmegaForm/meta/checks.ts +81 -0
- package/src/components/OmegaForm/meta/createMeta.ts +138 -0
- package/src/components/OmegaForm/meta/defaults.ts +132 -0
- package/src/components/OmegaForm/meta/redacted.ts +66 -0
- package/src/components/OmegaForm/meta/types.ts +78 -0
- package/src/components/OmegaForm/meta/walker.ts +248 -0
- package/src/components/OmegaForm/persistency.ts +247 -0
- package/src/components/OmegaForm/submit.ts +128 -0
- package/src/components/OmegaForm/types.ts +751 -0
- package/src/components/OmegaForm/useOmegaForm.ts +58 -893
- package/src/components/OmegaForm/validation/localized.ts +202 -0
- package/src/index.ts +0 -1
- package/src/reset.css +52 -0
- package/src/utils/index.ts +10 -7
- package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -157
- package/dist/types/constants/index.d.ts +0 -1
- package/dist/vue-components.es10.js +0 -239
- package/dist/vue-components.es11.js +0 -32
- package/dist/vue-components.es12.js +0 -481
- package/dist/vue-components.es13.js +0 -49
- package/dist/vue-components.es14.js +0 -4
- package/dist/vue-components.es15.js +0 -4
- package/dist/vue-components.es16.js +0 -13
- package/dist/vue-components.es17.js +0 -6
- package/dist/vue-components.es18.js +0 -13
- package/dist/vue-components.es19.js +0 -57
- package/dist/vue-components.es2.js +0 -31
- package/dist/vue-components.es20.js +0 -56
- package/dist/vue-components.es21.js +0 -8
- package/dist/vue-components.es22.js +0 -8
- package/dist/vue-components.es23.js +0 -5
- package/dist/vue-components.es24.js +0 -5
- package/dist/vue-components.es25.js +0 -4
- package/dist/vue-components.es26.js +0 -4
- package/dist/vue-components.es27.js +0 -4
- package/dist/vue-components.es28.js +0 -4
- package/dist/vue-components.es29.js +0 -19
- package/dist/vue-components.es3.js +0 -17
- package/dist/vue-components.es30.js +0 -194
- package/dist/vue-components.es32.js +0 -31
- package/dist/vue-components.es33.js +0 -6
- package/dist/vue-components.es34.js +0 -4
- package/dist/vue-components.es35.js +0 -4
- package/dist/vue-components.es36.js +0 -113
- package/dist/vue-components.es38.js +0 -9
- package/dist/vue-components.es39.js +0 -34
- package/dist/vue-components.es4.js +0 -52
- package/dist/vue-components.es41.js +0 -6
- package/dist/vue-components.es42.js +0 -25
- package/dist/vue-components.es43.js +0 -7
- package/dist/vue-components.es44.js +0 -23
- package/dist/vue-components.es45.js +0 -32
- package/dist/vue-components.es46.js +0 -24
- package/dist/vue-components.es47.js +0 -14
- package/dist/vue-components.es48.js +0 -7
- package/dist/vue-components.es49.js +0 -21
- package/dist/vue-components.es5.js +0 -52
- package/dist/vue-components.es50.js +0 -11
- package/dist/vue-components.es51.js +0 -33
- package/dist/vue-components.es52.js +0 -50
- package/dist/vue-components.es53.js +0 -28
- package/dist/vue-components.es54.js +0 -13
- package/dist/vue-components.es55.js +0 -67
- package/dist/vue-components.es56.js +0 -58
- package/dist/vue-components.es57.js +0 -19
- package/dist/vue-components.es58.js +0 -35
- package/dist/vue-components.es59.js +0 -31
- package/dist/vue-components.es6.js +0 -69
- package/dist/vue-components.es60.js +0 -44
- package/dist/vue-components.es61.js +0 -4
- package/dist/vue-components.es62.js +0 -46
- package/dist/vue-components.es63.js +0 -4
- package/dist/vue-components.es7.js +0 -83
- package/dist/vue-components.es8.js +0 -63
- package/dist/vue-components.es9.js +0 -21
- package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1184
- 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
|
+
}
|
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
+
}
|