@effect-app/vue-components 4.0.0-beta.6 → 4.0.0-beta.61

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 +51 -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 +440 -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 +29 -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 +498 -357
  73. package/src/components/OmegaForm/OmegaInternalInput.vue +9 -5
  74. package/src/components/OmegaForm/useOmegaForm.ts +57 -36
  75. package/src/reset.css +51 -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 -194
  107. package/dist/vue-components.es38.js +0 -320
  108. package/dist/vue-components.es39.js +0 -563
  109. package/dist/vue-components.es4.js +0 -52
  110. package/dist/vue-components.es40.js +0 -29
  111. package/dist/vue-components.es41.js +0 -54
  112. package/dist/vue-components.es42.js +0 -66
  113. package/dist/vue-components.es43.js +0 -6
  114. package/dist/vue-components.es44.js +0 -6
  115. package/dist/vue-components.es45.js +0 -26
  116. package/dist/vue-components.es46.js +0 -77
  117. package/dist/vue-components.es47.js +0 -42
  118. package/dist/vue-components.es48.js +0 -316
  119. package/dist/vue-components.es49.js +0 -101
  120. package/dist/vue-components.es5.js +0 -52
  121. package/dist/vue-components.es50.js +0 -33
  122. package/dist/vue-components.es51.js +0 -4
  123. package/dist/vue-components.es52.js +0 -4
  124. package/dist/vue-components.es53.js +0 -4
  125. package/dist/vue-components.es54.js +0 -113
  126. package/dist/vue-components.es56.js +0 -9
  127. package/dist/vue-components.es57.js +0 -34
  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 -23
  150. package/dist/vue-components.es79.js +0 -14
  151. package/dist/vue-components.es8.js +0 -63
  152. package/dist/vue-components.es80.js +0 -115
  153. package/dist/vue-components.es81.js +0 -5
  154. package/dist/vue-components.es82.js +0 -34
  155. package/dist/vue-components.es83.js +0 -4
  156. package/dist/vue-components.es84.js +0 -4
  157. package/dist/vue-components.es85.js +0 -17
  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>
@@ -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 withDefaultConstructor (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,7 @@ 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
683
- }
684
-
685
- // Always ensure required is set before returning
686
- if (!Object.hasOwnProperty.call(meta, "required")) {
687
- meta["required"] = !nullableOrUndefined
688
- }
817
+ meta = { ...getJsonSchemaAnnotation(property), ...getFieldMetadataFromAst(property), ...meta }
689
818
 
690
819
  return meta as FieldMeta
691
820
  }
@@ -711,27 +840,21 @@ const flattenMeta = <T>(meta: MetaRecord<T> | FieldMeta, parentKey: string = "")
711
840
  return result
712
841
  }
713
842
 
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
843
+ const metadataFromAst = <From, To>(
844
+ schema: S.Codec<To, From, never>
718
845
  ): { meta: MetaRecord<To>; defaultValues: Record<string, any>; unionMeta: Record<string, MetaRecord<To>> } => {
719
- const ast = schema.ast
846
+ const ast = unwrapDeclaration(schema.ast)
720
847
  const newMeta: MetaRecord<To> = {}
721
848
  const defaultValues: Record<string, any> = {}
722
849
  const unionMeta: Record<string, MetaRecord<To>> = {}
723
850
 
724
851
  // Handle root-level Union types (discriminated unions)
725
- if (AST.isUnion(ast)) {
726
- const types = ast.types
727
-
852
+ if (S.AST.isUnion(ast)) {
728
853
  // 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)
854
+ const nonNullTypes = getNonNullTypes(ast.types)
732
855
 
733
856
  // Check if this is a discriminated union (all members are structs)
734
- const allStructs = nonNullTypes.every((t: any) => AST.isObjects(t))
857
+ const allStructs = nonNullTypes.every(S.AST.isObjects)
735
858
 
736
859
  if (allStructs && nonNullTypes.length > 0) {
737
860
  // Extract discriminator values from each union member
@@ -739,16 +862,32 @@ const metadataFromAst = <_From, To>(
739
862
 
740
863
  // Store metadata for each union member by its tag value
741
864
  for (const memberType of nonNullTypes) {
742
- if (AST.isObjects(memberType)) {
865
+ if (S.AST.isObjects(memberType)) {
743
866
  // Find the discriminator field (usually _tag)
744
867
  const tagProp = memberType.propertySignatures.find(
745
- (p: any) => p.name.toString() === "_tag"
868
+ (p) => p.name.toString() === "_tag"
746
869
  )
747
870
 
748
871
  let tagValue: string | null = null
749
- if (tagProp && AST.isLiteral(tagProp.type)) {
750
- tagValue = tagProp.type.literal as string
872
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
873
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
874
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
875
+ tagValue = resolvedTagType.literal as string
751
876
  discriminatorValues.push(tagValue)
877
+ // Warn if the tag was wrapped in a single-element Union (legacy pattern)
878
+ if (
879
+ tagProp
880
+ && S.AST.isUnion(tagProp.type)
881
+ && isDevelopmentEnvironment()
882
+ && tagValue != null
883
+ && !legacyTagWarningEmittedFor.has(tagValue)
884
+ ) {
885
+ legacyTagWarningEmittedFor.add(tagValue)
886
+ console.warn(
887
+ `[OmegaForm] Union member with _tag "${tagValue}" uses S.Struct({ _tag: S.Literal("${tagValue}"), ... }). `
888
+ + `Please migrate to S.TaggedStruct("${tagValue}", { ... }) for cleaner AST handling.`
889
+ )
890
+ }
752
891
  }
753
892
 
754
893
  // Create metadata for this member's properties
@@ -779,7 +918,7 @@ const metadataFromAst = <_From, To>(
779
918
  }
780
919
  }
781
920
 
782
- if (AST.isObjects(ast)) {
921
+ if (S.AST.isObjects(ast)) {
783
922
  const meta = createMeta<To>({
784
923
  propertySignatures: ast.propertySignatures
785
924
  })
@@ -808,16 +947,80 @@ const metadataFromAst = <_From, To>(
808
947
  return { meta: newMeta, defaultValues, unionMeta }
809
948
  }
810
949
 
950
+ /*
951
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
952
+ * These need to be swapped to S.RedactedFromValue for form usage
953
+ * because S.Redacted expects Redacted objects, not plain strings.
954
+ */
955
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
956
+ S.AST.isDeclaration(ast)
957
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
958
+ && !ast.encoding
959
+
960
+ /*
961
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
962
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
963
+ * Redacted<A> on both encoded and type sides, so form inputs (which
964
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
965
+ * plain values on the encoded side and wraps them in Redacted on decode.
966
+ */
967
+ export const toFormSchema = <From, To>(
968
+ schema: S.Codec<To, From, never>
969
+ ): S.Codec<To, From, never> => {
970
+ const ast = schema.ast
971
+ const objAst = S.AST.isObjects(ast)
972
+ ? ast
973
+ : S.AST.isDeclaration(ast)
974
+ ? S.AST.toEncoded(ast)
975
+ : null
976
+
977
+ if (!objAst || !("propertySignatures" in objAst)) return schema
978
+
979
+ let hasRedacted = false
980
+ const props: Record<string, S.Struct.Fields[string]> = {}
981
+
982
+ for (const p of objAst.propertySignatures) {
983
+ if (isRedactedWithoutEncoding(p.type)) {
984
+ hasRedacted = true
985
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
986
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
987
+ } else if (S.AST.isUnion(p.type)) {
988
+ const types = p.type.types
989
+ const redactedType = types.find(isRedactedWithoutEncoding)
990
+ if (redactedType) {
991
+ hasRedacted = true
992
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
993
+ const hasNull = types.some(S.AST.isNull)
994
+ const hasUndefined = types.some(S.AST.isUndefined)
995
+ const base = S.RedactedFromValue(innerSchema)
996
+ props[p.name as string] = hasNull && hasUndefined
997
+ ? S.NullishOr(base)
998
+ : hasNull
999
+ ? S.NullOr(base)
1000
+ : hasUndefined
1001
+ ? S.UndefinedOr(base)
1002
+ : base
1003
+ } else {
1004
+ props[p.name as string] = S.make(p.type)
1005
+ }
1006
+ } else {
1007
+ props[p.name as string] = S.make(p.type)
1008
+ }
1009
+ }
1010
+
1011
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
1012
+ }
1013
+
811
1014
  export const duplicateSchema = <From, To>(
812
1015
  schema: S.Codec<To, From, never>
813
1016
  ) => {
814
1017
  return schema
815
1018
  }
816
1019
 
817
- export const generateMetaFromSchema = <_From, To>(
818
- schema: any // v4 Schema type is complex, use any for now
1020
+ export const generateMetaFromSchema = <From, To>(
1021
+ schema: S.Codec<To, From, never>
819
1022
  ): {
820
- schema: any
1023
+ schema: S.Codec<To, From, never>
821
1024
  meta: MetaRecord<To>
822
1025
  unionMeta: Record<string, MetaRecord<To>>
823
1026
  } => {
@@ -833,168 +1036,138 @@ export const generateInputStandardSchemaFromFieldMeta = (
833
1036
  if (!trans) {
834
1037
  trans = useIntl().trans
835
1038
  }
836
- let schema: S.Codec<any>
837
-
1039
+ let schema: any
838
1040
  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
- }
1041
+ case "string":
1042
+ schema = meta.format === "email"
1043
+ ? S.Email.annotate({
1044
+ message: trans("validation.email.invalid")
1045
+ })
1046
+ : S.String.annotate({
1047
+ message: trans("validation.empty")
1048
+ })
852
1049
 
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
- }
1050
+ if (meta.required) {
1051
+ schema = schema.check(S.isMinLength(1, {
1052
+ message: trans("validation.empty")
1053
+ }))
864
1054
  }
865
1055
 
866
1056
  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
- )
1057
+ schema = schema.check(S.isMaxLength(meta.maxLength, {
1058
+ message: trans("validation.string.maxLength", {
1059
+ maxLength: meta.maxLength
1060
+ })
1061
+ }))
1062
+ }
1063
+ if (typeof meta.minLength === "number") {
1064
+ schema = schema.check(S.isMinLength(meta.minLength, {
1065
+ message: trans("validation.string.minLength", {
1066
+ minLength: meta.minLength
1067
+ })
1068
+ }))
873
1069
  }
874
1070
  break
875
- }
876
1071
 
877
- case "number": {
1072
+ case "number":
878
1073
  if (meta.refinement === "int") {
879
- schema = S.Int
1074
+ schema = S
1075
+ .Number
1076
+ .annotate({
1077
+ message: trans("validation.empty")
1078
+ })
1079
+ .check(S.isInt({
1080
+ message: trans("validation.integer.expected", { actualValue: "NaN" })
1081
+ }))
880
1082
  } else {
881
- schema = S.Number
1083
+ schema = S.Finite.annotate({
1084
+ message: trans("validation.number.expected", { actualValue: "NaN" })
1085
+ })
1086
+
1087
+ if (meta.required) {
1088
+ schema = schema.annotate({
1089
+ message: trans("validation.empty")
1090
+ })
1091
+ }
882
1092
  }
883
1093
 
884
- // Apply numeric validations
885
1094
  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
- )
1095
+ schema = schema.check(S.isGreaterThanOrEqualTo(meta.minimum, {
1096
+ message: trans(meta.minimum === 0 ? "validation.number.positive" : "validation.number.min", {
1097
+ minimum: meta.minimum,
1098
+ isExclusive: true
1099
+ })
1100
+ }))
896
1101
  }
897
-
898
1102
  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
- )
1103
+ schema = schema.check(S.isLessThanOrEqualTo(meta.maximum, {
1104
+ message: trans("validation.number.max", {
1105
+ maximum: meta.maximum,
1106
+ isExclusive: true
1107
+ })
1108
+ }))
909
1109
  }
910
-
911
1110
  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
- )
1111
+ schema = schema.check(S.isGreaterThan(meta.exclusiveMinimum, {
1112
+ message: trans(meta.exclusiveMinimum === 0 ? "validation.number.positive" : "validation.number.min", {
1113
+ minimum: meta.exclusiveMinimum,
1114
+ isExclusive: false
1115
+ })
1116
+ }))
922
1117
  }
923
-
924
1118
  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
- )
1119
+ schema = schema.check(S.isLessThan(meta.exclusiveMaximum, {
1120
+ message: trans("validation.number.max", {
1121
+ maximum: meta.exclusiveMaximum,
1122
+ isExclusive: false
1123
+ })
1124
+ }))
935
1125
  }
936
1126
  break
937
- }
1127
+ case "select":
1128
+ schema = S.Literals(meta.members as [any, ...any[]]).annotate({
1129
+ message: trans("validation.not_a_valid", {
1130
+ type: "select",
1131
+ message: meta.members.join(", ")
1132
+ })
1133
+ })
938
1134
 
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
1135
  break
950
- }
951
1136
 
952
- case "multiple": {
953
- schema = S.Array(S.String)
1137
+ case "multiple":
1138
+ schema = S.Array(S.String).annotate({
1139
+ message: trans("validation.not_a_valid", {
1140
+ type: "multiple",
1141
+ message: meta.members.join(", ")
1142
+ })
1143
+ })
954
1144
  break
955
- }
956
1145
 
957
- case "boolean": {
1146
+ case "boolean":
958
1147
  schema = S.Boolean
959
1148
  break
960
- }
961
1149
 
962
- case "unknown": {
1150
+ case "date":
1151
+ schema = S.Date
1152
+ break
1153
+
1154
+ case "unknown":
963
1155
  schema = S.Unknown
964
1156
  break
965
- }
966
1157
 
967
- default: {
968
- console.warn(`Unhandled field type: ${(meta as any).type}`)
1158
+ default:
1159
+ // For any unhandled types, use Unknown schema to prevent undefined errors
1160
+ console.warn(`Unhandled field type: ${meta}`)
969
1161
  schema = S.Unknown
970
1162
  break
971
- }
972
1163
  }
973
-
974
- // Wrap in union with null/undefined if not required
975
1164
  if (!meta.required) {
976
- // v4 Union takes an array of schemas
977
- schema = S.Union([schema, S.Null, S.Undefined])
1165
+ schema = S.NullishOr(schema)
978
1166
  }
979
-
980
- return S.toStandardSchemaV1(schema as any)
1167
+ const result = S.toStandardSchemaV1(schema as any)
1168
+ return result
981
1169
  }
982
1170
 
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
1171
  export type OmegaAutoGenMeta<
999
1172
  From extends Record<PropertyKey, any>,
1000
1173
  To extends Record<PropertyKey, any>,
@@ -1037,18 +1210,8 @@ export function deepMerge(target: any, source: any) {
1037
1210
  return result
1038
1211
  }
1039
1212
 
1040
- // Type definitions for schemas with fields and members
1041
- type SchemaWithFields = {
1042
- fields: Record<string, S.Top>
1043
- }
1044
-
1045
1213
  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"
1214
+ members: readonly S.Schema<any>[]
1052
1215
  }
1053
1216
 
1054
1217
  function hasMembers(schema: any): schema is SchemaWithMembers {
@@ -1057,10 +1220,15 @@ function hasMembers(schema: any): schema is SchemaWithMembers {
1057
1220
 
1058
1221
  // Internal implementation with WeakSet tracking
1059
1222
  export const defaultsValueFromSchema = (
1060
- schema: S.Codec<any>,
1223
+ schema: S.Schema<any>,
1061
1224
  record: Record<string, any> = {}
1062
1225
  ): any => {
1063
1226
  const ast = schema.ast
1227
+ const defaultValue = getDefaultFromAst(ast)
1228
+
1229
+ if (defaultValue !== undefined) {
1230
+ return defaultValue
1231
+ }
1064
1232
 
1065
1233
  if (isNullableOrUndefined(schema.ast) === "null") {
1066
1234
  return null
@@ -1069,106 +1237,79 @@ export const defaultsValueFromSchema = (
1069
1237
  return undefined
1070
1238
  }
1071
1239
 
1072
- // Handle v4 Objects AST structure
1073
- if (AST.isObjects(ast)) {
1074
- const result: Record<string, any> = { ...record }
1240
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, ExtendedClass, etc.)
1241
+ const objectsAst = S.AST.isObjects(ast)
1242
+ ? ast
1243
+ : S.AST.isDeclaration(ast)
1244
+ ? unwrapDeclaration(ast)
1245
+ : undefined
1246
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
1247
+ const result: Record<string, any> = {}
1075
1248
 
1076
- for (const prop of ast.propertySignatures) {
1249
+ for (const prop of objectsAst.propertySignatures) {
1077
1250
  const key = prop.name.toString()
1078
1251
  const propType = prop.type
1079
1252
 
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)
1253
+ const propDefault = getDefaultFromAst(propType)
1254
+ if (propDefault !== undefined) {
1255
+ result[key] = propDefault
1256
+ continue
1087
1257
  }
1088
1258
 
1089
- // Recursively process the property to get its defaults
1259
+ const propSchema = S.make(propType)
1090
1260
  const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1091
1261
 
1092
1262
  if (propValue !== undefined) {
1093
1263
  result[key] = propValue
1264
+ } else if (isNullableOrUndefined(propType) === "undefined") {
1265
+ result[key] = undefined
1094
1266
  }
1095
1267
  }
1096
1268
 
1097
- return result
1269
+ return { ...result, ...record }
1098
1270
  }
1099
1271
 
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
1272
+ // Handle unions via AST or schema-level .members
1273
+ const unionTypes = S.AST.isUnion(ast)
1274
+ ? ast.types
1275
+ : hasMembers(schema)
1276
+ ? schema.members.map((m) => m.ast)
1277
+ : undefined
1278
+ if (unionTypes) {
1279
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
1280
+
1281
+ for (const memberAstRaw of unionTypes) {
1282
+ const memberAst = unwrapDeclaration(memberAstRaw)
1283
+ if (!S.AST.isObjects(memberAst)) continue
1284
+
1285
+ for (const prop of memberAst.propertySignatures) {
1286
+ const key = prop.name.toString()
1287
+ const fieldDefault = getDefaultFromAst(prop.type)
1288
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
1289
+
1290
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
1291
+ mergedFields[key] = { ast: prop.type }
1114
1292
  }
1115
1293
  }
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
1294
  }
1123
1295
 
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>)
1296
+ if (Object.keys(mergedFields).length === 0) {
1297
+ return Object.keys(record).length > 0 ? record : undefined
1298
+ }
1151
1299
 
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] || {})
1300
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
1301
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
1155
1302
  return acc
1156
1303
  }, record)
1157
1304
  }
1158
1305
 
1159
1306
  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
1307
+ if (S.AST.isString(ast)) {
1308
+ return ""
1165
1309
  }
1166
- }
1167
1310
 
1168
- if (AST.isString(ast)) {
1169
- return ""
1170
- }
1171
- if (AST.isBoolean(ast)) {
1172
- return false
1311
+ if (S.AST.isBoolean(ast)) {
1312
+ return false
1313
+ }
1173
1314
  }
1174
1315
  }