@effect-app/vue-components 4.0.0-beta.12 → 4.0.0-beta.120

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 +30 -15
  5. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +3 -5
  6. package/dist/types/utils/index.d.ts +6 -7
  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 +445 -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 +80 -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 +513 -353
  73. package/src/components/OmegaForm/OmegaInternalInput.vue +22 -6
  74. package/src/components/OmegaForm/useOmegaForm.ts +57 -36
  75. package/src/reset.css +52 -0
  76. package/src/utils/index.ts +9 -10
  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 -13
  85. package/dist/vue-components.es17.js +0 -725
  86. package/dist/vue-components.es18.js +0 -143
  87. package/dist/vue-components.es19.js +0 -6
  88. package/dist/vue-components.es2.js +0 -30
  89. package/dist/vue-components.es20.js +0 -13
  90. package/dist/vue-components.es21.js +0 -5
  91. package/dist/vue-components.es22.js +0 -26
  92. package/dist/vue-components.es23.js +0 -6
  93. package/dist/vue-components.es24.js +0 -10
  94. package/dist/vue-components.es25.js +0 -57
  95. package/dist/vue-components.es26.js +0 -71
  96. package/dist/vue-components.es27.js +0 -8
  97. package/dist/vue-components.es28.js +0 -8
  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 -5
  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 -4
  105. package/dist/vue-components.es35.js +0 -19
  106. package/dist/vue-components.es36.js +0 -320
  107. package/dist/vue-components.es37.js +0 -563
  108. package/dist/vue-components.es38.js +0 -29
  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 -66
  112. package/dist/vue-components.es41.js +0 -6
  113. package/dist/vue-components.es42.js +0 -6
  114. package/dist/vue-components.es43.js +0 -26
  115. package/dist/vue-components.es44.js +0 -77
  116. package/dist/vue-components.es45.js +0 -42
  117. package/dist/vue-components.es46.js +0 -316
  118. package/dist/vue-components.es47.js +0 -101
  119. package/dist/vue-components.es48.js +0 -33
  120. package/dist/vue-components.es49.js +0 -4
  121. package/dist/vue-components.es5.js +0 -52
  122. package/dist/vue-components.es50.js +0 -4
  123. package/dist/vue-components.es51.js +0 -4
  124. package/dist/vue-components.es52.js +0 -113
  125. package/dist/vue-components.es54.js +0 -9
  126. package/dist/vue-components.es55.js +0 -34
  127. package/dist/vue-components.es57.js +0 -194
  128. package/dist/vue-components.es59.js +0 -40
  129. package/dist/vue-components.es6.js +0 -69
  130. package/dist/vue-components.es60.js +0 -85
  131. package/dist/vue-components.es61.js +0 -43
  132. package/dist/vue-components.es62.js +0 -7
  133. package/dist/vue-components.es63.js +0 -6
  134. package/dist/vue-components.es64.js +0 -25
  135. package/dist/vue-components.es65.js +0 -7
  136. package/dist/vue-components.es66.js +0 -23
  137. package/dist/vue-components.es67.js +0 -32
  138. package/dist/vue-components.es68.js +0 -24
  139. package/dist/vue-components.es69.js +0 -14
  140. package/dist/vue-components.es7.js +0 -83
  141. package/dist/vue-components.es70.js +0 -7
  142. package/dist/vue-components.es71.js +0 -21
  143. package/dist/vue-components.es72.js +0 -11
  144. package/dist/vue-components.es73.js +0 -33
  145. package/dist/vue-components.es74.js +0 -50
  146. package/dist/vue-components.es75.js +0 -28
  147. package/dist/vue-components.es76.js +0 -103
  148. package/dist/vue-components.es77.js +0 -84
  149. package/dist/vue-components.es78.js +0 -17
  150. package/dist/vue-components.es79.js +0 -34
  151. package/dist/vue-components.es8.js +0 -63
  152. package/dist/vue-components.es80.js +0 -23
  153. package/dist/vue-components.es81.js +0 -14
  154. package/dist/vue-components.es82.js +0 -115
  155. package/dist/vue-components.es83.js +0 -5
  156. package/dist/vue-components.es84.js +0 -4
  157. package/dist/vue-components.es85.js +0 -4
  158. package/dist/vue-components.es86.js +0 -18
  159. package/dist/vue-components.es87.js +0 -72
  160. package/dist/vue-components.es88.js +0 -10
  161. package/dist/vue-components.es89.js +0 -4
  162. package/dist/vue-components.es9.js +0 -21
  163. package/dist/vue-components.es90.js +0 -17
  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>
@@ -236,6 +256,14 @@ export type FieldValidators<T> = {
236
256
  export type BaseFieldMeta = {
237
257
  required: boolean
238
258
  nullableOrUndefined?: false | "undefined" | "null"
259
+ /**
260
+ * True when the schema property is `S.optionalKey` (AST
261
+ * `context.isOptional`) — i.e. the key should be ABSENT from the submitted
262
+ * object when empty, not present with `undefined`. Distinct from
263
+ * `required: false`, which may also mean "empty string is valid" for
264
+ * unconstrained `S.String` fields.
265
+ */
266
+ isOptionalKey?: boolean
239
267
  }
240
268
 
241
269
  export type StringFieldMeta = BaseFieldMeta & {
@@ -262,13 +290,17 @@ export type SelectFieldMeta = BaseFieldMeta & {
262
290
  export type MultipleFieldMeta = BaseFieldMeta & {
263
291
  type: "multiple"
264
292
  members: any[] // TODO: should be non empty array?
265
- rest: readonly AST.AST[]
293
+ rest: readonly S.AST.AST[]
266
294
  }
267
295
 
268
296
  export type BooleanFieldMeta = BaseFieldMeta & {
269
297
  type: "boolean"
270
298
  }
271
299
 
300
+ export type DateFieldMeta = BaseFieldMeta & {
301
+ type: "date"
302
+ }
303
+
272
304
  export type UnknownFieldMeta = BaseFieldMeta & {
273
305
  type: "unknown"
274
306
  }
@@ -279,6 +311,7 @@ export type FieldMeta =
279
311
  | SelectFieldMeta
280
312
  | MultipleFieldMeta
281
313
  | BooleanFieldMeta
314
+ | DateFieldMeta
282
315
  | UnknownFieldMeta
283
316
 
284
317
  export type MetaRecord<T = string> = {
@@ -301,34 +334,57 @@ export type CreateMeta =
301
334
  }
302
335
  & (
303
336
  | {
304
- propertySignatures: readonly AST.PropertySignature[]
337
+ propertySignatures: readonly S.AST.PropertySignature[]
305
338
  property?: never
306
339
  }
307
340
  | {
308
341
  propertySignatures?: never
309
- property: AST.AST
342
+ property: S.AST.AST
310
343
  }
311
344
  )
312
345
 
313
- const getNullableOrUndefined = (property: AST.AST) => {
314
- if (!AST.isUnion(property)) return false
315
- return property.types.find((_) => AST.isUndefined(_) || _ === S.Null.ast)
346
+ const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
347
+ let current = getTransformationFrom(property)
348
+
349
+ while (S.AST.isDeclaration(current) && current.typeParameters.length > 0) {
350
+ current = getTransformationFrom(current.typeParameters[0]!)
351
+ }
352
+
353
+ return current
316
354
  }
317
355
 
318
- export const isNullableOrUndefined = (property: false | AST.AST | undefined) => {
319
- if (!property || !AST.isUnion(property)) return false
320
- if (property.types.find((_) => AST.isUndefined(_))) {
356
+ const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
357
+
358
+ /**
359
+ * Unwrap a single-element Union to its inner type if it's a Literal.
360
+ * After AST.toType, S.Struct({ _tag: S.Literal("X") }) produces Union([Literal("X")])
361
+ * instead of bare Literal("X") like S.TaggedStruct does.
362
+ * TODO: remove after manual _tag deprecation
363
+ */
364
+ const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
365
+ S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0]!)
366
+ ? ast.types[0]!
367
+ : ast
368
+
369
+ const getNullableOrUndefined = (property: S.AST.AST) =>
370
+ S.AST.isUnion(property)
371
+ ? property.types.find((_) => isNullishType(_))
372
+ : false
373
+
374
+ export const isNullableOrUndefined = (property: false | S.AST.AST | undefined) => {
375
+ if (!property || !S.AST.isUnion(property)) return false
376
+ if (property.types.find((_) => S.AST.isUndefined(_))) {
321
377
  return "undefined"
322
378
  }
323
- if (property.types.find((_) => _ === S.Null.ast)) return "null"
379
+ if (property.types.find((_) => S.AST.isNull(_))) return "null"
324
380
  return false
325
381
  }
326
382
 
327
383
  // 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[] = []
384
+ const unwrapNestedUnions = (types: readonly S.AST.AST[]): readonly S.AST.AST[] => {
385
+ const result: S.AST.AST[] = []
330
386
  for (const type of types) {
331
- if (AST.isUnion(type)) {
387
+ if (S.AST.isUnion(type)) {
332
388
  // Recursively unwrap nested unions
333
389
  const unwrapped = unwrapNestedUnions(type.types)
334
390
  result.push(...unwrapped)
@@ -339,31 +395,127 @@ const unwrapNestedUnions = (types: readonly AST.AST[]): readonly AST.AST[] => {
339
395
  return result
340
396
  }
341
397
 
398
+ const getNonNullTypes = (types: readonly S.AST.AST[]) =>
399
+ unwrapNestedUnions(types)
400
+ .map(unwrapDeclaration)
401
+ .filter((_) => !isNullishType(_))
402
+
403
+ const getJsonSchemaAnnotation = (property: S.AST.AST): Record<string, unknown> => {
404
+ const jsonSchema = S.AST.resolve(property)?.jsonSchema
405
+ return jsonSchema && typeof jsonSchema === "object" ? jsonSchema as Record<string, unknown> : {}
406
+ }
407
+
408
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
409
+ if (!link?.transformation?.decode?.run) return undefined
410
+ try {
411
+ const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
412
+ return Option.isSome(result) ? result.value : undefined
413
+ } catch {
414
+ return undefined
415
+ }
416
+ }
417
+
418
+ const getDefaultFromAst = (property: S.AST.AST) => {
419
+ // 1. Check withConstructorDefault (stored in context.defaultValue)
420
+ const constructorLink = property.context?.defaultValue?.[0]
421
+ const constructorDefault = extractDefaultFromLink(constructorLink)
422
+ if (constructorDefault !== undefined) return constructorDefault
423
+
424
+ // 2. Check withDecodingDefault (stored in encoding)
425
+ const encodingLink = property.encoding?.[0]
426
+ if (encodingLink && property.context?.isOptional) {
427
+ return extractDefaultFromLink(encodingLink)
428
+ }
429
+
430
+ return undefined
431
+ }
432
+
433
+ const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
434
+ const checks = property.checks ?? []
435
+
436
+ return checks.flatMap((check) => {
437
+ if (check._tag === "FilterGroup") {
438
+ return check.checks.flatMap((inner) => {
439
+ const meta = inner.annotations?.meta
440
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
441
+ })
442
+ }
443
+
444
+ const meta = check.annotations?.meta
445
+ return meta && typeof meta === "object" ? [meta as Record<string, any>] : []
446
+ })
447
+ }
448
+
449
+ const getFieldMetadataFromAst = (property: S.AST.AST) => {
450
+ const base: Partial<FieldMeta> & Record<string, unknown> = {
451
+ description: S.AST.resolveDescription(property)
452
+ }
453
+ const checks = getCheckMetas(property)
454
+
455
+ if (S.AST.isString(property)) {
456
+ base.type = "string"
457
+ for (const check of checks) {
458
+ switch (check._tag) {
459
+ case "isMinLength":
460
+ base.minLength = check.minLength
461
+ break
462
+ case "isMaxLength":
463
+ base.maxLength = check.maxLength
464
+ break
465
+ }
466
+ }
467
+
468
+ if (S.AST.resolveTitle(property) === "Email") {
469
+ base.format = "email"
470
+ }
471
+ } else if (S.AST.isNumber(property)) {
472
+ base.type = "number"
473
+ for (const check of checks) {
474
+ switch (check._tag) {
475
+ case "isInt":
476
+ base.refinement = "int"
477
+ break
478
+ case "isGreaterThanOrEqualTo":
479
+ base.minimum = check.minimum
480
+ break
481
+ case "isLessThanOrEqualTo":
482
+ base.maximum = check.maximum
483
+ break
484
+ case "isBetween":
485
+ base.minimum = check.minimum
486
+ base.maximum = check.maximum
487
+ break
488
+ case "isGreaterThan":
489
+ base.exclusiveMinimum = check.exclusiveMinimum
490
+ break
491
+ case "isLessThan":
492
+ base.exclusiveMaximum = check.exclusiveMaximum
493
+ break
494
+ }
495
+ }
496
+ } else if (S.AST.isBoolean(property)) {
497
+ base.type = "boolean"
498
+ } else if (
499
+ S.AST.isDeclaration(property)
500
+ && (property.annotations as any)?.typeConstructor?._tag === "Date"
501
+ ) {
502
+ base.type = "date"
503
+ } else {
504
+ base.type = "unknown"
505
+ }
506
+
507
+ return base
508
+ }
509
+
342
510
  export const createMeta = <T = any>(
343
511
  { meta = {}, parent = "", property, propertySignatures }: CreateMeta,
344
512
  acc: Partial<MetaRecord<T>> = {}
345
513
  ): 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
514
+ if (property) {
515
+ property = unwrapDeclaration(property)
364
516
  }
365
517
 
366
- if (property && AST.isObjects(property)) {
518
+ if (property && S.AST.isObjects(property)) {
367
519
  return createMeta<T>({
368
520
  meta,
369
521
  propertySignatures: property.propertySignatures
@@ -375,12 +527,11 @@ export const createMeta = <T = any>(
375
527
  const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
376
528
  const nullableOrUndefined = isNullableOrUndefined(p.type)
377
529
 
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"
530
+ const isOptionalKey = (p.type as any).context?.isOptional === true
381
531
 
382
532
  // Determine if this field should be required:
383
533
  // - For nullable discriminated unions, only _tag should be non-required
534
+ // - optionalKey fields are not required
384
535
  // - All other fields should calculate their required status normally
385
536
  let isRequired: boolean
386
537
  if (meta._isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
@@ -389,25 +540,18 @@ export const createMeta = <T = any>(
389
540
  } else if (meta.required === false) {
390
541
  // Explicitly set to non-required (legacy behavior for backwards compatibility)
391
542
  isRequired = false
543
+ } else if (isOptionalKey) {
544
+ isRequired = false
392
545
  } else {
393
546
  // Calculate from the property itself
394
547
  isRequired = !nullableOrUndefined
395
548
  }
396
549
 
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)
550
+ const typeToProcess = unwrapDeclaration(p.type)
551
+ if (S.AST.isUnion(p.type)) {
552
+ const nonNullTypes = getNonNullTypes(p.type.types)
407
553
 
408
- const hasStructMembers = nonNullTypes.some(
409
- (t) => AST.isObjects(t)
410
- )
554
+ const hasStructMembers = nonNullTypes.some(S.AST.isObjects)
411
555
 
412
556
  if (hasStructMembers) {
413
557
  // Only create parent meta for non-NullOr unions to avoid duplicates
@@ -422,25 +566,38 @@ export const createMeta = <T = any>(
422
566
 
423
567
  // Process each non-null type and merge their metadata
424
568
  for (const nonNullType of nonNullTypes) {
425
- if (AST.isObjects(nonNullType)) {
569
+ if (S.AST.isObjects(nonNullType)) {
426
570
  // For discriminated unions (multiple branches):
427
571
  // - If the parent union is nullable, only _tag should be non-required
428
572
  // - All other fields maintain their normal required status based on their own types
429
573
  const isNullableDiscriminatedUnion = nullableOrUndefined && nonNullTypes.length > 1
430
574
 
431
- Object.assign(
432
- acc,
433
- createMeta<T>({
434
- parent: key,
435
- propertySignatures: nonNullType.propertySignatures,
436
- meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
437
- })
438
- )
575
+ const branchMeta = createMeta<T>({
576
+ parent: key,
577
+ propertySignatures: nonNullType.propertySignatures,
578
+ meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
579
+ })
580
+
581
+ // Merge branch metadata, combining select members for shared discriminator fields
582
+ for (const [metaKey, metaValue] of Object.entries(branchMeta)) {
583
+ const existing = acc[metaKey as NestedKeyOf<T>] as FieldMeta | undefined
584
+ if (
585
+ existing && existing.type === "select" && (metaValue as any)?.type === "select"
586
+ ) {
587
+ existing.members = [
588
+ ...existing.members,
589
+ ...(metaValue as SelectFieldMeta).members.filter(
590
+ (m: any) => !existing.members.includes(m)
591
+ )
592
+ ]
593
+ } else {
594
+ acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
595
+ }
596
+ }
439
597
  }
440
598
  }
441
599
  } else {
442
- // Check if any of the union types are arrays
443
- const arrayTypes = nonNullTypes.filter(AST.isArrays)
600
+ const arrayTypes = nonNullTypes.filter(S.AST.isArrays)
444
601
  if (arrayTypes.length > 0) {
445
602
  const arrayType = arrayTypes[0] // Take the first array type
446
603
 
@@ -454,8 +611,8 @@ export const createMeta = <T = any>(
454
611
 
455
612
  // If the array has struct elements, also create metadata for their properties
456
613
  if (arrayType.rest && arrayType.rest.length > 0) {
457
- const restElement = arrayType.rest[0]
458
- if (AST.isObjects(restElement)) {
614
+ const restElement = unwrapDeclaration(arrayType.rest[0]!)
615
+ if (S.AST.isObjects(restElement)) {
459
616
  for (const prop of restElement.propertySignatures) {
460
617
  const propKey = `${key}.${prop.name.toString()}`
461
618
 
@@ -473,12 +630,12 @@ export const createMeta = <T = any>(
473
630
  acc[propKey as NestedKeyOf<T>] = propMeta as FieldMeta
474
631
 
475
632
  if (
476
- propMeta.type === "multiple" && AST.isArrays(prop.type) && prop
633
+ propMeta.type === "multiple" && S.AST.isArrays(prop.type) && prop
477
634
  .type
478
635
  .rest && prop.type.rest.length > 0
479
636
  ) {
480
- const nestedRestElement = prop.type.rest[0]
481
- if (AST.isObjects(nestedRestElement)) {
637
+ const nestedRestElement = unwrapDeclaration(prop.type.rest[0]!)
638
+ if (S.AST.isObjects(nestedRestElement)) {
482
639
  for (const nestedProp of nestedRestElement.propertySignatures) {
483
640
  const nestedPropKey = `${propKey}.${nestedProp.name.toString()}`
484
641
 
@@ -513,34 +670,32 @@ export const createMeta = <T = any>(
513
670
  }
514
671
  }
515
672
  } else {
516
- // Unwrap transformations (like ExtendedClass) to check for propertySignatures
517
- const unwrappedTypeToProcess = getTransformationFrom(typeToProcess)
518
- if (AST.isObjects(unwrappedTypeToProcess)) {
673
+ if (S.AST.isObjects(typeToProcess)) {
519
674
  Object.assign(
520
675
  acc,
521
676
  createMeta<T>({
522
677
  parent: key,
523
- propertySignatures: unwrappedTypeToProcess.propertySignatures,
678
+ propertySignatures: typeToProcess.propertySignatures,
524
679
  meta: { required: isRequired, nullableOrUndefined }
525
680
  })
526
681
  )
527
- } else if (AST.isArrays(p.type)) {
682
+ } else if (S.AST.isArrays(p.type)) {
528
683
  // Check if it has struct elements
529
684
  const hasStructElements = p.type.rest.length > 0
530
- && AST.isObjects(p.type.rest[0])
685
+ && S.AST.isObjects(unwrapDeclaration(p.type.rest[0]!))
531
686
 
532
687
  if (hasStructElements) {
533
688
  // 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)) {
689
+ const elementType = unwrapDeclaration(p.type.rest[0]!)
690
+ if (S.AST.isObjects(elementType)) {
536
691
  // Process each property in the array element
537
692
  for (const prop of elementType.propertySignatures) {
538
693
  const propKey = `${key}.${prop.name.toString()}`
539
694
 
540
695
  // 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)) {
696
+ if (S.AST.isArrays(prop.type) && prop.type.rest.length > 0) {
697
+ const nestedElementType = unwrapDeclaration(prop.type.rest[0]!)
698
+ if (S.AST.isObjects(nestedElementType)) {
544
699
  // Array with struct elements - process nested fields
545
700
  for (const nestedProp of nestedElementType.propertySignatures) {
546
701
  const nestedKey = `${propKey}.${nestedProp.name.toString()}`
@@ -592,9 +747,12 @@ export const createMeta = <T = any>(
592
747
  parent: key,
593
748
  property: p.type,
594
749
  meta: {
595
- required: isRequired,
750
+ // an empty string is valid for a S.String field, so we should not mark it as required
751
+ // TODO: handle this better via the createMeta minLength parsing
752
+ required: isRequired
753
+ && (!S.AST.isString(typeToProcess) || !!getFieldMetadataFromAst(typeToProcess).minLength),
596
754
  nullableOrUndefined,
597
- ...(isIntField && { refinement: "int" })
755
+ ...(isOptionalKey ? { isOptionalKey: true } : {})
598
756
  }
599
757
  })
600
758
 
@@ -607,19 +765,30 @@ export const createMeta = <T = any>(
607
765
 
608
766
  if (property) {
609
767
  const nullableOrUndefined = getNullableOrUndefined(property)
768
+ property = unwrapDeclaration(property)
610
769
 
611
770
  if (!Object.hasOwnProperty.call(meta, "required")) {
612
771
  meta["required"] = !nullableOrUndefined
613
772
  }
614
773
 
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
- )!
774
+ if (S.AST.isUnion(property)) {
775
+ const unwrappedTypes = unwrapNestedUnions(property.types).map(unwrapDeclaration)
776
+ const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
777
+
778
+ // Unwrap single-element unions when the literal is a boolean
779
+ // (effect-app's S.Literal wraps as S.Literals([x]) → Union([Literal(x)]))
780
+ // Don't unwrap string/number literals — they may be discriminator values in a union
781
+ if (
782
+ nonNullTypes.length === 1
783
+ && S.AST.isLiteral(nonNullTypes[0]!)
784
+ && typeof nonNullTypes[0]!.literal === "boolean"
785
+ ) {
786
+ return createMeta<T>({ parent, meta, property: nonNullTypes[0]! })
787
+ }
788
+
789
+ const nonNullType = nonNullTypes[0]!
621
790
 
622
- if (AST.isObjects(nonNullType)) {
791
+ if (S.AST.isObjects(nonNullType)) {
623
792
  return createMeta<T>({
624
793
  propertySignatures: nonNullType.propertySignatures,
625
794
  parent,
@@ -627,11 +796,13 @@ export const createMeta = <T = any>(
627
796
  })
628
797
  }
629
798
 
630
- if (unwrappedTypes.every(AST.isLiteral)) {
799
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
800
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
801
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
631
802
  return {
632
803
  ...meta,
633
804
  type: "select",
634
- members: unwrappedTypes.map((t) => t.literal)
805
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
635
806
  } as FieldMeta
636
807
  }
637
808
 
@@ -645,7 +816,7 @@ export const createMeta = <T = any>(
645
816
  } as FieldMeta
646
817
  }
647
818
 
648
- if (AST.isArrays(property)) {
819
+ if (S.AST.isArrays(property)) {
649
820
  return {
650
821
  ...meta,
651
822
  type: "multiple",
@@ -654,38 +825,15 @@ export const createMeta = <T = any>(
654
825
  } as FieldMeta
655
826
  }
656
827
 
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
828
+ if (S.AST.isLiteral(property)) {
829
+ return {
830
+ ...meta,
831
+ type: "select",
832
+ members: [property.literal]
833
+ } as FieldMeta
683
834
  }
684
835
 
685
- // Always ensure required is set before returning
686
- if (!Object.hasOwnProperty.call(meta, "required")) {
687
- meta["required"] = !nullableOrUndefined
688
- }
836
+ meta = { ...getJsonSchemaAnnotation(property), ...getFieldMetadataFromAst(property), ...meta }
689
837
 
690
838
  return meta as FieldMeta
691
839
  }
@@ -711,27 +859,21 @@ const flattenMeta = <T>(meta: MetaRecord<T> | FieldMeta, parentKey: string = "")
711
859
  return result
712
860
  }
713
861
 
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
862
+ const metadataFromAst = <From, To>(
863
+ schema: S.Codec<To, From, never>
718
864
  ): { meta: MetaRecord<To>; defaultValues: Record<string, any>; unionMeta: Record<string, MetaRecord<To>> } => {
719
- const ast = schema.ast
865
+ const ast = unwrapDeclaration(schema.ast)
720
866
  const newMeta: MetaRecord<To> = {}
721
867
  const defaultValues: Record<string, any> = {}
722
868
  const unionMeta: Record<string, MetaRecord<To>> = {}
723
869
 
724
870
  // Handle root-level Union types (discriminated unions)
725
- if (AST.isUnion(ast)) {
726
- const types = ast.types
727
-
871
+ if (S.AST.isUnion(ast)) {
728
872
  // 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)
873
+ const nonNullTypes = getNonNullTypes(ast.types)
732
874
 
733
875
  // Check if this is a discriminated union (all members are structs)
734
- const allStructs = nonNullTypes.every((t: any) => AST.isObjects(t))
876
+ const allStructs = nonNullTypes.every(S.AST.isObjects)
735
877
 
736
878
  if (allStructs && nonNullTypes.length > 0) {
737
879
  // Extract discriminator values from each union member
@@ -739,16 +881,32 @@ const metadataFromAst = <_From, To>(
739
881
 
740
882
  // Store metadata for each union member by its tag value
741
883
  for (const memberType of nonNullTypes) {
742
- if (AST.isObjects(memberType)) {
884
+ if (S.AST.isObjects(memberType)) {
743
885
  // Find the discriminator field (usually _tag)
744
886
  const tagProp = memberType.propertySignatures.find(
745
- (p: any) => p.name.toString() === "_tag"
887
+ (p) => p.name.toString() === "_tag"
746
888
  )
747
889
 
748
890
  let tagValue: string | null = null
749
- if (tagProp && AST.isLiteral(tagProp.type)) {
750
- tagValue = tagProp.type.literal as string
891
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
892
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
893
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
894
+ tagValue = resolvedTagType.literal as string
751
895
  discriminatorValues.push(tagValue)
896
+ // Warn if the tag was wrapped in a single-element Union (legacy pattern)
897
+ if (
898
+ tagProp
899
+ && S.AST.isUnion(tagProp.type)
900
+ && isDevelopmentEnvironment()
901
+ && tagValue != null
902
+ && !legacyTagWarningEmittedFor.has(tagValue)
903
+ ) {
904
+ legacyTagWarningEmittedFor.add(tagValue)
905
+ console.warn(
906
+ `[OmegaForm] Union member with _tag "${tagValue}" uses S.Struct({ _tag: S.Literal("${tagValue}"), ... }). `
907
+ + `Please migrate to S.TaggedStruct("${tagValue}", { ... }) for cleaner AST handling.`
908
+ )
909
+ }
752
910
  }
753
911
 
754
912
  // Create metadata for this member's properties
@@ -779,7 +937,7 @@ const metadataFromAst = <_From, To>(
779
937
  }
780
938
  }
781
939
 
782
- if (AST.isObjects(ast)) {
940
+ if (S.AST.isObjects(ast)) {
783
941
  const meta = createMeta<To>({
784
942
  propertySignatures: ast.propertySignatures
785
943
  })
@@ -808,16 +966,80 @@ const metadataFromAst = <_From, To>(
808
966
  return { meta: newMeta, defaultValues, unionMeta }
809
967
  }
810
968
 
969
+ /*
970
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
971
+ * These need to be swapped to S.RedactedFromValue for form usage
972
+ * because S.Redacted expects Redacted objects, not plain strings.
973
+ */
974
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
975
+ S.AST.isDeclaration(ast)
976
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
977
+ && !ast.encoding
978
+
979
+ /*
980
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
981
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
982
+ * Redacted<A> on both encoded and type sides, so form inputs (which
983
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
984
+ * plain values on the encoded side and wraps them in Redacted on decode.
985
+ */
986
+ export const toFormSchema = <From, To>(
987
+ schema: S.Codec<To, From, never>
988
+ ): S.Codec<To, From, never> => {
989
+ const ast = schema.ast
990
+ const objAst = S.AST.isObjects(ast)
991
+ ? ast
992
+ : S.AST.isDeclaration(ast)
993
+ ? S.AST.toEncoded(ast)
994
+ : null
995
+
996
+ if (!objAst || !("propertySignatures" in objAst)) return schema
997
+
998
+ let hasRedacted = false
999
+ const props: Record<string, S.Struct.Fields[string]> = {}
1000
+
1001
+ for (const p of objAst.propertySignatures) {
1002
+ if (isRedactedWithoutEncoding(p.type)) {
1003
+ hasRedacted = true
1004
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
1005
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
1006
+ } else if (S.AST.isUnion(p.type)) {
1007
+ const types = p.type.types
1008
+ const redactedType = types.find(isRedactedWithoutEncoding)
1009
+ if (redactedType) {
1010
+ hasRedacted = true
1011
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
1012
+ const hasNull = types.some(S.AST.isNull)
1013
+ const hasUndefined = types.some(S.AST.isUndefined)
1014
+ const base = S.RedactedFromValue(innerSchema)
1015
+ props[p.name as string] = hasNull && hasUndefined
1016
+ ? S.NullishOr(base)
1017
+ : hasNull
1018
+ ? S.NullOr(base)
1019
+ : hasUndefined
1020
+ ? S.UndefinedOr(base)
1021
+ : base
1022
+ } else {
1023
+ props[p.name as string] = S.make(p.type)
1024
+ }
1025
+ } else {
1026
+ props[p.name as string] = S.make(p.type)
1027
+ }
1028
+ }
1029
+
1030
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
1031
+ }
1032
+
811
1033
  export const duplicateSchema = <From, To>(
812
1034
  schema: S.Codec<To, From, never>
813
1035
  ) => {
814
1036
  return schema
815
1037
  }
816
1038
 
817
- export const generateMetaFromSchema = <_From, To>(
818
- schema: any // v4 Schema type is complex, use any for now
1039
+ export const generateMetaFromSchema = <From, To>(
1040
+ schema: S.Codec<To, From, never>
819
1041
  ): {
820
- schema: any
1042
+ schema: S.Codec<To, From, never>
821
1043
  meta: MetaRecord<To>
822
1044
  unionMeta: Record<string, MetaRecord<To>>
823
1045
  } => {
@@ -833,168 +1055,138 @@ export const generateInputStandardSchemaFromFieldMeta = (
833
1055
  if (!trans) {
834
1056
  trans = useIntl().trans
835
1057
  }
836
- let schema: S.Codec<any>
837
-
1058
+ let schema: any
838
1059
  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
- }
1060
+ case "string":
1061
+ schema = meta.format === "email"
1062
+ ? S.Email.annotate({
1063
+ message: trans("validation.email.invalid")
1064
+ })
1065
+ : S.String.annotate({
1066
+ message: trans("validation.empty")
1067
+ })
852
1068
 
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
- }
1069
+ if (meta.required) {
1070
+ schema = schema.check(S.isMinLength(1, {
1071
+ message: trans("validation.empty")
1072
+ }))
864
1073
  }
865
1074
 
866
1075
  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
- )
1076
+ schema = schema.check(S.isMaxLength(meta.maxLength, {
1077
+ message: trans("validation.string.maxLength", {
1078
+ maxLength: meta.maxLength
1079
+ })
1080
+ }))
1081
+ }
1082
+ if (typeof meta.minLength === "number") {
1083
+ schema = schema.check(S.isMinLength(meta.minLength, {
1084
+ message: trans("validation.string.minLength", {
1085
+ minLength: meta.minLength
1086
+ })
1087
+ }))
873
1088
  }
874
1089
  break
875
- }
876
1090
 
877
- case "number": {
1091
+ case "number":
878
1092
  if (meta.refinement === "int") {
879
- schema = S.Int
1093
+ schema = S
1094
+ .Number
1095
+ .annotate({
1096
+ message: trans("validation.empty")
1097
+ })
1098
+ .check(S.isInt({
1099
+ message: trans("validation.integer.expected", { actualValue: "NaN" })
1100
+ }))
880
1101
  } else {
881
- schema = S.Number
1102
+ schema = S.Finite.annotate({
1103
+ message: trans("validation.number.expected", { actualValue: "NaN" })
1104
+ })
1105
+
1106
+ if (meta.required) {
1107
+ schema = schema.annotate({
1108
+ message: trans("validation.empty")
1109
+ })
1110
+ }
882
1111
  }
883
1112
 
884
- // Apply numeric validations
885
1113
  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
- )
1114
+ schema = schema.check(S.isGreaterThanOrEqualTo(meta.minimum, {
1115
+ message: trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
1116
+ minimum: meta.minimum,
1117
+ isExclusive: true
1118
+ })
1119
+ }))
896
1120
  }
897
-
898
1121
  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
- )
1122
+ schema = schema.check(S.isLessThanOrEqualTo(meta.maximum, {
1123
+ message: trans("validation.number.max", {
1124
+ maximum: meta.maximum,
1125
+ isExclusive: true
1126
+ })
1127
+ }))
909
1128
  }
910
-
911
1129
  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
- )
1130
+ schema = schema.check(S.isGreaterThan(meta.exclusiveMinimum, {
1131
+ message: trans(meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min", {
1132
+ minimum: meta.exclusiveMinimum,
1133
+ isExclusive: false
1134
+ })
1135
+ }))
922
1136
  }
923
-
924
1137
  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
- )
1138
+ schema = schema.check(S.isLessThan(meta.exclusiveMaximum, {
1139
+ message: trans("validation.number.max", {
1140
+ maximum: meta.exclusiveMaximum,
1141
+ isExclusive: false
1142
+ })
1143
+ }))
935
1144
  }
936
1145
  break
937
- }
1146
+ case "select":
1147
+ schema = S.Literals(meta.members as [any, ...any[]]).annotate({
1148
+ message: trans("validation.not_a_valid", {
1149
+ type: "select",
1150
+ message: meta.members.join(", ")
1151
+ })
1152
+ })
938
1153
 
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
1154
  break
950
- }
951
1155
 
952
- case "multiple": {
953
- schema = S.Array(S.String)
1156
+ case "multiple":
1157
+ schema = S.Array(S.String).annotate({
1158
+ message: trans("validation.not_a_valid", {
1159
+ type: "multiple",
1160
+ message: meta.members.join(", ")
1161
+ })
1162
+ })
954
1163
  break
955
- }
956
1164
 
957
- case "boolean": {
1165
+ case "boolean":
958
1166
  schema = S.Boolean
959
1167
  break
960
- }
961
1168
 
962
- case "unknown": {
1169
+ case "date":
1170
+ schema = S.Date
1171
+ break
1172
+
1173
+ case "unknown":
963
1174
  schema = S.Unknown
964
1175
  break
965
- }
966
1176
 
967
- default: {
968
- console.warn(`Unhandled field type: ${(meta as any).type}`)
1177
+ default:
1178
+ // For any unhandled types, use Unknown schema to prevent undefined errors
1179
+ console.warn(`Unhandled field type: ${meta}`)
969
1180
  schema = S.Unknown
970
1181
  break
971
- }
972
1182
  }
973
-
974
- // Wrap in union with null/undefined if not required
975
1183
  if (!meta.required) {
976
- // v4 Union takes an array of schemas
977
- schema = S.Union([schema, S.Null, S.Undefined])
1184
+ schema = S.NullishOr(schema)
978
1185
  }
979
-
980
- return S.toStandardSchemaV1(schema as any)
1186
+ const result = S.toStandardSchemaV1(schema as any)
1187
+ return result
981
1188
  }
982
1189
 
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
1190
  export type OmegaAutoGenMeta<
999
1191
  From extends Record<PropertyKey, any>,
1000
1192
  To extends Record<PropertyKey, any>,
@@ -1037,18 +1229,8 @@ export function deepMerge(target: any, source: any) {
1037
1229
  return result
1038
1230
  }
1039
1231
 
1040
- // Type definitions for schemas with fields and members
1041
- type SchemaWithFields = {
1042
- fields: Record<string, S.Top>
1043
- }
1044
-
1045
1232
  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"
1233
+ members: readonly S.Schema<any>[]
1052
1234
  }
1053
1235
 
1054
1236
  function hasMembers(schema: any): schema is SchemaWithMembers {
@@ -1057,10 +1239,15 @@ function hasMembers(schema: any): schema is SchemaWithMembers {
1057
1239
 
1058
1240
  // Internal implementation with WeakSet tracking
1059
1241
  export const defaultsValueFromSchema = (
1060
- schema: S.Codec<any>,
1242
+ schema: S.Schema<any>,
1061
1243
  record: Record<string, any> = {}
1062
1244
  ): any => {
1063
1245
  const ast = schema.ast
1246
+ const defaultValue = getDefaultFromAst(ast)
1247
+
1248
+ if (defaultValue !== undefined) {
1249
+ return defaultValue
1250
+ }
1064
1251
 
1065
1252
  if (isNullableOrUndefined(schema.ast) === "null") {
1066
1253
  return null
@@ -1069,106 +1256,79 @@ export const defaultsValueFromSchema = (
1069
1256
  return undefined
1070
1257
  }
1071
1258
 
1072
- // Handle v4 Objects AST structure
1073
- if (AST.isObjects(ast)) {
1074
- const result: Record<string, any> = { ...record }
1259
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, ExtendedClass, etc.)
1260
+ const objectsAst = S.AST.isObjects(ast)
1261
+ ? ast
1262
+ : S.AST.isDeclaration(ast)
1263
+ ? unwrapDeclaration(ast)
1264
+ : undefined
1265
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
1266
+ const result: Record<string, any> = {}
1075
1267
 
1076
- for (const prop of ast.propertySignatures) {
1268
+ for (const prop of objectsAst.propertySignatures) {
1077
1269
  const key = prop.name.toString()
1078
1270
  const propType = prop.type
1079
1271
 
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)
1272
+ const propDefault = getDefaultFromAst(propType)
1273
+ if (propDefault !== undefined) {
1274
+ result[key] = propDefault
1275
+ continue
1087
1276
  }
1088
1277
 
1089
- // Recursively process the property to get its defaults
1278
+ const propSchema = S.make(propType)
1090
1279
  const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1091
1280
 
1092
1281
  if (propValue !== undefined) {
1093
1282
  result[key] = propValue
1283
+ } else if (isNullableOrUndefined(propType) === "undefined") {
1284
+ result[key] = undefined
1094
1285
  }
1095
1286
  }
1096
1287
 
1097
- return result
1288
+ return { ...result, ...record }
1098
1289
  }
1099
1290
 
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
1291
+ // Handle unions via AST or schema-level .members
1292
+ const unionTypes = S.AST.isUnion(ast)
1293
+ ? ast.types
1294
+ : hasMembers(schema)
1295
+ ? schema.members.map((m) => m.ast)
1296
+ : undefined
1297
+ if (unionTypes) {
1298
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
1299
+
1300
+ for (const memberAstRaw of unionTypes) {
1301
+ const memberAst = unwrapDeclaration(memberAstRaw)
1302
+ if (!S.AST.isObjects(memberAst)) continue
1303
+
1304
+ for (const prop of memberAst.propertySignatures) {
1305
+ const key = prop.name.toString()
1306
+ const fieldDefault = getDefaultFromAst(prop.type)
1307
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
1308
+
1309
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
1310
+ mergedFields[key] = { ast: prop.type }
1114
1311
  }
1115
1312
  }
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
1313
  }
1123
1314
 
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>)
1315
+ if (Object.keys(mergedFields).length === 0) {
1316
+ return Object.keys(record).length > 0 ? record : undefined
1317
+ }
1151
1318
 
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] || {})
1319
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
1320
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
1155
1321
  return acc
1156
1322
  }, record)
1157
1323
  }
1158
1324
 
1159
1325
  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
1326
+ if (S.AST.isString(ast)) {
1327
+ return ""
1165
1328
  }
1166
- }
1167
1329
 
1168
- if (AST.isString(ast)) {
1169
- return ""
1170
- }
1171
- if (AST.isBoolean(ast)) {
1172
- return false
1330
+ if (S.AST.isBoolean(ast)) {
1331
+ return false
1332
+ }
1173
1333
  }
1174
1334
  }