@effect-app/vue-components 4.0.0-beta.158 → 4.0.0-beta.159

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 (124) hide show
  1. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  2. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  3. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
  4. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  5. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  6. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +2 -1
  7. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +2 -2
  9. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  10. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  12. package/dist/types/components/OmegaForm/index.d.ts +13 -3
  13. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  14. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  15. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +32 -0
  16. package/dist/types/components/OmegaForm/meta/defaults.d.ts +2 -0
  17. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  18. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  19. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  20. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  21. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  22. package/dist/types/components/OmegaForm/types.d.ts +281 -0
  23. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +6 -212
  24. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  25. package/dist/vue-components.es.js +24 -16
  26. package/dist/vue-components10.es.js +4 -4
  27. package/dist/vue-components11.es.js +19 -12
  28. package/dist/vue-components12.es.js +22 -444
  29. package/dist/vue-components13.es.js +126 -3
  30. package/dist/vue-components14.es.js +61 -34
  31. package/dist/vue-components15.es.js +57 -24
  32. package/dist/vue-components16.es.js +20 -26
  33. package/dist/vue-components17.es.js +4 -6
  34. package/dist/vue-components18.es.js +78 -16
  35. package/dist/vue-components19.es.js +86 -30
  36. package/dist/vue-components20.es.js +72 -17
  37. package/dist/vue-components21.es.js +10 -19
  38. package/dist/vue-components22.es.js +54 -28
  39. package/dist/vue-components23.es.js +4 -6
  40. package/dist/vue-components24.es.js +43 -8
  41. package/dist/vue-components25.es.js +4 -37
  42. package/dist/vue-components26.es.js +83 -24
  43. package/dist/vue-components28.es.js +6 -22
  44. package/dist/vue-components29.es.js +8 -20
  45. package/dist/vue-components3.es.js +2 -2
  46. package/dist/vue-components30.es.js +267 -7
  47. package/dist/vue-components32.es.js +7 -4
  48. package/dist/vue-components33.es.js +71 -27
  49. package/dist/vue-components34.es.js +4 -4
  50. package/dist/vue-components35.es.js +50 -27
  51. package/dist/vue-components36.es.js +4 -5
  52. package/dist/vue-components37.es.js +23 -17
  53. package/dist/vue-components38.es.js +4 -55
  54. package/dist/vue-components39.es.js +57 -3
  55. package/dist/vue-components40.es.js +4 -43
  56. package/dist/vue-components41.es.js +11 -4
  57. package/dist/vue-components42.es.js +17 -79
  58. package/dist/vue-components44.es.js +8 -7
  59. package/dist/vue-components45.es.js +3 -8
  60. package/dist/vue-components46.es.js +36 -267
  61. package/dist/vue-components47.es.js +27 -0
  62. package/dist/vue-components48.es.js +27 -7
  63. package/dist/vue-components49.es.js +6 -79
  64. package/dist/vue-components50.es.js +17 -4
  65. package/dist/vue-components51.es.js +32 -69
  66. package/dist/vue-components52.es.js +17 -4
  67. package/dist/vue-components53.es.js +19 -22
  68. package/dist/vue-components54.es.js +29 -4
  69. package/dist/vue-components55.es.js +6 -58
  70. package/dist/vue-components56.es.js +8 -4
  71. package/dist/vue-components57.es.js +37 -11
  72. package/dist/vue-components58.es.js +24 -21
  73. package/dist/{vue-components27.es.js → vue-components59.es.js} +2 -2
  74. package/dist/vue-components6.es.js +11 -11
  75. package/dist/vue-components60.es.js +23 -8
  76. package/dist/vue-components61.es.js +18 -232
  77. package/dist/vue-components62.es.js +7 -31
  78. package/dist/vue-components63.es.js +19 -8
  79. package/dist/vue-components64.es.js +4 -35
  80. package/dist/vue-components65.es.js +29 -0
  81. package/dist/vue-components66.es.js +5 -0
  82. package/dist/vue-components67.es.js +29 -0
  83. package/dist/vue-components68.es.js +6 -0
  84. package/dist/vue-components69.es.js +18 -0
  85. package/dist/vue-components7.es.js +11 -26
  86. package/dist/vue-components70.es.js +40 -0
  87. package/dist/vue-components71.es.js +81 -0
  88. package/dist/vue-components72.es.js +33 -0
  89. package/dist/vue-components73.es.js +19 -0
  90. package/dist/vue-components74.es.js +48 -0
  91. package/dist/vue-components8.es.js +33 -45
  92. package/dist/vue-components9.es.js +46 -4
  93. package/package.json +7 -7
  94. package/src/components/CommandButton.vue +3 -1
  95. package/src/components/OmegaForm/OmegaArray.vue +1 -1
  96. package/src/components/OmegaForm/OmegaAutoGen.vue +2 -1
  97. package/src/components/OmegaForm/OmegaErrorsInternal.vue +1 -1
  98. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  99. package/src/components/OmegaForm/OmegaInput.vue +6 -68
  100. package/src/components/OmegaForm/OmegaInputVuetify.vue +1 -1
  101. package/src/components/OmegaForm/OmegaInternalInput.vue +5 -11
  102. package/src/components/OmegaForm/OmegaTaggedUnion.vue +2 -1
  103. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  104. package/src/components/OmegaForm/blockDialog.ts +10 -1
  105. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +2 -1
  106. package/src/components/OmegaForm/errors.ts +136 -0
  107. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  108. package/src/components/OmegaForm/hocs.ts +19 -0
  109. package/src/components/OmegaForm/index.ts +16 -4
  110. package/src/components/OmegaForm/inputs.ts +22 -0
  111. package/src/components/OmegaForm/meta/checks.ts +81 -0
  112. package/src/components/OmegaForm/meta/createMeta.ts +138 -0
  113. package/src/components/OmegaForm/meta/defaults.ts +132 -0
  114. package/src/components/OmegaForm/meta/redacted.ts +66 -0
  115. package/src/components/OmegaForm/meta/types.ts +78 -0
  116. package/src/components/OmegaForm/meta/walker.ts +247 -0
  117. package/src/components/OmegaForm/persistency.ts +247 -0
  118. package/src/components/OmegaForm/submit.ts +128 -0
  119. package/src/components/OmegaForm/types.ts +751 -0
  120. package/src/components/OmegaForm/useOmegaForm.ts +49 -913
  121. package/src/components/OmegaForm/validation/localized.ts +202 -0
  122. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -173
  123. package/dist/vue-components31.es.js +0 -19
  124. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1422
@@ -1,1422 +0,0 @@
1
- import { Effect, Option, type Record, S } from "effect-app"
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { type DeepKeys, type DeepValue, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type FormApi, type FormAsyncValidateOrFn, type FormOptions, type FormState, type FormValidateOrFn, type StandardSchemaV1, type VueFormApi } from "@tanstack/vue-form"
4
- import { isObject } from "@vueuse/core"
5
- import type { Fiber as EffectFiber } from "effect/Fiber"
6
- import type { Redacted } from "effect/Redacted"
7
- import { getTransformationFrom, useIntl } from "../../utils"
8
- import { type OmegaFieldInternalApi } from "./InputProps"
9
- import { type OF, type OmegaFormReturn } from "./useOmegaForm"
10
-
11
- const legacyTagWarningEmittedFor = new Set<string>()
12
- type GlobalThisWithOptionalProcess = typeof globalThis & {
13
- process?: {
14
- env?: {
15
- NODE_ENV?: string
16
- }
17
- }
18
- }
19
-
20
- const isDevelopmentEnvironment = () => {
21
- const process = (globalThis as GlobalThisWithOptionalProcess).process
22
- return process?.env?.NODE_ENV !== "production"
23
- }
24
-
25
- export type FieldPath<T> = unknown extends T ? string
26
- // technically we cannot have primitive at the root
27
- : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? ""
28
- // technically we cannot have array at the root
29
- : T extends ReadonlyArray<infer U> ? FieldPath_<U, `[${number}]`>
30
- : {
31
- [K in keyof T]: FieldPath_<T[K], `${K & string}`>
32
- }[keyof T]
33
-
34
- export type FieldPath_<T, Path extends string> = unknown extends T ? string
35
- : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? Path
36
- : T extends ReadonlyArray<infer U> ? FieldPath_<U, `${Path}[${number}]`> | Path
37
- : {
38
- [K in keyof T]: FieldPath_<T[K], `${Path}.${K & string}`>
39
- }[keyof T]
40
-
41
- export type BaseProps<From, TName extends FieldPath<From>> = {
42
- /**
43
- * Will fallback to i18n when not specified.
44
- * Can also be provided via #label slot for custom HTML labels.
45
- * When using the slot, it receives bindings: { required, id, label }
46
- */
47
- label?: string
48
- validators?: FieldValidators<From>
49
- // Use FlexibleArrayPath: if name contains [], just use TName; otherwise intersect with Leaves<From>
50
- name: TName
51
- /**
52
- * Optional class to apply to the input element.
53
- * - If a string is provided, it will be used instead of the general class
54
- * - If null is provided, no class will be applied (neither inputClass nor general class)
55
- * - If undefined (not provided), the general class will be used
56
- */
57
- inputClass?: string | null
58
- }
59
-
60
- export type TypesWithOptions = "radio" | "select" | "multiple" | "autocomplete" | "autocompletemultiple"
61
- export type DefaultTypeProps = {
62
- type?: TypeOverride
63
- options?: undefined
64
- } | {
65
- type?: TypesWithOptions
66
- // TODO: options should depend on `type`, but since there is auto-type, we can't currently enforce it.
67
- // hence we allow it also for type? (undefined) atm
68
- options?: {
69
- title: string
70
- value: unknown
71
- }[]
72
- }
73
-
74
- export type OmegaInputPropsBase<
75
- From extends Record<PropertyKey, any>,
76
- To extends Record<PropertyKey, any>,
77
- Name extends DeepKeys<From>
78
- > = {
79
- form: OF<From, To> & {
80
- meta: MetaRecord<From>
81
- i18nNamespace?: string
82
- }
83
- } & BaseProps<From, Name>
84
-
85
- export type OmegaInputProps<
86
- From extends Record<PropertyKey, any>,
87
- To extends Record<PropertyKey, any>,
88
- Name extends DeepKeys<From>,
89
- TypeProps = DefaultTypeProps
90
- > = {
91
- form: OmegaFormReturn<From, To, TypeProps> & {
92
- meta: MetaRecord<From>
93
- i18nNamespace?: string
94
- }
95
- } & BaseProps<From, Name>
96
-
97
- export type OmegaArrayProps<
98
- From extends Record<PropertyKey, any>,
99
- To extends Record<PropertyKey, any>,
100
- Name extends DeepKeys<From>
101
- > =
102
- & Omit<
103
- OmegaInputProps<From, To, Name>,
104
- "validators" | "options" | "label" | "type" | "items" | "name"
105
- >
106
- & {
107
- name: DeepKeys<From>
108
- defaultItems?: DeepValue<From, DeepKeys<From>>
109
- // deprecated items, caused bugs in state update, use defaultItems instead. It's not a simple Never, because Volar explodes
110
- items?: "please use `defaultItems` instead"
111
- }
112
-
113
- export type TypeOverride =
114
- | "string"
115
- | "text"
116
- | "number"
117
- | "select"
118
- | "multiple"
119
- | "boolean"
120
- | "radio"
121
- | "autocomplete"
122
- | "autocompletemultiple"
123
- | "switch"
124
- | "range"
125
- | "password"
126
- | "email"
127
- | "date"
128
-
129
- export interface OmegaError {
130
- label: string
131
- inputId: string
132
- errors: readonly string[]
133
- }
134
-
135
- export type FormProps<From, To> =
136
- & Omit<
137
- FormOptions<
138
- From,
139
- FormValidateOrFn<From> | undefined,
140
- FormValidateOrFn<From> | undefined,
141
- StandardSchemaV1<From, To>,
142
- FormValidateOrFn<From> | undefined,
143
- FormAsyncValidateOrFn<From> | undefined,
144
- FormValidateOrFn<From> | undefined,
145
- FormAsyncValidateOrFn<From> | undefined,
146
- FormValidateOrFn<From> | undefined,
147
- FormAsyncValidateOrFn<From> | undefined,
148
- FormAsyncValidateOrFn<From> | undefined,
149
- Record<string, any> | undefined // TODO
150
- >,
151
- | "onSubmit"
152
- | "defaultValues"
153
- >
154
- & {
155
- // when defaultValues are allowed to be undefined, then they should also be allowed to be partial
156
- // this fixes validator issues where a defaultValue of "" leads to "requires at least 1 character", while manually emptying the field changes it to "is required"
157
- defaultValues?: Partial<From>
158
- onSubmit?: (props: {
159
- formApi: OmegaFormParams<From, To>
160
- meta: any
161
- value: To
162
- }) => Promise<any> | EffectFiber<any, any> | Effect.Effect<unknown, any, never>
163
- }
164
-
165
- export type OmegaFormParams<From, To> = FormApi<
166
- From,
167
- FormValidateOrFn<From> | undefined,
168
- FormValidateOrFn<From> | undefined,
169
- StandardSchemaV1<From, To>,
170
- FormValidateOrFn<From> | undefined,
171
- FormAsyncValidateOrFn<From> | undefined,
172
- FormValidateOrFn<From> | undefined,
173
- FormAsyncValidateOrFn<From> | undefined,
174
- FormValidateOrFn<From> | undefined,
175
- FormAsyncValidateOrFn<From> | undefined,
176
- FormAsyncValidateOrFn<From> | undefined,
177
- Record<string, any> | undefined
178
- >
179
-
180
- export type OmegaFormState<From, To> = FormState<
181
- From,
182
- FormValidateOrFn<From> | undefined,
183
- FormValidateOrFn<From> | undefined,
184
- StandardSchemaV1<From, To>,
185
- FormValidateOrFn<From> | undefined,
186
- FormAsyncValidateOrFn<From> | undefined,
187
- FormValidateOrFn<From> | undefined,
188
- FormAsyncValidateOrFn<From> | undefined,
189
- FormValidateOrFn<From> | undefined,
190
- FormAsyncValidateOrFn<From> | undefined,
191
- FormAsyncValidateOrFn<From> | undefined
192
- >
193
-
194
- // TODO: stitch TSubmitMeta somehow
195
- export type OmegaFormApi<From, To, TSubmitMeta = Record<string, any> | undefined> =
196
- & OmegaFormParams<From, To>
197
- & VueFormApi<
198
- From,
199
- FormValidateOrFn<From> | undefined,
200
- FormValidateOrFn<From> | undefined,
201
- StandardSchemaV1<From, To>,
202
- FormValidateOrFn<From> | undefined,
203
- FormAsyncValidateOrFn<From> | undefined,
204
- FormValidateOrFn<From> | undefined,
205
- FormAsyncValidateOrFn<From> | undefined,
206
- FormValidateOrFn<From> | undefined,
207
- FormAsyncValidateOrFn<From> | undefined,
208
- FormAsyncValidateOrFn<From> | undefined,
209
- TSubmitMeta
210
- >
211
-
212
- export type FormComponent<T, S> = VueFormApi<
213
- T,
214
- FormValidateOrFn<T> | undefined,
215
- FormValidateOrFn<T> | undefined,
216
- StandardSchemaV1<T, S>,
217
- FormValidateOrFn<T> | undefined,
218
- FormAsyncValidateOrFn<T> | undefined,
219
- FormValidateOrFn<T> | undefined,
220
- FormAsyncValidateOrFn<T> | undefined,
221
- FormValidateOrFn<T> | undefined,
222
- FormAsyncValidateOrFn<T> | undefined,
223
- FormAsyncValidateOrFn<T> | undefined,
224
- Record<string, any> | undefined
225
- >
226
-
227
- export type FormType<
228
- From extends Record<PropertyKey, any>,
229
- To extends Record<PropertyKey, any>,
230
- Name extends DeepKeys<From>
231
- > = OmegaFormApi<From, To> & {
232
- Field: OmegaFieldInternalApi<From, Name>
233
- }
234
-
235
- export type PrefixFromDepth<
236
- K extends string | number,
237
- _TDepth extends any[]
238
- > = K
239
-
240
- // Recursively replace Redacted<A> with its inner type so DeepKeys treats it as a leaf
241
- type StripRedacted<T> = T extends Redacted<any> ? string
242
- : T extends ReadonlyArray<infer U> ? ReadonlyArray<StripRedacted<U>>
243
- : T extends Record<string, any> ? { [K in keyof T]: StripRedacted<T[K]> }
244
- : T
245
-
246
- export type NestedKeyOf<T> = DeepKeys<StripRedacted<T>>
247
-
248
- export type FieldValidators<T> = {
249
- onChangeAsync?: FieldAsyncValidateOrFn<T, any, any>
250
- onChange?: FieldValidateOrFn<T, any, any>
251
- onBlur?: FieldValidateOrFn<T, any, any>
252
- onBlurAsync?: FieldAsyncValidateOrFn<T, any, any>
253
- }
254
-
255
- // Field metadata type definitions
256
- export type BaseFieldMeta = {
257
- required: boolean
258
- nullableOrUndefined?: false | "undefined" | "null"
259
- originalSchema?: StandardSchemaV1<any, any>
260
- /**
261
- * True when the schema property is `S.optionalKey` (AST
262
- * `context.isOptional`) — i.e. the key should be ABSENT from the submitted
263
- * object when empty, not present with `undefined`. Distinct from
264
- * `required: false`, which may also mean "empty string is valid" for
265
- * unconstrained `S.String` fields.
266
- */
267
- isOptionalKey?: boolean
268
- }
269
-
270
- export type StringFieldMeta = BaseFieldMeta & {
271
- type: "string"
272
- maxLength?: number
273
- minLength?: number
274
- format?: string
275
- }
276
-
277
- export type NumberFieldMeta = BaseFieldMeta & {
278
- type: "number"
279
- minimum?: number
280
- maximum?: number
281
- exclusiveMinimum?: number
282
- exclusiveMaximum?: number
283
- refinement?: "int"
284
- }
285
-
286
- export type SelectFieldMeta = BaseFieldMeta & {
287
- type: "select"
288
- members: any[] // TODO: should be non empty array?
289
- }
290
-
291
- export type MultipleFieldMeta = BaseFieldMeta & {
292
- type: "multiple"
293
- members: any[] // TODO: should be non empty array?
294
- rest: readonly S.AST.AST[]
295
- }
296
-
297
- export type BooleanFieldMeta = BaseFieldMeta & {
298
- type: "boolean"
299
- }
300
-
301
- export type DateFieldMeta = BaseFieldMeta & {
302
- type: "date"
303
- }
304
-
305
- export type UnknownFieldMeta = BaseFieldMeta & {
306
- type: "unknown"
307
- }
308
-
309
- export type FieldMeta =
310
- | StringFieldMeta
311
- | NumberFieldMeta
312
- | SelectFieldMeta
313
- | MultipleFieldMeta
314
- | BooleanFieldMeta
315
- | DateFieldMeta
316
- | UnknownFieldMeta
317
-
318
- export type MetaRecord<T = string> = {
319
- [K in NestedKeyOf<T>]?: FieldMeta
320
- }
321
-
322
- export type FilterItems = {
323
- items: readonly [string, ...string[]]
324
- message:
325
- | string
326
- | Effect.Effect<string, never, never>
327
- | { readonly message: string | Effect.Effect<string> }
328
- }
329
-
330
- export type CreateMeta =
331
- & {
332
- parent?: string
333
- meta?: Record<string, any>
334
- nullableOrUndefined?: false | "undefined" | "null"
335
- }
336
- & (
337
- | {
338
- propertySignatures: readonly S.AST.PropertySignature[]
339
- property?: never
340
- }
341
- | {
342
- propertySignatures?: never
343
- property: S.AST.AST
344
- }
345
- )
346
-
347
- const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
348
- let current = getTransformationFrom(property)
349
-
350
- while (S.AST.isDeclaration(current) && current.typeParameters.length > 0) {
351
- current = getTransformationFrom(current.typeParameters[0]!)
352
- }
353
-
354
- return current
355
- }
356
-
357
- const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
358
-
359
- /**
360
- * Unwrap a single-element Union to its inner type if it's a Literal.
361
- * After AST.toType, S.Struct({ _tag: S.Literal("X") }) produces Union([Literal("X")])
362
- * instead of bare Literal("X") like S.TaggedStruct does.
363
- * TODO: remove after manual _tag deprecation
364
- */
365
- const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
366
- S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0]!)
367
- ? ast.types[0]!
368
- : ast
369
-
370
- const getNullableOrUndefined = (property: S.AST.AST) =>
371
- S.AST.isUnion(property)
372
- ? property.types.find((_) => isNullishType(_))
373
- : false
374
-
375
- export const isNullableOrUndefined = (property: false | S.AST.AST | undefined) => {
376
- if (!property || !S.AST.isUnion(property)) return false
377
- if (property.types.find((_) => S.AST.isUndefined(_))) {
378
- return "undefined"
379
- }
380
- if (property.types.find((_) => S.AST.isNull(_))) return "null"
381
- return false
382
- }
383
-
384
- // Helper function to recursively unwrap nested unions (e.g., S.NullOr(S.NullOr(X)) -> X)
385
- const unwrapNestedUnions = (types: readonly S.AST.AST[]): readonly S.AST.AST[] => {
386
- const result: S.AST.AST[] = []
387
- for (const type of types) {
388
- if (S.AST.isUnion(type)) {
389
- // Recursively unwrap nested unions
390
- const unwrapped = unwrapNestedUnions(type.types)
391
- result.push(...unwrapped)
392
- } else {
393
- result.push(type)
394
- }
395
- }
396
- return result
397
- }
398
-
399
- const getNonNullTypes = (types: readonly S.AST.AST[]) =>
400
- unwrapNestedUnions(types)
401
- .map(unwrapDeclaration)
402
- .filter((_) => !isNullishType(_))
403
-
404
- const getJsonSchemaAnnotation = (property: S.AST.AST): Record<string, unknown> => {
405
- const jsonSchema = S.AST.resolve(property)?.jsonSchema
406
- return jsonSchema && typeof jsonSchema === "object" ? jsonSchema as Record<string, unknown> : {}
407
- }
408
-
409
- const extractDefaultFromLink = (link: any): unknown | undefined => {
410
- if (!link?.transformation?.decode?.run) return undefined
411
- try {
412
- const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
413
- return Option.isSome(result) ? result.value : undefined
414
- } catch {
415
- return undefined
416
- }
417
- }
418
-
419
- const getDefaultFromAst = (property: S.AST.AST) => {
420
- // 1. Check withConstructorDefault (stored in context.defaultValue)
421
- const constructorLink = property.context?.defaultValue?.[0]
422
- const constructorDefault = extractDefaultFromLink(constructorLink)
423
- if (constructorDefault !== undefined) return constructorDefault
424
-
425
- // 2. Check withDecodingDefault (stored in encoding)
426
- const encodingLink = property.encoding?.[0]
427
- if (encodingLink && property.context?.isOptional) {
428
- return extractDefaultFromLink(encodingLink)
429
- }
430
-
431
- return undefined
432
- }
433
-
434
- const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
435
- const checks = property.checks ?? []
436
-
437
- return checks.flatMap((check) => {
438
- if (check._tag === "FilterGroup") {
439
- return check.checks.flatMap((inner) => {
440
- const meta = inner.annotations?.meta
441
- return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
442
- })
443
- }
444
-
445
- const meta = check.annotations?.meta
446
- return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
447
- })
448
- }
449
-
450
- const getFieldMetadataFromAst = (property: S.AST.AST) => {
451
- const base: Partial<FieldMeta> & Record<string, unknown> = {
452
- description: S.AST.resolveDescription(property)
453
- }
454
- const checks = getCheckMetas(property)
455
-
456
- if (S.AST.isString(property)) {
457
- base.type = "string"
458
- for (const check of checks) {
459
- switch (check._tag) {
460
- case "isMinLength":
461
- base.minLength = check.minLength
462
- break
463
- case "isMaxLength":
464
- base.maxLength = check.maxLength
465
- break
466
- }
467
- }
468
-
469
- if (S.AST.resolveTitle(property) === "Email") {
470
- base.format = "email"
471
- }
472
- } else if (S.AST.isNumber(property)) {
473
- base.type = "number"
474
- for (const check of checks) {
475
- switch (check._tag) {
476
- case "isInt":
477
- base.refinement = "int"
478
- break
479
- case "isGreaterThanOrEqualTo":
480
- base.minimum = check.minimum
481
- break
482
- case "isLessThanOrEqualTo":
483
- base.maximum = check.maximum
484
- break
485
- case "isBetween":
486
- base.minimum = check.minimum
487
- base.maximum = check.maximum
488
- break
489
- case "isGreaterThan":
490
- base.exclusiveMinimum = check.exclusiveMinimum
491
- break
492
- case "isLessThan":
493
- base.exclusiveMaximum = check.exclusiveMaximum
494
- break
495
- }
496
- }
497
- } else if (S.AST.isBoolean(property)) {
498
- base.type = "boolean"
499
- } else if (
500
- S.AST.isDeclaration(property)
501
- && (property.annotations as any)?.typeConstructor?._tag === "Date"
502
- ) {
503
- base.type = "date"
504
- } else {
505
- base.type = "unknown"
506
- }
507
-
508
- return base
509
- }
510
-
511
- export const createMeta = <T = any>(
512
- { meta = {}, parent = "", property, propertySignatures }: CreateMeta,
513
- acc: Partial<MetaRecord<T>> = {},
514
- fieldAstByPath?: Record<string, S.AST.AST>
515
- ): MetaRecord<T> | FieldMeta => {
516
- if (property) {
517
- property = unwrapDeclaration(property)
518
- }
519
-
520
- if (property && S.AST.isObjects(property)) {
521
- return createMeta<T>({
522
- meta,
523
- propertySignatures: property.propertySignatures
524
- })
525
- }
526
-
527
- if (propertySignatures) {
528
- for (const p of propertySignatures) {
529
- const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
530
- const nullableOrUndefined = isNullableOrUndefined(p.type)
531
-
532
- const isOptionalKey = (p.type as any).context?.isOptional === true
533
-
534
- // Determine if this field should be required:
535
- // - For nullable discriminated unions, only _tag should be non-required
536
- // - optionalKey fields are not required
537
- // - All other fields should calculate their required status normally
538
- let isRequired: boolean
539
- if (meta._isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
540
- // _tag in a nullable discriminated union is not required
541
- isRequired = false
542
- } else if (meta.required === false) {
543
- // Explicitly set to non-required (legacy behavior for backwards compatibility)
544
- isRequired = false
545
- } else if (isOptionalKey) {
546
- isRequired = false
547
- } else {
548
- // Calculate from the property itself
549
- isRequired = !nullableOrUndefined
550
- }
551
-
552
- const typeToProcess = unwrapDeclaration(p.type)
553
- if (S.AST.isUnion(p.type)) {
554
- const nonNullTypes = getNonNullTypes(p.type.types)
555
-
556
- const hasStructMembers = nonNullTypes.some(S.AST.isObjects)
557
-
558
- if (hasStructMembers) {
559
- // Only create parent meta for non-NullOr unions to avoid duplicates
560
- if (!nullableOrUndefined) {
561
- const parentMeta = createMeta<T>({
562
- parent: key,
563
- property: p.type,
564
- meta: { required: isRequired, nullableOrUndefined }
565
- })
566
- acc[key as NestedKeyOf<T>] = parentMeta as FieldMeta
567
- }
568
-
569
- // Process each non-null type and merge their metadata
570
- for (const nonNullType of nonNullTypes) {
571
- if (S.AST.isObjects(nonNullType)) {
572
- // For discriminated unions (multiple branches):
573
- // - If the parent union is nullable, only _tag should be non-required
574
- // - All other fields maintain their normal required status based on their own types
575
- const isNullableDiscriminatedUnion = nullableOrUndefined && nonNullTypes.length > 1
576
-
577
- const branchMeta = createMeta<T>({
578
- parent: key,
579
- propertySignatures: nonNullType.propertySignatures,
580
- meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
581
- })
582
-
583
- // Merge branch metadata, combining select members for shared discriminator fields
584
- for (const [metaKey, metaValue] of Object.entries(branchMeta)) {
585
- const existing = acc[metaKey as NestedKeyOf<T>] as FieldMeta | undefined
586
- if (
587
- existing && existing.type === "select" && (metaValue as any)?.type === "select"
588
- ) {
589
- existing.members = [
590
- ...existing.members,
591
- ...(metaValue as SelectFieldMeta).members.filter(
592
- (m: any) => !existing.members.includes(m)
593
- )
594
- ]
595
- } else {
596
- acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
597
- }
598
- }
599
- }
600
- }
601
- } else {
602
- const arrayTypes = nonNullTypes.filter(S.AST.isArrays)
603
- if (arrayTypes.length > 0) {
604
- const arrayType = arrayTypes[0] // Take the first array type
605
-
606
- acc[key as NestedKeyOf<T>] = {
607
- type: "multiple",
608
- members: arrayType.elements,
609
- rest: arrayType.rest,
610
- required: isRequired,
611
- nullableOrUndefined
612
- } as FieldMeta
613
- if (fieldAstByPath) {
614
- fieldAstByPath[key] = p.type
615
- }
616
-
617
- // If the array has struct elements, also create metadata for their properties
618
- if (arrayType.rest && arrayType.rest.length > 0) {
619
- const restElement = unwrapDeclaration(arrayType.rest[0]!)
620
- if (S.AST.isObjects(restElement)) {
621
- for (const prop of restElement.propertySignatures) {
622
- const propKey = `${key}.${prop.name.toString()}`
623
-
624
- const propMeta = createMeta<T>({
625
- parent: propKey,
626
- property: prop.type,
627
- meta: {
628
- required: !isNullableOrUndefined(prop.type),
629
- nullableOrUndefined: isNullableOrUndefined(prop.type)
630
- }
631
- })
632
-
633
- // add to accumulator if valid
634
- if (propMeta && typeof propMeta === "object" && "type" in propMeta) {
635
- acc[propKey as NestedKeyOf<T>] = propMeta as FieldMeta
636
- if (fieldAstByPath) {
637
- fieldAstByPath[propKey] = prop.type
638
- }
639
-
640
- if (
641
- propMeta.type === "multiple" && S.AST.isArrays(prop.type) && prop
642
- .type
643
- .rest && prop.type.rest.length > 0
644
- ) {
645
- const nestedRestElement = unwrapDeclaration(prop.type.rest[0]!)
646
- if (S.AST.isObjects(nestedRestElement)) {
647
- for (const nestedProp of nestedRestElement.propertySignatures) {
648
- const nestedPropKey = `${propKey}.${nestedProp.name.toString()}`
649
-
650
- const nestedPropMeta = createMeta<T>({
651
- parent: nestedPropKey,
652
- property: nestedProp.type,
653
- meta: {
654
- required: !isNullableOrUndefined(nestedProp.type),
655
- nullableOrUndefined: isNullableOrUndefined(nestedProp.type)
656
- }
657
- })
658
-
659
- // add to accumulator if valid
660
- if (nestedPropMeta && typeof nestedPropMeta === "object" && "type" in nestedPropMeta) {
661
- acc[nestedPropKey as NestedKeyOf<T>] = nestedPropMeta as FieldMeta
662
- if (fieldAstByPath) {
663
- fieldAstByPath[nestedPropKey] = nestedProp.type
664
- }
665
- }
666
- }
667
- }
668
- }
669
- }
670
- }
671
- }
672
- }
673
- } else {
674
- // If no struct members and no arrays, process as regular union
675
- const newMeta = createMeta<T>({
676
- parent: key,
677
- property: p.type,
678
- meta: { required: isRequired, nullableOrUndefined }
679
- })
680
- acc[key as NestedKeyOf<T>] = newMeta as FieldMeta
681
- if (fieldAstByPath) {
682
- fieldAstByPath[key] = p.type
683
- }
684
- }
685
- }
686
- } else {
687
- if (S.AST.isObjects(typeToProcess)) {
688
- Object.assign(
689
- acc,
690
- createMeta<T>(
691
- {
692
- parent: key,
693
- propertySignatures: typeToProcess.propertySignatures,
694
- meta: { required: isRequired, nullableOrUndefined }
695
- },
696
- {},
697
- fieldAstByPath
698
- )
699
- )
700
- } else if (S.AST.isArrays(p.type)) {
701
- // Check if it has struct elements
702
- const hasStructElements = p.type.rest.length > 0
703
- && S.AST.isObjects(unwrapDeclaration(p.type.rest[0]!))
704
-
705
- if (hasStructElements) {
706
- // For arrays with struct elements, only create meta for nested fields, not the array itself
707
- const elementType = unwrapDeclaration(p.type.rest[0]!)
708
- if (S.AST.isObjects(elementType)) {
709
- // Process each property in the array element
710
- for (const prop of elementType.propertySignatures) {
711
- const propKey = `${key}.${prop.name.toString()}`
712
-
713
- // Check if the property is another array
714
- if (S.AST.isArrays(prop.type) && prop.type.rest.length > 0) {
715
- const nestedElementType = unwrapDeclaration(prop.type.rest[0]!)
716
- if (S.AST.isObjects(nestedElementType)) {
717
- // Array with struct elements - process nested fields
718
- for (const nestedProp of nestedElementType.propertySignatures) {
719
- const nestedKey = `${propKey}.${nestedProp.name.toString()}`
720
- const nestedMeta = createMeta<T>({
721
- parent: nestedKey,
722
- property: nestedProp.type,
723
- meta: {
724
- required: !isNullableOrUndefined(nestedProp.type),
725
- nullableOrUndefined: isNullableOrUndefined(nestedProp.type)
726
- }
727
- })
728
- acc[nestedKey as NestedKeyOf<T>] = nestedMeta as FieldMeta
729
- if (fieldAstByPath) {
730
- fieldAstByPath[nestedKey] = nestedProp.type
731
- }
732
- }
733
- } else {
734
- // Array with primitive elements - create meta for the array itself
735
- acc[propKey as NestedKeyOf<T>] = {
736
- type: "multiple",
737
- members: prop.type.elements,
738
- rest: prop.type.rest,
739
- required: !isNullableOrUndefined(prop.type),
740
- nullableOrUndefined: isNullableOrUndefined(prop.type)
741
- } as FieldMeta
742
- if (fieldAstByPath) {
743
- fieldAstByPath[propKey] = prop.type
744
- }
745
- }
746
- } else {
747
- const fieldMeta = createMeta<T>({
748
- parent: propKey,
749
- property: prop.type,
750
- meta: {
751
- required: !isNullableOrUndefined(prop.type),
752
- nullableOrUndefined: isNullableOrUndefined(prop.type)
753
- }
754
- })
755
- acc[propKey as NestedKeyOf<T>] = fieldMeta as FieldMeta
756
- if (fieldAstByPath) {
757
- fieldAstByPath[propKey] = prop.type
758
- }
759
- }
760
- }
761
- }
762
- } else {
763
- // For arrays with primitive elements, create the array meta
764
- acc[key as NestedKeyOf<T>] = {
765
- type: "multiple",
766
- members: p.type.elements,
767
- rest: p.type.rest,
768
- required: isRequired,
769
- nullableOrUndefined
770
- } as FieldMeta
771
- if (fieldAstByPath) {
772
- fieldAstByPath[key] = p.type
773
- }
774
- }
775
- } else {
776
- const newMeta = createMeta<T>({
777
- parent: key,
778
- property: p.type,
779
- meta: {
780
- // an empty string is valid for a S.String field, so we should not mark it as required
781
- // TODO: handle this better via the createMeta minLength parsing
782
- required: isRequired
783
- && (!S.AST.isString(typeToProcess) || !!getFieldMetadataFromAst(typeToProcess).minLength),
784
- nullableOrUndefined,
785
- ...(isOptionalKey ? { isOptionalKey: true } : {})
786
- }
787
- })
788
-
789
- acc[key as NestedKeyOf<T>] = newMeta as FieldMeta
790
- if (fieldAstByPath) {
791
- fieldAstByPath[key] = p.type
792
- }
793
- }
794
- }
795
- }
796
- return acc
797
- }
798
-
799
- if (property) {
800
- const nullableOrUndefined = getNullableOrUndefined(property)
801
- property = unwrapDeclaration(property)
802
-
803
- if (!Object.hasOwnProperty.call(meta, "required")) {
804
- meta["required"] = !nullableOrUndefined
805
- }
806
-
807
- if (S.AST.isUnion(property)) {
808
- const unwrappedTypes = unwrapNestedUnions(property.types).map(unwrapDeclaration)
809
- const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
810
-
811
- // Unwrap single-element unions when the literal is a boolean
812
- // (effect-app's S.Literal wraps as S.Literals([x]) → Union([Literal(x)]))
813
- // Don't unwrap string/number literals — they may be discriminator values in a union
814
- if (
815
- nonNullTypes.length === 1
816
- && S.AST.isLiteral(nonNullTypes[0]!)
817
- && typeof nonNullTypes[0]!.literal === "boolean"
818
- ) {
819
- return createMeta<T>({ parent, meta, property: nonNullTypes[0]! })
820
- }
821
-
822
- const nonNullType = nonNullTypes[0]!
823
-
824
- if (S.AST.isObjects(nonNullType)) {
825
- return createMeta<T>({
826
- propertySignatures: nonNullType.propertySignatures,
827
- parent,
828
- meta
829
- })
830
- }
831
-
832
- // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
833
- const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
834
- if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
835
- return {
836
- ...meta,
837
- type: "select",
838
- members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
839
- } as FieldMeta
840
- }
841
-
842
- return {
843
- ...meta,
844
- ...createMeta<T>({
845
- parent,
846
- meta,
847
- property: nonNullType
848
- })
849
- } as FieldMeta
850
- }
851
-
852
- if (S.AST.isArrays(property)) {
853
- return {
854
- ...meta,
855
- type: "multiple",
856
- members: property.elements,
857
- rest: property.rest
858
- } as FieldMeta
859
- }
860
-
861
- if (S.AST.isLiteral(property)) {
862
- return {
863
- ...meta,
864
- type: "select",
865
- members: [property.literal]
866
- } as FieldMeta
867
- }
868
-
869
- meta = { ...getJsonSchemaAnnotation(property), ...getFieldMetadataFromAst(property), ...meta }
870
-
871
- return meta as FieldMeta
872
- }
873
-
874
- return acc
875
- }
876
-
877
- // Helper to flatten nested meta structure into dot-notation keys
878
- const flattenMeta = <T>(meta: MetaRecord<T> | FieldMeta, parentKey: string = ""): MetaRecord<T> => {
879
- const result: MetaRecord<T> = {}
880
-
881
- for (const key in meta) {
882
- const value = (meta as any)[key]
883
- const newKey = parentKey ? `${parentKey}.${key}` : key
884
-
885
- if (value && typeof value === "object" && "type" in value) {
886
- result[newKey as DeepKeys<T>] = value as FieldMeta
887
- } else if (value && typeof value === "object") {
888
- Object.assign(result, flattenMeta<T>(value, newKey))
889
- }
890
- }
891
-
892
- return result
893
- }
894
-
895
- const metadataFromAst = <From, To>(
896
- schema: S.Codec<To, From, never>
897
- ): {
898
- meta: MetaRecord<To>
899
- defaultValues: Record<string, any>
900
- unionMeta: Record<string, MetaRecord<To>>
901
- } => {
902
- const ast = unwrapDeclaration(schema.ast)
903
- const newMeta: MetaRecord<To> = {}
904
- const defaultValues: Record<string, any> = {}
905
- const unionMeta: Record<string, MetaRecord<To>> = {}
906
- const fieldAstByPath: Record<string, S.AST.AST> = {}
907
-
908
- const toFieldStandardSchema = (
909
- propertyAst: S.AST.AST,
910
- required: boolean
911
- ): StandardSchemaV1<any, any> => {
912
- const base = S.make(propertyAst)
913
- const fieldSchema = required ? base : S.NullishOr(base)
914
- return S.toStandardSchemaV1(fieldSchema as any)
915
- }
916
-
917
- const attachOriginalSchemas = (metaRecord: MetaRecord<To>) => {
918
- for (const [key, fieldAst] of Object.entries(fieldAstByPath)) {
919
- const fieldMeta = metaRecord[key as NestedKeyOf<To>]
920
- if (!fieldMeta) {
921
- continue
922
- }
923
- try {
924
- const required = fieldMeta.required ?? true
925
- Object.defineProperty(fieldMeta, "originalSchema", {
926
- value: toFieldStandardSchema(fieldAst, required),
927
- enumerable: false,
928
- configurable: true,
929
- writable: true
930
- })
931
- } catch {
932
- Object.defineProperty(fieldMeta, "originalSchema", {
933
- value: S.toStandardSchemaV1(S.Unknown),
934
- enumerable: false,
935
- configurable: true,
936
- writable: true
937
- })
938
- }
939
- }
940
- }
941
-
942
- // Handle root-level Union types (discriminated unions)
943
- if (S.AST.isUnion(ast)) {
944
- // Filter out null/undefined types and unwrap transformations
945
- const nonNullTypes = getNonNullTypes(ast.types)
946
-
947
- // Check if this is a discriminated union (all members are structs)
948
- const allStructs = nonNullTypes.every(S.AST.isObjects)
949
-
950
- if (allStructs && nonNullTypes.length > 0) {
951
- // Extract discriminator values from each union member
952
- const discriminatorValues: any[] = []
953
-
954
- // Store metadata for each union member by its tag value
955
- for (const memberType of nonNullTypes) {
956
- if (S.AST.isObjects(memberType)) {
957
- // Find the discriminator field (usually _tag)
958
- const tagProp = memberType.propertySignatures.find(
959
- (p) => p.name.toString() === "_tag"
960
- )
961
-
962
- let tagValue: string | null = null
963
- // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
964
- const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
965
- if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
966
- tagValue = resolvedTagType.literal as string
967
- discriminatorValues.push(tagValue)
968
- // Warn if the tag was wrapped in a single-element Union (legacy pattern)
969
- if (
970
- tagProp
971
- && S.AST.isUnion(tagProp.type)
972
- && isDevelopmentEnvironment()
973
- && tagValue != null
974
- && !legacyTagWarningEmittedFor.has(tagValue)
975
- ) {
976
- legacyTagWarningEmittedFor.add(tagValue)
977
- console.warn(
978
- `[OmegaForm] Union member with _tag "${tagValue}" uses S.Struct({ _tag: S.Literal("${tagValue}"), ... }). `
979
- + `Please migrate to S.TaggedStruct("${tagValue}", { ... }) for cleaner AST handling.`
980
- )
981
- }
982
- }
983
-
984
- // Create metadata for this member's properties
985
- const memberMeta = createMeta<To>(
986
- {
987
- propertySignatures: memberType.propertySignatures
988
- },
989
- {},
990
- fieldAstByPath
991
- )
992
-
993
- // Store per-tag metadata for reactive lookup
994
- if (tagValue) {
995
- unionMeta[tagValue] = flattenMeta<To>(memberMeta)
996
- }
997
-
998
- // Merge into result (for backward compatibility)
999
- Object.assign(newMeta, memberMeta)
1000
- }
1001
- }
1002
-
1003
- // Create metadata for the discriminator field
1004
- if (discriminatorValues.length > 0) {
1005
- newMeta["_tag" as DeepKeys<To>] = {
1006
- type: "select",
1007
- members: discriminatorValues,
1008
- required: true
1009
- } as FieldMeta
1010
- }
1011
-
1012
- attachOriginalSchemas(newMeta)
1013
- return { meta: newMeta, defaultValues, unionMeta }
1014
- }
1015
- }
1016
-
1017
- if (S.AST.isObjects(ast)) {
1018
- const meta = createMeta<To>(
1019
- {
1020
- propertySignatures: ast.propertySignatures
1021
- },
1022
- {},
1023
- fieldAstByPath
1024
- )
1025
-
1026
- if (Object.values(meta).every((value) => value && "type" in value)) {
1027
- const typedMeta = meta as MetaRecord<To>
1028
- attachOriginalSchemas(typedMeta)
1029
- return {
1030
- meta: typedMeta,
1031
- defaultValues,
1032
- unionMeta
1033
- }
1034
- }
1035
-
1036
- const flattenObject = (
1037
- obj: Record<string, any>,
1038
- parentKey: string = ""
1039
- ) => {
1040
- for (const key in obj) {
1041
- const newKey = parentKey ? `${parentKey}.${key}` : key
1042
- if (obj[key] && typeof obj[key] === "object" && "type" in obj[key]) {
1043
- newMeta[newKey as DeepKeys<To>] = obj[key] as FieldMeta
1044
- } else if (obj[key] && typeof obj[key] === "object") {
1045
- flattenObject(obj[key], newKey)
1046
- }
1047
- }
1048
- }
1049
-
1050
- flattenObject(meta)
1051
- }
1052
-
1053
- attachOriginalSchemas(newMeta)
1054
- return { meta: newMeta, defaultValues, unionMeta }
1055
- }
1056
-
1057
- /*
1058
- * Checks if an AST node is a S.Redacted Declaration without encoding.
1059
- * These need to be swapped to S.RedactedFromValue for form usage
1060
- * because S.Redacted expects Redacted objects, not plain strings.
1061
- */
1062
- const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
1063
- S.AST.isDeclaration(ast)
1064
- && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
1065
- && !ast.encoding
1066
-
1067
- /*
1068
- * Creates a form-compatible schema by replacing S.Redacted(X) with
1069
- * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
1070
- * Redacted<A> on both encoded and type sides, so form inputs (which
1071
- * produce plain strings) fail validation. S.RedactedFromValue accepts
1072
- * plain values on the encoded side and wraps them in Redacted on decode.
1073
- */
1074
- export const toFormSchema = <From, To>(
1075
- schema: S.Codec<To, From, never>
1076
- ): S.Codec<To, From, never> => {
1077
- const ast = schema.ast
1078
- const objAst = S.AST.isObjects(ast)
1079
- ? ast
1080
- : S.AST.isDeclaration(ast)
1081
- ? S.AST.toEncoded(ast)
1082
- : null
1083
-
1084
- if (!objAst || !("propertySignatures" in objAst)) return schema
1085
-
1086
- let hasRedacted = false
1087
- const props: Record<string, S.Struct.Fields[string]> = {}
1088
-
1089
- for (const p of objAst.propertySignatures) {
1090
- if (isRedactedWithoutEncoding(p.type)) {
1091
- hasRedacted = true
1092
- const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
1093
- props[p.name as string] = S.RedactedFromValue(innerSchema)
1094
- } else if (S.AST.isUnion(p.type)) {
1095
- const types = p.type.types
1096
- const redactedType = types.find(isRedactedWithoutEncoding)
1097
- if (redactedType) {
1098
- hasRedacted = true
1099
- const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
1100
- const hasNull = types.some(S.AST.isNull)
1101
- const hasUndefined = types.some(S.AST.isUndefined)
1102
- const base = S.RedactedFromValue(innerSchema)
1103
- props[p.name as string] = hasNull && hasUndefined
1104
- ? S.NullishOr(base)
1105
- : hasNull
1106
- ? S.NullOr(base)
1107
- : hasUndefined
1108
- ? S.UndefinedOr(base)
1109
- : base
1110
- } else {
1111
- props[p.name as string] = S.make(p.type)
1112
- }
1113
- } else {
1114
- props[p.name as string] = S.make(p.type)
1115
- }
1116
- }
1117
-
1118
- return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
1119
- }
1120
-
1121
- export const duplicateSchema = <From, To>(
1122
- schema: S.Codec<To, From, never>
1123
- ) => {
1124
- return schema
1125
- }
1126
-
1127
- export const generateMetaFromSchema = <From, To>(
1128
- schema: S.Codec<To, From, never>
1129
- ): {
1130
- schema: S.Codec<To, From, never>
1131
- meta: MetaRecord<To>
1132
- unionMeta: Record<string, MetaRecord<To>>
1133
- } => {
1134
- const { meta, unionMeta } = metadataFromAst(schema)
1135
-
1136
- return { schema, meta, unionMeta }
1137
- }
1138
-
1139
- export const generateInputStandardSchemaFromFieldMeta = (
1140
- meta: FieldMeta,
1141
- trans?: ReturnType<typeof useIntl>["trans"]
1142
- ): StandardSchemaV1<any, any> => {
1143
- if (!trans) {
1144
- trans = useIntl().trans
1145
- }
1146
- let schema: any
1147
- switch (meta.type) {
1148
- case "string":
1149
- schema = meta.format === "email"
1150
- ? S.Email.annotate({
1151
- message: trans("validation.email.invalid")
1152
- })
1153
- : S.String.annotate({
1154
- message: trans("validation.empty")
1155
- })
1156
-
1157
- if (meta.required) {
1158
- schema = schema.check(S.isMinLength(1, {
1159
- message: trans("validation.empty")
1160
- }))
1161
- }
1162
-
1163
- if (typeof meta.maxLength === "number") {
1164
- schema = schema.check(S.isMaxLength(meta.maxLength, {
1165
- message: trans("validation.string.maxLength", {
1166
- maxLength: meta.maxLength
1167
- })
1168
- }))
1169
- }
1170
- if (typeof meta.minLength === "number") {
1171
- schema = schema.check(S.isMinLength(meta.minLength, {
1172
- message: trans("validation.string.minLength", {
1173
- minLength: meta.minLength
1174
- })
1175
- }))
1176
- }
1177
- break
1178
-
1179
- case "number":
1180
- if (meta.refinement === "int") {
1181
- schema = S
1182
- .Number
1183
- .annotate({
1184
- message: trans("validation.empty")
1185
- })
1186
- .check(S.isInt({
1187
- message: trans("validation.integer.expected", { actualValue: "NaN" })
1188
- }))
1189
- } else {
1190
- schema = S.Finite.annotate({
1191
- message: trans("validation.number.expected", { actualValue: "NaN" })
1192
- })
1193
-
1194
- if (meta.required) {
1195
- schema = schema.annotate({
1196
- message: trans("validation.empty")
1197
- })
1198
- }
1199
- }
1200
-
1201
- if (typeof meta.minimum === "number") {
1202
- schema = schema.check(S.isGreaterThanOrEqualTo(meta.minimum, {
1203
- message: trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
1204
- minimum: meta.minimum,
1205
- isExclusive: true
1206
- })
1207
- }))
1208
- }
1209
- if (typeof meta.maximum === "number") {
1210
- schema = schema.check(S.isLessThanOrEqualTo(meta.maximum, {
1211
- message: trans("validation.number.max", {
1212
- maximum: meta.maximum,
1213
- isExclusive: true
1214
- })
1215
- }))
1216
- }
1217
- if (typeof meta.exclusiveMinimum === "number") {
1218
- schema = schema.check(S.isGreaterThan(meta.exclusiveMinimum, {
1219
- message: trans(meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min", {
1220
- minimum: meta.exclusiveMinimum,
1221
- isExclusive: false
1222
- })
1223
- }))
1224
- }
1225
- if (typeof meta.exclusiveMaximum === "number") {
1226
- schema = schema.check(S.isLessThan(meta.exclusiveMaximum, {
1227
- message: trans("validation.number.max", {
1228
- maximum: meta.exclusiveMaximum,
1229
- isExclusive: false
1230
- })
1231
- }))
1232
- }
1233
- break
1234
- case "select":
1235
- schema = S.Literals(meta.members as [any, ...any[]]).annotate({
1236
- message: trans("validation.not_a_valid", {
1237
- type: "select",
1238
- message: meta.members.join(", ")
1239
- })
1240
- })
1241
-
1242
- break
1243
-
1244
- case "multiple":
1245
- schema = S.Array(S.String).annotate({
1246
- message: trans("validation.not_a_valid", {
1247
- type: "multiple",
1248
- message: meta.members.join(", ")
1249
- })
1250
- })
1251
- break
1252
-
1253
- case "boolean":
1254
- schema = S.Boolean
1255
- break
1256
-
1257
- case "date":
1258
- schema = S.Date
1259
- break
1260
-
1261
- case "unknown":
1262
- schema = S.Unknown
1263
- break
1264
-
1265
- default:
1266
- // For any unhandled types, use Unknown schema to prevent undefined errors
1267
- console.warn(`Unhandled field type: ${meta}`)
1268
- schema = S.Unknown
1269
- break
1270
- }
1271
- if (!meta.required) {
1272
- schema = S.NullishOr(schema)
1273
- }
1274
- const result = S.toStandardSchemaV1(schema as any)
1275
- return result
1276
- }
1277
-
1278
- export type OmegaAutoGenMeta<
1279
- From extends Record<PropertyKey, any>,
1280
- To extends Record<PropertyKey, any>,
1281
- Name extends DeepKeys<From>
1282
- > = Omit<OmegaInputProps<From, To, Name>, "form">
1283
-
1284
- const supportedInputs = [
1285
- "button",
1286
- "checkbox",
1287
- "color",
1288
- "date",
1289
- "email",
1290
- "number",
1291
- "password",
1292
- "radio",
1293
- "range",
1294
- "search",
1295
- "submit",
1296
- "tel",
1297
- "text",
1298
- "time",
1299
- "url"
1300
- ] as const
1301
- export type SupportedInputs = typeof supportedInputs[number]
1302
- export const getInputType = (input: string): SupportedInputs =>
1303
- (supportedInputs as readonly string[]).includes(input) ? input as SupportedInputs : "text"
1304
-
1305
- export function deepMerge(target: any, source: any) {
1306
- const result = { ...target }
1307
- for (const key in source) {
1308
- if (Array.isArray(source[key])) {
1309
- // Arrays should be copied directly, not deep merged
1310
- result[key] = source[key]
1311
- } else if (source[key] && isObject(source[key])) {
1312
- result[key] = deepMerge(result[key], source[key])
1313
- } else {
1314
- result[key] = source[key]
1315
- }
1316
- }
1317
- return result
1318
- }
1319
-
1320
- type SchemaWithMembers = {
1321
- members: readonly S.Schema<any>[]
1322
- }
1323
-
1324
- function hasMembers(schema: any): schema is SchemaWithMembers {
1325
- return schema && "members" in schema && Array.isArray(schema.members)
1326
- }
1327
-
1328
- // Internal implementation with WeakSet tracking
1329
- export const defaultsValueFromSchema = (
1330
- schema: S.Schema<any>,
1331
- record: Record<string, any> = {}
1332
- ): any => {
1333
- const ast = schema.ast
1334
- const defaultValue = getDefaultFromAst(ast)
1335
-
1336
- if (defaultValue !== undefined) {
1337
- return defaultValue
1338
- }
1339
-
1340
- if (isNullableOrUndefined(schema.ast) === "null") {
1341
- return null
1342
- }
1343
- if (isNullableOrUndefined(schema.ast) === "undefined") {
1344
- return undefined
1345
- }
1346
-
1347
- // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, Class, etc.)
1348
- const objectsAst = S.AST.isObjects(ast)
1349
- ? ast
1350
- : S.AST.isDeclaration(ast)
1351
- ? unwrapDeclaration(ast)
1352
- : undefined
1353
- if (objectsAst && S.AST.isObjects(objectsAst)) {
1354
- const result: Record<string, any> = {}
1355
-
1356
- for (const prop of objectsAst.propertySignatures) {
1357
- const key = prop.name.toString()
1358
- const propType = prop.type
1359
-
1360
- const propDefault = getDefaultFromAst(propType)
1361
- if (propDefault !== undefined) {
1362
- result[key] = propDefault
1363
- continue
1364
- }
1365
-
1366
- const propSchema = S.make(propType)
1367
- const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1368
-
1369
- if (propValue !== undefined) {
1370
- result[key] = propValue
1371
- } else if (isNullableOrUndefined(propType) === "undefined") {
1372
- result[key] = undefined
1373
- }
1374
- }
1375
-
1376
- return { ...result, ...record }
1377
- }
1378
-
1379
- // Handle unions via AST or schema-level .members
1380
- const unionTypes = S.AST.isUnion(ast)
1381
- ? ast.types
1382
- : hasMembers(schema)
1383
- ? schema.members.map((m) => m.ast)
1384
- : undefined
1385
- if (unionTypes) {
1386
- const mergedFields: Record<string, { ast: S.AST.AST }> = {}
1387
-
1388
- for (const memberAstRaw of unionTypes) {
1389
- const memberAst = unwrapDeclaration(memberAstRaw)
1390
- if (!S.AST.isObjects(memberAst)) continue
1391
-
1392
- for (const prop of memberAst.propertySignatures) {
1393
- const key = prop.name.toString()
1394
- const fieldDefault = getDefaultFromAst(prop.type)
1395
- const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
1396
-
1397
- if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
1398
- mergedFields[key] = { ast: prop.type }
1399
- }
1400
- }
1401
- }
1402
-
1403
- if (Object.keys(mergedFields).length === 0) {
1404
- return Object.keys(record).length > 0 ? record : undefined
1405
- }
1406
-
1407
- return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
1408
- acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
1409
- return acc
1410
- }, record)
1411
- }
1412
-
1413
- if (Object.keys(record).length === 0) {
1414
- if (S.AST.isString(ast)) {
1415
- return ""
1416
- }
1417
-
1418
- if (S.AST.isBoolean(ast)) {
1419
- return false
1420
- }
1421
- }
1422
- }