@effect-app/vue-components 4.0.0-beta.9 → 4.0.0-beta.91

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 (172) hide show
  1. package/README.md +24 -0
  2. package/dist/reset.css +52 -0
  3. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  4. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +22 -15
  5. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +3 -5
  6. package/dist/types/utils/index.d.ts +3 -4
  7. package/dist/vue-components.es.js +21 -44
  8. package/dist/vue-components10.es.js +5 -0
  9. package/dist/vue-components11.es.js +13 -0
  10. package/dist/vue-components12.es.js +444 -0
  11. package/dist/vue-components13.es.js +4 -0
  12. package/dist/vue-components14.es.js +38 -0
  13. package/dist/vue-components15.es.js +27 -0
  14. package/dist/vue-components16.es.js +28 -0
  15. package/dist/vue-components17.es.js +7 -0
  16. package/dist/vue-components18.es.js +18 -0
  17. package/dist/vue-components19.es.js +36 -0
  18. package/dist/vue-components2.es.js +11 -0
  19. package/dist/vue-components20.es.js +18 -0
  20. package/dist/vue-components21.es.js +21 -0
  21. package/dist/vue-components22.es.js +30 -0
  22. package/dist/vue-components23.es.js +7 -0
  23. package/dist/vue-components24.es.js +9 -0
  24. package/dist/vue-components25.es.js +38 -0
  25. package/dist/vue-components26.es.js +25 -0
  26. package/dist/vue-components27.es.js +128 -0
  27. package/dist/vue-components28.es.js +24 -0
  28. package/dist/vue-components29.es.js +21 -0
  29. package/dist/vue-components3.es.js +54 -0
  30. package/dist/vue-components30.es.js +9 -0
  31. package/dist/vue-components31.es.js +19 -0
  32. package/dist/vue-components32.es.js +5 -0
  33. package/dist/vue-components33.es.js +29 -0
  34. package/dist/vue-components34.es.js +5 -0
  35. package/dist/vue-components35.es.js +29 -0
  36. package/dist/vue-components36.es.js +6 -0
  37. package/dist/vue-components37.es.js +18 -0
  38. package/dist/vue-components38.es.js +56 -0
  39. package/dist/vue-components39.es.js +5 -0
  40. package/dist/vue-components4.es.js +5 -0
  41. package/dist/vue-components40.es.js +44 -0
  42. package/dist/vue-components41.es.js +5 -0
  43. package/dist/vue-components42.es.js +84 -0
  44. package/dist/vue-components44.es.js +8 -0
  45. package/dist/vue-components45.es.js +7 -0
  46. package/dist/vue-components46.es.js +267 -0
  47. package/dist/vue-components48.es.js +6 -0
  48. package/dist/vue-components49.es.js +79 -0
  49. package/dist/vue-components5.es.js +24 -0
  50. package/dist/vue-components50.es.js +5 -0
  51. package/dist/vue-components51.es.js +66 -0
  52. package/dist/vue-components52.es.js +5 -0
  53. package/dist/vue-components53.es.js +24 -0
  54. package/dist/vue-components54.es.js +5 -0
  55. package/dist/vue-components55.es.js +59 -0
  56. package/dist/vue-components56.es.js +5 -0
  57. package/dist/vue-components57.es.js +12 -0
  58. package/dist/vue-components58.es.js +22 -0
  59. package/dist/vue-components6.es.js +13 -0
  60. package/dist/vue-components60.es.js +7 -0
  61. package/dist/vue-components61.es.js +235 -0
  62. package/dist/vue-components62.es.js +33 -0
  63. package/dist/vue-components63.es.js +8 -0
  64. package/dist/vue-components64.es.js +36 -0
  65. package/dist/vue-components7.es.js +28 -0
  66. package/dist/vue-components8.es.js +47 -0
  67. package/dist/vue-components9.es.js +5 -0
  68. package/package.json +30 -25
  69. package/src/components/CommandButton.vue +1 -1
  70. package/src/components/OmegaForm/OmegaAutoGen.vue +25 -30
  71. package/src/components/OmegaForm/OmegaErrorsInternal.vue +2 -3
  72. package/src/components/OmegaForm/OmegaFormStuff.ts +504 -355
  73. package/src/components/OmegaForm/OmegaInternalInput.vue +9 -5
  74. package/src/components/OmegaForm/useOmegaForm.ts +57 -36
  75. package/src/reset.css +52 -0
  76. package/src/utils/index.ts +4 -8
  77. package/dist/vue-components.es10.js +0 -237
  78. package/dist/vue-components.es100.js +0 -4
  79. package/dist/vue-components.es11.js +0 -32
  80. package/dist/vue-components.es12.js +0 -439
  81. package/dist/vue-components.es13.js +0 -49
  82. package/dist/vue-components.es14.js +0 -4
  83. package/dist/vue-components.es15.js +0 -4
  84. package/dist/vue-components.es16.js +0 -725
  85. package/dist/vue-components.es17.js +0 -143
  86. package/dist/vue-components.es18.js +0 -6
  87. package/dist/vue-components.es19.js +0 -13
  88. package/dist/vue-components.es2.js +0 -30
  89. package/dist/vue-components.es20.js +0 -5
  90. package/dist/vue-components.es21.js +0 -26
  91. package/dist/vue-components.es22.js +0 -6
  92. package/dist/vue-components.es23.js +0 -10
  93. package/dist/vue-components.es24.js +0 -57
  94. package/dist/vue-components.es25.js +0 -71
  95. package/dist/vue-components.es26.js +0 -8
  96. package/dist/vue-components.es27.js +0 -8
  97. package/dist/vue-components.es28.js +0 -5
  98. package/dist/vue-components.es29.js +0 -5
  99. package/dist/vue-components.es3.js +0 -16
  100. package/dist/vue-components.es30.js +0 -4
  101. package/dist/vue-components.es31.js +0 -4
  102. package/dist/vue-components.es32.js +0 -4
  103. package/dist/vue-components.es33.js +0 -4
  104. package/dist/vue-components.es34.js +0 -19
  105. package/dist/vue-components.es35.js +0 -13
  106. package/dist/vue-components.es36.js +0 -40
  107. package/dist/vue-components.es37.js +0 -6
  108. package/dist/vue-components.es38.js +0 -85
  109. package/dist/vue-components.es39.js +0 -54
  110. package/dist/vue-components.es4.js +0 -52
  111. package/dist/vue-components.es40.js +0 -563
  112. package/dist/vue-components.es41.js +0 -43
  113. package/dist/vue-components.es42.js +0 -29
  114. package/dist/vue-components.es43.js +0 -7
  115. package/dist/vue-components.es44.js +0 -42
  116. package/dist/vue-components.es45.js +0 -316
  117. package/dist/vue-components.es46.js +0 -33
  118. package/dist/vue-components.es47.js +0 -6
  119. package/dist/vue-components.es48.js +0 -26
  120. package/dist/vue-components.es49.js +0 -77
  121. package/dist/vue-components.es5.js +0 -52
  122. package/dist/vue-components.es50.js +0 -101
  123. package/dist/vue-components.es51.js +0 -4
  124. package/dist/vue-components.es52.js +0 -320
  125. package/dist/vue-components.es53.js +0 -66
  126. package/dist/vue-components.es54.js +0 -4
  127. package/dist/vue-components.es55.js +0 -4
  128. package/dist/vue-components.es56.js +0 -113
  129. package/dist/vue-components.es58.js +0 -9
  130. package/dist/vue-components.es59.js +0 -34
  131. package/dist/vue-components.es6.js +0 -69
  132. package/dist/vue-components.es61.js +0 -194
  133. package/dist/vue-components.es63.js +0 -6
  134. package/dist/vue-components.es64.js +0 -103
  135. package/dist/vue-components.es65.js +0 -4
  136. package/dist/vue-components.es66.js +0 -23
  137. package/dist/vue-components.es67.js +0 -84
  138. package/dist/vue-components.es68.js +0 -14
  139. package/dist/vue-components.es69.js +0 -115
  140. package/dist/vue-components.es7.js +0 -83
  141. package/dist/vue-components.es70.js +0 -5
  142. package/dist/vue-components.es71.js +0 -34
  143. package/dist/vue-components.es72.js +0 -4
  144. package/dist/vue-components.es73.js +0 -4
  145. package/dist/vue-components.es74.js +0 -17
  146. package/dist/vue-components.es75.js +0 -72
  147. package/dist/vue-components.es76.js +0 -17
  148. package/dist/vue-components.es77.js +0 -18
  149. package/dist/vue-components.es78.js +0 -10
  150. package/dist/vue-components.es79.js +0 -25
  151. package/dist/vue-components.es8.js +0 -63
  152. package/dist/vue-components.es80.js +0 -7
  153. package/dist/vue-components.es81.js +0 -23
  154. package/dist/vue-components.es82.js +0 -32
  155. package/dist/vue-components.es83.js +0 -24
  156. package/dist/vue-components.es84.js +0 -14
  157. package/dist/vue-components.es85.js +0 -7
  158. package/dist/vue-components.es86.js +0 -21
  159. package/dist/vue-components.es87.js +0 -11
  160. package/dist/vue-components.es88.js +0 -33
  161. package/dist/vue-components.es89.js +0 -50
  162. package/dist/vue-components.es9.js +0 -21
  163. package/dist/vue-components.es90.js +0 -28
  164. package/dist/vue-components.es91.js +0 -13
  165. package/dist/vue-components.es92.js +0 -67
  166. package/dist/vue-components.es93.js +0 -58
  167. package/dist/vue-components.es94.js +0 -19
  168. package/dist/vue-components.es95.js +0 -35
  169. package/dist/vue-components.es96.js +0 -31
  170. package/dist/vue-components.es97.js +0 -44
  171. package/dist/vue-components.es98.js +0 -4
  172. package/dist/vue-components.es99.js +0 -46
@@ -1,16 +1,30 @@
1
- import type * as Effect from "effect/Effect"
2
- import * as AST from "effect/SchemaAST"
1
+ import { Effect, Option, type Record, S } from "effect-app"
3
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
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"
5
4
  import { isObject } from "@vueuse/core"
6
- import * as S from "effect/Schema"
5
+ import type { Fiber as EffectFiber } from "effect/Fiber"
6
+ import type { Redacted } from "effect/Redacted"
7
7
  import { getTransformationFrom, useIntl } from "../../utils"
8
8
  import { type OmegaFieldInternalApi } from "./InputProps"
9
9
  import { type OF, type OmegaFormReturn } from "./useOmegaForm"
10
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
+
11
25
  export type FieldPath<T> = unknown extends T ? string
12
26
  // technically we cannot have primitive at the root
13
- : T extends string | boolean | number | null | undefined | symbol | bigint ? ""
27
+ : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? ""
14
28
  // technically we cannot have array at the root
15
29
  : T extends ReadonlyArray<infer U> ? FieldPath_<U, `[${number}]`>
16
30
  : {
@@ -18,7 +32,7 @@ export type FieldPath<T> = unknown extends T ? string
18
32
  }[keyof T]
19
33
 
20
34
  export type FieldPath_<T, Path extends string> = unknown extends T ? string
21
- : T extends string | boolean | number | null | undefined | symbol | bigint ? Path
35
+ : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? Path
22
36
  : T extends ReadonlyArray<infer U> ? FieldPath_<U, `${Path}[${number}]`> | Path
23
37
  : {
24
38
  [K in keyof T]: FieldPath_<T[K], `${Path}.${K & string}`>
@@ -145,7 +159,7 @@ export type FormProps<From, To> =
145
159
  formApi: OmegaFormParams<From, To>
146
160
  meta: any
147
161
  value: To
148
- }) => Promise<any> | Effect.Effect<unknown, any, never>
162
+ }) => Promise<any> | EffectFiber<any, any> | Effect.Effect<unknown, any, never>
149
163
  }
150
164
 
151
165
  export type OmegaFormParams<From, To> = FormApi<
@@ -223,7 +237,13 @@ export type PrefixFromDepth<
223
237
  _TDepth extends any[]
224
238
  > = K
225
239
 
226
- export type NestedKeyOf<T> = DeepKeys<T>
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>>
227
247
 
228
248
  export type FieldValidators<T> = {
229
249
  onChangeAsync?: FieldAsyncValidateOrFn<T, any, any>
@@ -262,13 +282,17 @@ export type SelectFieldMeta = BaseFieldMeta & {
262
282
  export type MultipleFieldMeta = BaseFieldMeta & {
263
283
  type: "multiple"
264
284
  members: any[] // TODO: should be non empty array?
265
- rest: readonly AST.AST[]
285
+ rest: readonly S.AST.AST[]
266
286
  }
267
287
 
268
288
  export type BooleanFieldMeta = BaseFieldMeta & {
269
289
  type: "boolean"
270
290
  }
271
291
 
292
+ export type DateFieldMeta = BaseFieldMeta & {
293
+ type: "date"
294
+ }
295
+
272
296
  export type UnknownFieldMeta = BaseFieldMeta & {
273
297
  type: "unknown"
274
298
  }
@@ -279,6 +303,7 @@ export type FieldMeta =
279
303
  | SelectFieldMeta
280
304
  | MultipleFieldMeta
281
305
  | BooleanFieldMeta
306
+ | DateFieldMeta
282
307
  | UnknownFieldMeta
283
308
 
284
309
  export type MetaRecord<T = string> = {
@@ -301,34 +326,57 @@ export type CreateMeta =
301
326
  }
302
327
  & (
303
328
  | {
304
- propertySignatures: readonly AST.PropertySignature[]
329
+ propertySignatures: readonly S.AST.PropertySignature[]
305
330
  property?: never
306
331
  }
307
332
  | {
308
333
  propertySignatures?: never
309
- property: AST.AST
334
+ property: S.AST.AST
310
335
  }
311
336
  )
312
337
 
313
- const getNullableOrUndefined = (property: AST.AST) => {
314
- if (!AST.isUnion(property)) return false
315
- return property.types.find((_) => AST.isUndefined(_) || _ === S.Null.ast)
338
+ const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
339
+ let current = getTransformationFrom(property)
340
+
341
+ while (S.AST.isDeclaration(current) && current.typeParameters.length > 0) {
342
+ current = getTransformationFrom(current.typeParameters[0]!)
343
+ }
344
+
345
+ return current
316
346
  }
317
347
 
318
- export const isNullableOrUndefined = (property: false | AST.AST | undefined) => {
319
- if (!property || !AST.isUnion(property)) return false
320
- if (property.types.find((_) => AST.isUndefined(_))) {
348
+ const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
349
+
350
+ /**
351
+ * Unwrap a single-element Union to its inner type if it's a Literal.
352
+ * After AST.toType, S.Struct({ _tag: S.Literal("X") }) produces Union([Literal("X")])
353
+ * instead of bare Literal("X") like S.TaggedStruct does.
354
+ * TODO: remove after manual _tag deprecation
355
+ */
356
+ const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
357
+ S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0]!)
358
+ ? ast.types[0]!
359
+ : ast
360
+
361
+ const getNullableOrUndefined = (property: S.AST.AST) =>
362
+ S.AST.isUnion(property)
363
+ ? property.types.find((_) => isNullishType(_))
364
+ : false
365
+
366
+ export const isNullableOrUndefined = (property: false | S.AST.AST | undefined) => {
367
+ if (!property || !S.AST.isUnion(property)) return false
368
+ if (property.types.find((_) => S.AST.isUndefined(_))) {
321
369
  return "undefined"
322
370
  }
323
- if (property.types.find((_) => _ === S.Null.ast)) return "null"
371
+ if (property.types.find((_) => S.AST.isNull(_))) return "null"
324
372
  return false
325
373
  }
326
374
 
327
375
  // Helper function to recursively unwrap nested unions (e.g., S.NullOr(S.NullOr(X)) -> X)
328
- const unwrapNestedUnions = (types: readonly AST.AST[]): readonly AST.AST[] => {
329
- const result: AST.AST[] = []
376
+ const unwrapNestedUnions = (types: readonly S.AST.AST[]): readonly S.AST.AST[] => {
377
+ const result: S.AST.AST[] = []
330
378
  for (const type of types) {
331
- if (AST.isUnion(type)) {
379
+ if (S.AST.isUnion(type)) {
332
380
  // Recursively unwrap nested unions
333
381
  const unwrapped = unwrapNestedUnions(type.types)
334
382
  result.push(...unwrapped)
@@ -339,31 +387,127 @@ const unwrapNestedUnions = (types: readonly AST.AST[]): readonly AST.AST[] => {
339
387
  return result
340
388
  }
341
389
 
390
+ const getNonNullTypes = (types: readonly S.AST.AST[]) =>
391
+ unwrapNestedUnions(types)
392
+ .map(unwrapDeclaration)
393
+ .filter((_) => !isNullishType(_))
394
+
395
+ const getJsonSchemaAnnotation = (property: S.AST.AST): Record<string, unknown> => {
396
+ const jsonSchema = S.AST.resolve(property)?.jsonSchema
397
+ return jsonSchema && typeof jsonSchema === "object" ? jsonSchema as Record<string, unknown> : {}
398
+ }
399
+
400
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
401
+ if (!link?.transformation?.decode?.run) return undefined
402
+ try {
403
+ const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
404
+ return Option.isSome(result) ? result.value : undefined
405
+ } catch {
406
+ return undefined
407
+ }
408
+ }
409
+
410
+ const getDefaultFromAst = (property: S.AST.AST) => {
411
+ // 1. Check withConstructorDefault (stored in context.defaultValue)
412
+ const constructorLink = property.context?.defaultValue?.[0]
413
+ const constructorDefault = extractDefaultFromLink(constructorLink)
414
+ if (constructorDefault !== undefined) return constructorDefault
415
+
416
+ // 2. Check withDecodingDefault (stored in encoding)
417
+ const encodingLink = property.encoding?.[0]
418
+ if (encodingLink && property.context?.isOptional) {
419
+ return extractDefaultFromLink(encodingLink)
420
+ }
421
+
422
+ return undefined
423
+ }
424
+
425
+ const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
426
+ const checks = property.checks ?? []
427
+
428
+ return checks.flatMap((check) => {
429
+ if (check._tag === "FilterGroup") {
430
+ return check.checks.flatMap((inner) => {
431
+ const meta = inner.annotations?.meta
432
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
433
+ })
434
+ }
435
+
436
+ const meta = check.annotations?.meta
437
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
438
+ })
439
+ }
440
+
441
+ const getFieldMetadataFromAst = (property: S.AST.AST) => {
442
+ const base: Partial<FieldMeta> & Record<string, unknown> = {
443
+ description: S.AST.resolveDescription(property)
444
+ }
445
+ const checks = getCheckMetas(property)
446
+
447
+ if (S.AST.isString(property)) {
448
+ base.type = "string"
449
+ for (const check of checks) {
450
+ switch (check._tag) {
451
+ case "isMinLength":
452
+ base.minLength = check.minLength
453
+ break
454
+ case "isMaxLength":
455
+ base.maxLength = check.maxLength
456
+ break
457
+ }
458
+ }
459
+
460
+ if (S.AST.resolveTitle(property) === "Email") {
461
+ base.format = "email"
462
+ }
463
+ } else if (S.AST.isNumber(property)) {
464
+ base.type = "number"
465
+ for (const check of checks) {
466
+ switch (check._tag) {
467
+ case "isInt":
468
+ base.refinement = "int"
469
+ break
470
+ case "isGreaterThanOrEqualTo":
471
+ base.minimum = check.minimum
472
+ break
473
+ case "isLessThanOrEqualTo":
474
+ base.maximum = check.maximum
475
+ break
476
+ case "isBetween":
477
+ base.minimum = check.minimum
478
+ base.maximum = check.maximum
479
+ break
480
+ case "isGreaterThan":
481
+ base.exclusiveMinimum = check.exclusiveMinimum
482
+ break
483
+ case "isLessThan":
484
+ base.exclusiveMaximum = check.exclusiveMaximum
485
+ break
486
+ }
487
+ }
488
+ } else if (S.AST.isBoolean(property)) {
489
+ base.type = "boolean"
490
+ } else if (
491
+ S.AST.isDeclaration(property)
492
+ && (property.annotations as any)?.typeConstructor?._tag === "Date"
493
+ ) {
494
+ base.type = "date"
495
+ } else {
496
+ base.type = "unknown"
497
+ }
498
+
499
+ return base
500
+ }
501
+
342
502
  export const createMeta = <T = any>(
343
503
  { meta = {}, parent = "", property, propertySignatures }: CreateMeta,
344
504
  acc: Partial<MetaRecord<T>> = {}
345
505
  ): MetaRecord<T> | FieldMeta => {
346
- // unwraps class (Class are transformations)
347
- // this calls createMeta recursively, so wrapped transformations are also unwrapped
348
- // BUT: check for Int title annotation first - S.Int and branded Int have title "Int" or "int"
349
- // and we don't want to lose that information by unwrapping
350
- if (property && AST.isDeclaration(property)) {
351
- const titleOnTransform = property.annotations?.title ?? ""
352
-
353
- // only unwrap if this is NOT an Int type
354
- if (titleOnTransform !== "Int" && titleOnTransform !== "int") {
355
- // In v4, Declaration doesn't have a 'from' property
356
- // Just return the property as-is
357
- return createMeta<T>({
358
- parent,
359
- meta,
360
- property
361
- })
362
- }
363
- // if it's Int, fall through to process it with the Int type
506
+ if (property) {
507
+ property = unwrapDeclaration(property)
364
508
  }
365
509
 
366
- if (property && AST.isObjects(property)) {
510
+ if (property && S.AST.isObjects(property)) {
367
511
  return createMeta<T>({
368
512
  meta,
369
513
  propertySignatures: property.propertySignatures
@@ -375,12 +519,9 @@ export const createMeta = <T = any>(
375
519
  const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
376
520
  const nullableOrUndefined = isNullableOrUndefined(p.type)
377
521
 
378
- // Check if this property has title "Int" or "int" annotation (from Int brand wrapper)
379
- const propertyTitle = p.type.annotations?.title ?? ""
380
- const isIntField = propertyTitle === "Int" || propertyTitle === "int"
381
-
382
522
  // Determine if this field should be required:
383
523
  // - For nullable discriminated unions, only _tag should be non-required
524
+ // - optionalKey fields are not required
384
525
  // - All other fields should calculate their required status normally
385
526
  let isRequired: boolean
386
527
  if (meta._isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
@@ -389,25 +530,18 @@ export const createMeta = <T = any>(
389
530
  } else if (meta.required === false) {
390
531
  // Explicitly set to non-required (legacy behavior for backwards compatibility)
391
532
  isRequired = false
533
+ } else if ((p.type as any).context?.isOptional) {
534
+ isRequired = false
392
535
  } else {
393
536
  // Calculate from the property itself
394
537
  isRequired = !nullableOrUndefined
395
538
  }
396
539
 
397
- const typeToProcess = p.type
398
- if (AST.isUnion(p.type)) {
399
- // First unwrap any nested unions, then filter out null/undefined
400
- const unwrappedTypes = unwrapNestedUnions(p.type.types)
401
- const nonNullTypes = unwrappedTypes
402
- .filter(
403
- (t) => !AST.isUndefined(t) && !AST.isNull(t)
404
- )
405
- // unwraps class (Class are transformations)
406
- .map(getTransformationFrom)
540
+ const typeToProcess = unwrapDeclaration(p.type)
541
+ if (S.AST.isUnion(p.type)) {
542
+ const nonNullTypes = getNonNullTypes(p.type.types)
407
543
 
408
- const hasStructMembers = nonNullTypes.some(
409
- (t) => AST.isObjects(t)
410
- )
544
+ const hasStructMembers = nonNullTypes.some(S.AST.isObjects)
411
545
 
412
546
  if (hasStructMembers) {
413
547
  // Only create parent meta for non-NullOr unions to avoid duplicates
@@ -422,25 +556,38 @@ export const createMeta = <T = any>(
422
556
 
423
557
  // Process each non-null type and merge their metadata
424
558
  for (const nonNullType of nonNullTypes) {
425
- if (AST.isObjects(nonNullType)) {
559
+ if (S.AST.isObjects(nonNullType)) {
426
560
  // For discriminated unions (multiple branches):
427
561
  // - If the parent union is nullable, only _tag should be non-required
428
562
  // - All other fields maintain their normal required status based on their own types
429
563
  const isNullableDiscriminatedUnion = nullableOrUndefined && nonNullTypes.length > 1
430
564
 
431
- Object.assign(
432
- acc,
433
- createMeta<T>({
434
- parent: key,
435
- propertySignatures: nonNullType.propertySignatures,
436
- meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
437
- })
438
- )
565
+ const branchMeta = createMeta<T>({
566
+ parent: key,
567
+ propertySignatures: nonNullType.propertySignatures,
568
+ meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
569
+ })
570
+
571
+ // Merge branch metadata, combining select members for shared discriminator fields
572
+ for (const [metaKey, metaValue] of Object.entries(branchMeta)) {
573
+ const existing = acc[metaKey as NestedKeyOf<T>] as FieldMeta | undefined
574
+ if (
575
+ existing && existing.type === "select" && (metaValue as any)?.type === "select"
576
+ ) {
577
+ existing.members = [
578
+ ...existing.members,
579
+ ...(metaValue as SelectFieldMeta).members.filter(
580
+ (m: any) => !existing.members.includes(m)
581
+ )
582
+ ]
583
+ } else {
584
+ acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
585
+ }
586
+ }
439
587
  }
440
588
  }
441
589
  } else {
442
- // Check if any of the union types are arrays
443
- const arrayTypes = nonNullTypes.filter(AST.isArrays)
590
+ const arrayTypes = nonNullTypes.filter(S.AST.isArrays)
444
591
  if (arrayTypes.length > 0) {
445
592
  const arrayType = arrayTypes[0] // Take the first array type
446
593
 
@@ -454,8 +601,8 @@ export const createMeta = <T = any>(
454
601
 
455
602
  // If the array has struct elements, also create metadata for their properties
456
603
  if (arrayType.rest && arrayType.rest.length > 0) {
457
- const restElement = arrayType.rest[0]
458
- if (AST.isObjects(restElement)) {
604
+ const restElement = unwrapDeclaration(arrayType.rest[0]!)
605
+ if (S.AST.isObjects(restElement)) {
459
606
  for (const prop of restElement.propertySignatures) {
460
607
  const propKey = `${key}.${prop.name.toString()}`
461
608
 
@@ -473,12 +620,12 @@ export const createMeta = <T = any>(
473
620
  acc[propKey as NestedKeyOf<T>] = propMeta as FieldMeta
474
621
 
475
622
  if (
476
- propMeta.type === "multiple" && AST.isArrays(prop.type) && prop
623
+ propMeta.type === "multiple" && S.AST.isArrays(prop.type) && prop
477
624
  .type
478
625
  .rest && prop.type.rest.length > 0
479
626
  ) {
480
- const nestedRestElement = prop.type.rest[0]
481
- if (AST.isObjects(nestedRestElement)) {
627
+ const nestedRestElement = unwrapDeclaration(prop.type.rest[0]!)
628
+ if (S.AST.isObjects(nestedRestElement)) {
482
629
  for (const nestedProp of nestedRestElement.propertySignatures) {
483
630
  const nestedPropKey = `${propKey}.${nestedProp.name.toString()}`
484
631
 
@@ -513,34 +660,32 @@ export const createMeta = <T = any>(
513
660
  }
514
661
  }
515
662
  } else {
516
- // Unwrap transformations (like ExtendedClass) to check for propertySignatures
517
- const unwrappedTypeToProcess = getTransformationFrom(typeToProcess)
518
- if (AST.isObjects(unwrappedTypeToProcess)) {
663
+ if (S.AST.isObjects(typeToProcess)) {
519
664
  Object.assign(
520
665
  acc,
521
666
  createMeta<T>({
522
667
  parent: key,
523
- propertySignatures: unwrappedTypeToProcess.propertySignatures,
668
+ propertySignatures: typeToProcess.propertySignatures,
524
669
  meta: { required: isRequired, nullableOrUndefined }
525
670
  })
526
671
  )
527
- } else if (AST.isArrays(p.type)) {
672
+ } else if (S.AST.isArrays(p.type)) {
528
673
  // Check if it has struct elements
529
674
  const hasStructElements = p.type.rest.length > 0
530
- && AST.isObjects(p.type.rest[0])
675
+ && S.AST.isObjects(unwrapDeclaration(p.type.rest[0]!))
531
676
 
532
677
  if (hasStructElements) {
533
678
  // For arrays with struct elements, only create meta for nested fields, not the array itself
534
- const elementType = p.type.rest[0]
535
- if (AST.isObjects(elementType)) {
679
+ const elementType = unwrapDeclaration(p.type.rest[0]!)
680
+ if (S.AST.isObjects(elementType)) {
536
681
  // Process each property in the array element
537
682
  for (const prop of elementType.propertySignatures) {
538
683
  const propKey = `${key}.${prop.name.toString()}`
539
684
 
540
685
  // Check if the property is another array
541
- if (AST.isArrays(prop.type) && prop.type.rest.length > 0) {
542
- const nestedElementType = prop.type.rest[0]
543
- if (AST.isObjects(nestedElementType)) {
686
+ if (S.AST.isArrays(prop.type) && prop.type.rest.length > 0) {
687
+ const nestedElementType = unwrapDeclaration(prop.type.rest[0]!)
688
+ if (S.AST.isObjects(nestedElementType)) {
544
689
  // Array with struct elements - process nested fields
545
690
  for (const nestedProp of nestedElementType.propertySignatures) {
546
691
  const nestedKey = `${propKey}.${nestedProp.name.toString()}`
@@ -592,9 +737,11 @@ export const createMeta = <T = any>(
592
737
  parent: key,
593
738
  property: p.type,
594
739
  meta: {
595
- required: isRequired,
596
- nullableOrUndefined,
597
- ...(isIntField && { refinement: "int" })
740
+ // an empty string is valid for a S.String field, so we should not mark it as required
741
+ // TODO: handle this better via the createMeta minLength parsing
742
+ required: isRequired
743
+ && (!S.AST.isString(typeToProcess) || !!getFieldMetadataFromAst(typeToProcess).minLength),
744
+ nullableOrUndefined
598
745
  }
599
746
  })
600
747
 
@@ -607,19 +754,30 @@ export const createMeta = <T = any>(
607
754
 
608
755
  if (property) {
609
756
  const nullableOrUndefined = getNullableOrUndefined(property)
757
+ property = unwrapDeclaration(property)
610
758
 
611
759
  if (!Object.hasOwnProperty.call(meta, "required")) {
612
760
  meta["required"] = !nullableOrUndefined
613
761
  }
614
762
 
615
- if (AST.isUnion(property)) {
616
- // First unwrap any nested unions, then filter out null/undefined
617
- const unwrappedTypes = unwrapNestedUnions(property.types)
618
- const nonNullType = unwrappedTypes.find(
619
- (t) => !AST.isUndefined(t) && !AST.isNull(t)
620
- )!
763
+ if (S.AST.isUnion(property)) {
764
+ const unwrappedTypes = unwrapNestedUnions(property.types).map(unwrapDeclaration)
765
+ const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
766
+
767
+ // Unwrap single-element unions when the literal is a boolean
768
+ // (effect-app's S.Literal wraps as S.Literals([x]) → Union([Literal(x)]))
769
+ // Don't unwrap string/number literals — they may be discriminator values in a union
770
+ if (
771
+ nonNullTypes.length === 1
772
+ && S.AST.isLiteral(nonNullTypes[0]!)
773
+ && typeof nonNullTypes[0]!.literal === "boolean"
774
+ ) {
775
+ return createMeta<T>({ parent, meta, property: nonNullTypes[0]! })
776
+ }
777
+
778
+ const nonNullType = nonNullTypes[0]!
621
779
 
622
- if (AST.isObjects(nonNullType)) {
780
+ if (S.AST.isObjects(nonNullType)) {
623
781
  return createMeta<T>({
624
782
  propertySignatures: nonNullType.propertySignatures,
625
783
  parent,
@@ -627,11 +785,13 @@ export const createMeta = <T = any>(
627
785
  })
628
786
  }
629
787
 
630
- if (unwrappedTypes.every(AST.isLiteral)) {
788
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
789
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
790
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
631
791
  return {
632
792
  ...meta,
633
793
  type: "select",
634
- members: unwrappedTypes.map((t) => t.literal)
794
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
635
795
  } as FieldMeta
636
796
  }
637
797
 
@@ -645,7 +805,7 @@ export const createMeta = <T = any>(
645
805
  } as FieldMeta
646
806
  }
647
807
 
648
- if (AST.isArrays(property)) {
808
+ if (S.AST.isArrays(property)) {
649
809
  return {
650
810
  ...meta,
651
811
  type: "multiple",
@@ -654,38 +814,15 @@ export const createMeta = <T = any>(
654
814
  } as FieldMeta
655
815
  }
656
816
 
657
- const JSONAnnotation = (property.annotations?.jsonSchema ?? {}) as Record<string, unknown>
658
-
659
- meta = { ...JSONAnnotation, ...meta }
660
-
661
- // check the title annotation BEFORE following "from" to detect refinements like S.Int
662
- let titleType = property.annotations?.title ?? "unknown"
663
-
664
- // Detect basic types from AST if no title annotation
665
- if (titleType === "unknown") {
666
- if (AST.isString(property)) {
667
- titleType = "string"
668
- } else if (AST.isNumber(property)) {
669
- titleType = "number"
670
- } else if (AST.isBoolean(property)) {
671
- titleType = "boolean"
672
- }
673
- }
674
-
675
- // if this is S.Int (a refinement), set the type and skip following "from"
676
- // otherwise we'd lose the "Int" information and get "number" instead
677
- if (titleType === "Int" || titleType === "int") {
678
- meta["type"] = "number"
679
- meta["refinement"] = "int"
680
- // don't follow "from" for Int refinements
681
- } else {
682
- meta["type"] = titleType
817
+ if (S.AST.isLiteral(property)) {
818
+ return {
819
+ ...meta,
820
+ type: "select",
821
+ members: [property.literal]
822
+ } as FieldMeta
683
823
  }
684
824
 
685
- // Always ensure required is set before returning
686
- if (!Object.hasOwnProperty.call(meta, "required")) {
687
- meta["required"] = !nullableOrUndefined
688
- }
825
+ meta = { ...getJsonSchemaAnnotation(property), ...getFieldMetadataFromAst(property), ...meta }
689
826
 
690
827
  return meta as FieldMeta
691
828
  }
@@ -711,27 +848,21 @@ const flattenMeta = <T>(meta: MetaRecord<T> | FieldMeta, parentKey: string = "")
711
848
  return result
712
849
  }
713
850
 
714
- const _schemaFromAst = (ast: AST.AST): S.Codec<any> => S.make(ast)
715
-
716
- const metadataFromAst = <_From, To>(
717
- schema: any // v4 Schema type is complex, use any for now
851
+ const metadataFromAst = <From, To>(
852
+ schema: S.Codec<To, From, never>
718
853
  ): { meta: MetaRecord<To>; defaultValues: Record<string, any>; unionMeta: Record<string, MetaRecord<To>> } => {
719
- const ast = schema.ast
854
+ const ast = unwrapDeclaration(schema.ast)
720
855
  const newMeta: MetaRecord<To> = {}
721
856
  const defaultValues: Record<string, any> = {}
722
857
  const unionMeta: Record<string, MetaRecord<To>> = {}
723
858
 
724
859
  // Handle root-level Union types (discriminated unions)
725
- if (AST.isUnion(ast)) {
726
- const types = ast.types
727
-
860
+ if (S.AST.isUnion(ast)) {
728
861
  // Filter out null/undefined types and unwrap transformations
729
- const nonNullTypes = types
730
- .filter((t: any) => !AST.isUndefined(t) && !AST.isNull(t))
731
- .map(getTransformationFrom)
862
+ const nonNullTypes = getNonNullTypes(ast.types)
732
863
 
733
864
  // Check if this is a discriminated union (all members are structs)
734
- const allStructs = nonNullTypes.every((t: any) => AST.isObjects(t))
865
+ const allStructs = nonNullTypes.every(S.AST.isObjects)
735
866
 
736
867
  if (allStructs && nonNullTypes.length > 0) {
737
868
  // Extract discriminator values from each union member
@@ -739,16 +870,32 @@ const metadataFromAst = <_From, To>(
739
870
 
740
871
  // Store metadata for each union member by its tag value
741
872
  for (const memberType of nonNullTypes) {
742
- if (AST.isObjects(memberType)) {
873
+ if (S.AST.isObjects(memberType)) {
743
874
  // Find the discriminator field (usually _tag)
744
875
  const tagProp = memberType.propertySignatures.find(
745
- (p: any) => p.name.toString() === "_tag"
876
+ (p) => p.name.toString() === "_tag"
746
877
  )
747
878
 
748
879
  let tagValue: string | null = null
749
- if (tagProp && AST.isLiteral(tagProp.type)) {
750
- tagValue = tagProp.type.literal as string
880
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
881
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
882
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
883
+ tagValue = resolvedTagType.literal as string
751
884
  discriminatorValues.push(tagValue)
885
+ // Warn if the tag was wrapped in a single-element Union (legacy pattern)
886
+ if (
887
+ tagProp
888
+ && S.AST.isUnion(tagProp.type)
889
+ && isDevelopmentEnvironment()
890
+ && tagValue != null
891
+ && !legacyTagWarningEmittedFor.has(tagValue)
892
+ ) {
893
+ legacyTagWarningEmittedFor.add(tagValue)
894
+ console.warn(
895
+ `[OmegaForm] Union member with _tag "${tagValue}" uses S.Struct({ _tag: S.Literal("${tagValue}"), ... }). `
896
+ + `Please migrate to S.TaggedStruct("${tagValue}", { ... }) for cleaner AST handling.`
897
+ )
898
+ }
752
899
  }
753
900
 
754
901
  // Create metadata for this member's properties
@@ -779,7 +926,7 @@ const metadataFromAst = <_From, To>(
779
926
  }
780
927
  }
781
928
 
782
- if (AST.isObjects(ast)) {
929
+ if (S.AST.isObjects(ast)) {
783
930
  const meta = createMeta<To>({
784
931
  propertySignatures: ast.propertySignatures
785
932
  })
@@ -808,16 +955,80 @@ const metadataFromAst = <_From, To>(
808
955
  return { meta: newMeta, defaultValues, unionMeta }
809
956
  }
810
957
 
958
+ /*
959
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
960
+ * These need to be swapped to S.RedactedFromValue for form usage
961
+ * because S.Redacted expects Redacted objects, not plain strings.
962
+ */
963
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
964
+ S.AST.isDeclaration(ast)
965
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
966
+ && !ast.encoding
967
+
968
+ /*
969
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
970
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
971
+ * Redacted<A> on both encoded and type sides, so form inputs (which
972
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
973
+ * plain values on the encoded side and wraps them in Redacted on decode.
974
+ */
975
+ export const toFormSchema = <From, To>(
976
+ schema: S.Codec<To, From, never>
977
+ ): S.Codec<To, From, never> => {
978
+ const ast = schema.ast
979
+ const objAst = S.AST.isObjects(ast)
980
+ ? ast
981
+ : S.AST.isDeclaration(ast)
982
+ ? S.AST.toEncoded(ast)
983
+ : null
984
+
985
+ if (!objAst || !("propertySignatures" in objAst)) return schema
986
+
987
+ let hasRedacted = false
988
+ const props: Record<string, S.Struct.Fields[string]> = {}
989
+
990
+ for (const p of objAst.propertySignatures) {
991
+ if (isRedactedWithoutEncoding(p.type)) {
992
+ hasRedacted = true
993
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
994
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
995
+ } else if (S.AST.isUnion(p.type)) {
996
+ const types = p.type.types
997
+ const redactedType = types.find(isRedactedWithoutEncoding)
998
+ if (redactedType) {
999
+ hasRedacted = true
1000
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
1001
+ const hasNull = types.some(S.AST.isNull)
1002
+ const hasUndefined = types.some(S.AST.isUndefined)
1003
+ const base = S.RedactedFromValue(innerSchema)
1004
+ props[p.name as string] = hasNull && hasUndefined
1005
+ ? S.NullishOr(base)
1006
+ : hasNull
1007
+ ? S.NullOr(base)
1008
+ : hasUndefined
1009
+ ? S.UndefinedOr(base)
1010
+ : base
1011
+ } else {
1012
+ props[p.name as string] = S.make(p.type)
1013
+ }
1014
+ } else {
1015
+ props[p.name as string] = S.make(p.type)
1016
+ }
1017
+ }
1018
+
1019
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
1020
+ }
1021
+
811
1022
  export const duplicateSchema = <From, To>(
812
1023
  schema: S.Codec<To, From, never>
813
1024
  ) => {
814
1025
  return schema
815
1026
  }
816
1027
 
817
- export const generateMetaFromSchema = <_From, To>(
818
- schema: any // v4 Schema type is complex, use any for now
1028
+ export const generateMetaFromSchema = <From, To>(
1029
+ schema: S.Codec<To, From, never>
819
1030
  ): {
820
- schema: any
1031
+ schema: S.Codec<To, From, never>
821
1032
  meta: MetaRecord<To>
822
1033
  unionMeta: Record<string, MetaRecord<To>>
823
1034
  } => {
@@ -833,168 +1044,138 @@ export const generateInputStandardSchemaFromFieldMeta = (
833
1044
  if (!trans) {
834
1045
  trans = useIntl().trans
835
1046
  }
836
- let schema: S.Codec<any>
837
-
1047
+ let schema: any
838
1048
  switch (meta.type) {
839
- case "string": {
840
- schema = S.String
841
-
842
- // Apply format-specific schemas
843
- if (meta.format === "email") {
844
- // v4 doesn't have S.Email, use pattern validation
845
- schema = S.String.check(
846
- S.makeFilter(
847
- (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) || trans("validation.email.invalid"),
848
- { title: "email format" }
849
- )
850
- )
851
- }
1049
+ case "string":
1050
+ schema = meta.format === "email"
1051
+ ? S.Email.annotate({
1052
+ message: trans("validation.email.invalid")
1053
+ })
1054
+ : S.String.annotate({
1055
+ message: trans("validation.empty")
1056
+ })
852
1057
 
853
- // Apply length validations
854
- if (meta.required || typeof meta.minLength === "number") {
855
- const minLen = meta.required ? Math.max(1, meta.minLength || 0) : (meta.minLength || 0)
856
- if (minLen > 0) {
857
- schema = schema.check(
858
- S.makeFilter(
859
- (s) => s.length >= minLen || trans("validation.string.minLength", { minLength: minLen }),
860
- { title: `minLength(${minLen})` }
861
- )
862
- )
863
- }
1058
+ if (meta.required) {
1059
+ schema = schema.check(S.isMinLength(1, {
1060
+ message: trans("validation.empty")
1061
+ }))
864
1062
  }
865
1063
 
866
1064
  if (typeof meta.maxLength === "number") {
867
- schema = schema.check(
868
- S.makeFilter(
869
- (s) => s.length <= meta.maxLength! || trans("validation.string.maxLength", { maxLength: meta.maxLength }),
870
- { title: `maxLength(${meta.maxLength})` }
871
- )
872
- )
1065
+ schema = schema.check(S.isMaxLength(meta.maxLength, {
1066
+ message: trans("validation.string.maxLength", {
1067
+ maxLength: meta.maxLength
1068
+ })
1069
+ }))
1070
+ }
1071
+ if (typeof meta.minLength === "number") {
1072
+ schema = schema.check(S.isMinLength(meta.minLength, {
1073
+ message: trans("validation.string.minLength", {
1074
+ minLength: meta.minLength
1075
+ })
1076
+ }))
873
1077
  }
874
1078
  break
875
- }
876
1079
 
877
- case "number": {
1080
+ case "number":
878
1081
  if (meta.refinement === "int") {
879
- schema = S.Int
1082
+ schema = S
1083
+ .Number
1084
+ .annotate({
1085
+ message: trans("validation.empty")
1086
+ })
1087
+ .check(S.isInt({
1088
+ message: trans("validation.integer.expected", { actualValue: "NaN" })
1089
+ }))
880
1090
  } else {
881
- schema = S.Number
1091
+ schema = S.Finite.annotate({
1092
+ message: trans("validation.number.expected", { actualValue: "NaN" })
1093
+ })
1094
+
1095
+ if (meta.required) {
1096
+ schema = schema.annotate({
1097
+ message: trans("validation.empty")
1098
+ })
1099
+ }
882
1100
  }
883
1101
 
884
- // Apply numeric validations
885
1102
  if (typeof meta.minimum === "number") {
886
- schema = schema.check(
887
- S.makeFilter(
888
- (n) =>
889
- n >= meta.minimum! || trans(
890
- meta.minimum === 0 ? "validation.number.positive" : "validation.number.min",
891
- { minimum: meta.minimum, isExclusive: false }
892
- ),
893
- { title: `>=${meta.minimum}` }
894
- )
895
- )
1103
+ schema = schema.check(S.isGreaterThanOrEqualTo(meta.minimum, {
1104
+ message: trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
1105
+ minimum: meta.minimum,
1106
+ isExclusive: true
1107
+ })
1108
+ }))
896
1109
  }
897
-
898
1110
  if (typeof meta.maximum === "number") {
899
- schema = schema.check(
900
- S.makeFilter(
901
- (n) =>
902
- n <= meta.maximum! || trans("validation.number.max", {
903
- maximum: meta.maximum,
904
- isExclusive: false
905
- }),
906
- { title: `<=${meta.maximum}` }
907
- )
908
- )
1111
+ schema = schema.check(S.isLessThanOrEqualTo(meta.maximum, {
1112
+ message: trans("validation.number.max", {
1113
+ maximum: meta.maximum,
1114
+ isExclusive: true
1115
+ })
1116
+ }))
909
1117
  }
910
-
911
1118
  if (typeof meta.exclusiveMinimum === "number") {
912
- schema = schema.check(
913
- S.makeFilter(
914
- (n) =>
915
- n > meta.exclusiveMinimum! || trans(
916
- meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min",
917
- { minimum: meta.exclusiveMinimum, isExclusive: true }
918
- ),
919
- { title: `>${meta.exclusiveMinimum}` }
920
- )
921
- )
1119
+ schema = schema.check(S.isGreaterThan(meta.exclusiveMinimum, {
1120
+ message: trans(meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min", {
1121
+ minimum: meta.exclusiveMinimum,
1122
+ isExclusive: false
1123
+ })
1124
+ }))
922
1125
  }
923
-
924
1126
  if (typeof meta.exclusiveMaximum === "number") {
925
- schema = schema.check(
926
- S.makeFilter(
927
- (n) =>
928
- n < meta.exclusiveMaximum! || trans("validation.number.max", {
929
- maximum: meta.exclusiveMaximum,
930
- isExclusive: true
931
- }),
932
- { title: `<${meta.exclusiveMaximum}` }
933
- )
934
- )
1127
+ schema = schema.check(S.isLessThan(meta.exclusiveMaximum, {
1128
+ message: trans("validation.number.max", {
1129
+ maximum: meta.exclusiveMaximum,
1130
+ isExclusive: false
1131
+ })
1132
+ }))
935
1133
  }
936
1134
  break
937
- }
1135
+ case "select":
1136
+ schema = S.Literals(meta.members as [any, ...any[]]).annotate({
1137
+ message: trans("validation.not_a_valid", {
1138
+ type: "select",
1139
+ message: meta.members.join(", ")
1140
+ })
1141
+ })
938
1142
 
939
- case "select": {
940
- // Use Literal for select options
941
- if (meta.members.length === 0) {
942
- schema = S.Unknown
943
- } else if (meta.members.length === 1) {
944
- schema = S.Literal(meta.members[0])
945
- } else {
946
- // v4 Union accepts an array of schemas
947
- schema = S.Union(meta.members.map((m) => S.Literal(m)))
948
- }
949
1143
  break
950
- }
951
1144
 
952
- case "multiple": {
953
- schema = S.Array(S.String)
1145
+ case "multiple":
1146
+ schema = S.Array(S.String).annotate({
1147
+ message: trans("validation.not_a_valid", {
1148
+ type: "multiple",
1149
+ message: meta.members.join(", ")
1150
+ })
1151
+ })
954
1152
  break
955
- }
956
1153
 
957
- case "boolean": {
1154
+ case "boolean":
958
1155
  schema = S.Boolean
959
1156
  break
960
- }
961
1157
 
962
- case "unknown": {
1158
+ case "date":
1159
+ schema = S.Date
1160
+ break
1161
+
1162
+ case "unknown":
963
1163
  schema = S.Unknown
964
1164
  break
965
- }
966
1165
 
967
- default: {
968
- console.warn(`Unhandled field type: ${(meta as any).type}`)
1166
+ default:
1167
+ // For any unhandled types, use Unknown schema to prevent undefined errors
1168
+ console.warn(`Unhandled field type: ${meta}`)
969
1169
  schema = S.Unknown
970
1170
  break
971
- }
972
1171
  }
973
-
974
- // Wrap in union with null/undefined if not required
975
1172
  if (!meta.required) {
976
- // v4 Union takes an array of schemas
977
- schema = S.Union([schema, S.Null, S.Undefined])
1173
+ schema = S.NullishOr(schema)
978
1174
  }
979
-
980
- return S.toStandardSchemaV1(schema as any)
1175
+ const result = S.toStandardSchemaV1(schema as any)
1176
+ return result
981
1177
  }
982
1178
 
983
- // TODO: Fix v4 migration - nullableInput transformation needs proper type handling
984
- // export const nullableInput = <A>(
985
- // schema: S.Codec<A>,
986
- // defaultValue: () => A
987
- // ): S.Codec<A> =>
988
- // S.NullOr(schema).pipe(
989
- // S.decodeTo(
990
- // schema,
991
- // SchemaTransformation.transform({
992
- // decode: (input: A | null) => input ?? defaultValue(),
993
- // encode: (output: A) => output
994
- // })
995
- // )
996
- // )
997
-
998
1179
  export type OmegaAutoGenMeta<
999
1180
  From extends Record<PropertyKey, any>,
1000
1181
  To extends Record<PropertyKey, any>,
@@ -1037,18 +1218,8 @@ export function deepMerge(target: any, source: any) {
1037
1218
  return result
1038
1219
  }
1039
1220
 
1040
- // Type definitions for schemas with fields and members
1041
- type SchemaWithFields = {
1042
- fields: Record<string, S.Top>
1043
- }
1044
-
1045
1221
  type SchemaWithMembers = {
1046
- members: readonly S.Top[]
1047
- }
1048
-
1049
- // Type guards to check schema types
1050
- function hasFields(schema: any): schema is SchemaWithFields {
1051
- return schema && "fields" in schema && typeof schema.fields === "object"
1222
+ members: readonly S.Schema<any>[]
1052
1223
  }
1053
1224
 
1054
1225
  function hasMembers(schema: any): schema is SchemaWithMembers {
@@ -1057,10 +1228,15 @@ function hasMembers(schema: any): schema is SchemaWithMembers {
1057
1228
 
1058
1229
  // Internal implementation with WeakSet tracking
1059
1230
  export const defaultsValueFromSchema = (
1060
- schema: S.Codec<any>,
1231
+ schema: S.Schema<any>,
1061
1232
  record: Record<string, any> = {}
1062
1233
  ): any => {
1063
1234
  const ast = schema.ast
1235
+ const defaultValue = getDefaultFromAst(ast)
1236
+
1237
+ if (defaultValue !== undefined) {
1238
+ return defaultValue
1239
+ }
1064
1240
 
1065
1241
  if (isNullableOrUndefined(schema.ast) === "null") {
1066
1242
  return null
@@ -1069,106 +1245,79 @@ export const defaultsValueFromSchema = (
1069
1245
  return undefined
1070
1246
  }
1071
1247
 
1072
- // Handle v4 Objects AST structure
1073
- if (AST.isObjects(ast)) {
1074
- const result: Record<string, any> = { ...record }
1248
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, ExtendedClass, etc.)
1249
+ const objectsAst = S.AST.isObjects(ast)
1250
+ ? ast
1251
+ : S.AST.isDeclaration(ast)
1252
+ ? unwrapDeclaration(ast)
1253
+ : undefined
1254
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
1255
+ const result: Record<string, any> = {}
1075
1256
 
1076
- for (const prop of ast.propertySignatures) {
1257
+ for (const prop of objectsAst.propertySignatures) {
1077
1258
  const key = prop.name.toString()
1078
1259
  const propType = prop.type
1079
1260
 
1080
- // Get the property schema from the original schema's fields if available
1081
- // This preserves schema wrappers like withDefaultConstructor
1082
- let propSchema: S.Codec<any>
1083
- if ((schema as any).fields && (schema as any).fields[key]) {
1084
- propSchema = (schema as any).fields[key]
1085
- } else {
1086
- propSchema = S.make(propType)
1261
+ const propDefault = getDefaultFromAst(propType)
1262
+ if (propDefault !== undefined) {
1263
+ result[key] = propDefault
1264
+ continue
1087
1265
  }
1088
1266
 
1089
- // Recursively process the property to get its defaults
1267
+ const propSchema = S.make(propType)
1090
1268
  const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1091
1269
 
1092
1270
  if (propValue !== undefined) {
1093
1271
  result[key] = propValue
1272
+ } else if (isNullableOrUndefined(propType) === "undefined") {
1273
+ result[key] = undefined
1094
1274
  }
1095
1275
  }
1096
1276
 
1097
- return result
1277
+ return { ...result, ...record }
1098
1278
  }
1099
1279
 
1100
- // v3 compatible fields extraction
1101
- if (hasFields(schema)) {
1102
- // Process fields and extract default values
1103
- const result: Record<string, any> = {}
1104
-
1105
- for (const [key, fieldSchema] of Object.entries(schema.fields)) {
1106
- // Check if this field has a defaultValue in its AST
1107
- const fieldAst = (fieldSchema as any)?.ast
1108
- if (fieldAst?.defaultValue) {
1109
- try {
1110
- result[key] = fieldAst.defaultValue()
1111
- continue
1112
- } catch {
1113
- // If defaultValue() throws, fall through to recursive processing
1280
+ // Handle unions via AST or schema-level .members
1281
+ const unionTypes = S.AST.isUnion(ast)
1282
+ ? ast.types
1283
+ : hasMembers(schema)
1284
+ ? schema.members.map((m) => m.ast)
1285
+ : undefined
1286
+ if (unionTypes) {
1287
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
1288
+
1289
+ for (const memberAstRaw of unionTypes) {
1290
+ const memberAst = unwrapDeclaration(memberAstRaw)
1291
+ if (!S.AST.isObjects(memberAst)) continue
1292
+
1293
+ for (const prop of memberAst.propertySignatures) {
1294
+ const key = prop.name.toString()
1295
+ const fieldDefault = getDefaultFromAst(prop.type)
1296
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
1297
+
1298
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
1299
+ mergedFields[key] = { ast: prop.type }
1114
1300
  }
1115
1301
  }
1116
-
1117
- // Recursively process the field
1118
- const fieldValue = defaultsValueFromSchema(fieldSchema as any, record[key] || {})
1119
- if (fieldValue !== undefined) {
1120
- result[key] = fieldValue
1121
- }
1122
1302
  }
1123
1303
 
1124
- return { ...result, ...record }
1125
- }
1126
-
1127
- // Check if schema has fields in from (for ExtendedClass and similar transformations)
1128
- if ((schema as any)?.from && hasFields((schema as any).from)) {
1129
- return defaultsValueFromSchema((schema as any).from, record)
1130
- }
1131
-
1132
- if (hasMembers(schema)) {
1133
- // Merge all member fields, giving precedence to fields with default values
1134
- const mergedMembers = schema.members.reduce((acc, member) => {
1135
- if (hasFields(member)) {
1136
- // Check each field and give precedence to ones with default values
1137
- Object.entries(member.fields).forEach(([key, fieldSchema]) => {
1138
- const fieldAst: any = fieldSchema.ast
1139
- const existingFieldAst: any = acc[key]?.ast
1140
-
1141
- // If field doesn't exist yet, or new field has default and existing doesn't, use new field
1142
- if (!acc[key] || (fieldAst?.defaultValue && !existingFieldAst?.defaultValue)) {
1143
- acc[key] = fieldSchema
1144
- }
1145
- // If both have defaults or neither have defaults, keep the first one (existing)
1146
- })
1147
- return acc
1148
- }
1149
- return acc
1150
- }, {} as Record<string, any>)
1304
+ if (Object.keys(mergedFields).length === 0) {
1305
+ return Object.keys(record).length > 0 ? record : undefined
1306
+ }
1151
1307
 
1152
- // Use reduce to properly accumulate the merged fields
1153
- return Object.entries(mergedMembers).reduce((acc, [key, value]) => {
1154
- acc[key] = defaultsValueFromSchema(value, record[key] || {})
1308
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
1309
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
1155
1310
  return acc
1156
1311
  }, record)
1157
1312
  }
1158
1313
 
1159
1314
  if (Object.keys(record).length === 0) {
1160
- // Check for constructor defaults in v4's context
1161
- if (ast.context?.defaultValue) {
1162
- // In v4, defaultValue is an Encoding type, not directly callable
1163
- // For now, skip complex default extraction
1164
- // TODO: properly extract default from encoding chain
1315
+ if (S.AST.isString(ast)) {
1316
+ return ""
1165
1317
  }
1166
- }
1167
1318
 
1168
- if (AST.isString(ast)) {
1169
- return ""
1170
- }
1171
- if (AST.isBoolean(ast)) {
1172
- return false
1319
+ if (S.AST.isBoolean(ast)) {
1320
+ return false
1321
+ }
1173
1322
  }
1174
1323
  }