diff --git a/.changeset/wise-dodos-rescue.md b/.changeset/wise-dodos-rescue.md new file mode 100644 index 0000000000..f4e8ed898b --- /dev/null +++ b/.changeset/wise-dodos-rescue.md @@ -0,0 +1,5 @@ +--- +"@effect/experimental": patch +--- + +Add VariantSchema.fieldEvolve api diff --git a/packages/experimental/src/VariantSchema.ts b/packages/experimental/src/VariantSchema.ts index cd820591d2..15f077cd1c 100644 --- a/packages/experimental/src/VariantSchema.ts +++ b/packages/experimental/src/VariantSchema.ts @@ -8,6 +8,8 @@ import type { Brand } from "effect/Brand" import type * as Effect from "effect/Effect" import { constUndefined, dual } from "effect/Function" import * as Option from "effect/Option" +import { type Pipeable, pipeArguments } from "effect/Pipeable" +import * as Struct_ from "effect/Struct" /** * @since 1.0.0 @@ -25,7 +27,7 @@ export type TypeId = typeof TypeId * @since 1.0.0 * @category models */ -export interface Struct { +export interface Struct extends Pipeable { readonly [TypeId]: A } @@ -77,7 +79,7 @@ export type FieldTypeId = typeof FieldTypeId * @since 1.0.0 * @category models */ -export interface Field { +export interface Field extends Pipeable { readonly [FieldTypeId]: FieldTypeId readonly schemas: A } @@ -87,6 +89,12 @@ export interface Field { * @category models */ export declare namespace Field { + /** + * @since 1.0.0 + * @category models + */ + type ValueAny = Schema.Schema.All | Schema.PropertySignature.All + /** * @since 1.0.0 * @category models @@ -116,22 +124,73 @@ export declare namespace Field { } } +const StructProto = { + pipe() { + return pipeArguments(this, arguments) + } +} + /** * @since 1.0.0 * @category constructors */ -export const Struct = (fields: A): Struct => ({ - [TypeId]: fields -}) +export const Struct = (fields: A): Struct => { + const self = Object.create(StructProto) + self[TypeId] = fields + return self +} + +const FieldProto = { + [FieldTypeId]: FieldTypeId, + pipe() { + return pipeArguments(this, arguments) + } +} /** * @since 1.0.0 * @category constructors */ -export const Field = (schemas: A): Field => ({ - [FieldTypeId]: FieldTypeId, - schemas -}) +export const Field = (schemas: A): Field => { + const self = Object.create(FieldProto) + self.schemas = schemas + return self +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fieldEvolve: { + < + Self extends Field, + Mapping extends { + readonly [K in keyof Self["schemas"]]?: (variant: Self["schemas"][K]) => Field.ValueAny + } + >(f: Mapping): (self: Self) => Field< + { + readonly [K in keyof Self["schemas"]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self["schemas"][K] + : Self["schemas"][K] + } + > + < + Self extends Field, + Mapping extends { + readonly [K in keyof Self["schemas"]]?: (variant: Self["schemas"][K]) => Field.ValueAny + } + >(self: Self, f: Mapping): Field< + { + readonly [K in keyof Self["schemas"]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self["schemas"][K] + : Self["schemas"][K] + } + > +} = dual( + 2, + (self: Field, f: Record Field.ValueAny>): Field => + Field(Struct_.evolve(self.schemas, f)) +) /** * @since 1.0.0 @@ -281,6 +340,42 @@ export const make = < ) => ( schema: S ) => Field<{ readonly [K in Exclude]: S }> + readonly fieldEvolve: { + < + Self extends Field | Field.ValueAny, + Mapping extends (Self extends Field ? { readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny } + : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) + >(f: Mapping): (self: Self) => Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] + : S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self + : Self + } + > + < + Self extends Field | Field.ValueAny, + Mapping extends (Self extends Field ? { + readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny + } + : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) + >(self: Self, f: Mapping): Field< + Self extends Field ? { + readonly [K in keyof S]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] + : S[K] + } : + { + readonly [K in Variants[number]]: K extends keyof Mapping + ? Mapping[K] extends (arg: any) => any ? ReturnType : Self + : Self + } + > + } readonly Class: ( identifier: string ) => ( @@ -335,12 +430,25 @@ export const make = < return Field(obj) } } + const fieldEvolveVariants = dual( + 2, + ( + self: Field | Schema.Schema.All | Schema.PropertySignature.All, + f: Record Field.ValueAny> + ): Field => { + const field = FieldTypeId in self ? self : Field(Object.fromEntries( + options.variants.map((variant) => [variant, self]) + )) + return fieldEvolve(field, f) + } + ) return { Struct, Field, FieldOnly, FieldExcept, - Class + Class, + fieldEvolve: fieldEvolveVariants } as any } diff --git a/packages/sql/src/Model.ts b/packages/sql/src/Model.ts index 89c0707886..dfce6b1f3a 100644 --- a/packages/sql/src/Model.ts +++ b/packages/sql/src/Model.ts @@ -8,14 +8,14 @@ import type { Brand } from "effect/Brand" import * as DateTime from "effect/DateTime" import * as Effect from "effect/Effect" import * as Option from "effect/Option" -import * as Record from "effect/Record" const { Class, Field, FieldExcept, FieldOnly, - Struct + Struct, + fieldEvolve } = VariantSchema.make({ variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"], defaultVariant: "select" @@ -81,6 +81,11 @@ export { * @category fields */ Field, + /** + * @since 1.0.0 + * @category fields + */ + fieldEvolve, /** * @since 1.0.0 * @category fields @@ -208,48 +213,45 @@ export const Sensitive = | Schema.Schema.Any>( +export interface FieldOption extends + VariantSchema.Field<{ + readonly select: Schema.OptionFromNullOr + readonly insert: Schema.OptionFromNullOr + readonly update: Schema.OptionFromNullOr + readonly json: Schema.optionalWith + readonly jsonCreate: Schema.optionalWith + readonly jsonUpdate: Schema.optionalWith + }> +{} + +/** + * Convert a field to one that is optional for all variants. + * + * For the database variants, it will accept `null`able values. + * For the JSON variants, it will also accept missing keys. + * + * @since 1.0.0 + * @category optional + */ +export const FieldOption: | Schema.Schema.Any>( self: Field -): VariantSchema.Field< - Field extends Schema.Schema.Any ? { - readonly select: Schema.OptionFromNullOr - readonly insert: Schema.OptionFromNullOr - readonly update: Schema.OptionFromNullOr - readonly json: Schema.optionalWith - readonly jsonCreate: Schema.optionalWith - readonly jsonUpdate: Schema.optionalWith - } - : Field extends VariantSchema.Field ? { +) => Field extends Schema.Schema.Any ? FieldOption + : Field extends VariantSchema.Field ? VariantSchema.Field< + { readonly [K in keyof S]: S[K] extends Schema.Schema.Any ? K extends VariantsDatabase ? Schema.OptionFromNullOr : Schema.optionalWith : never - } : - {} -> => { - if (Schema.isSchema(self)) { - return Field({ - select: Schema.OptionFromNullOr(self), - insert: Schema.OptionFromNullOr(self), - update: Schema.OptionFromNullOr(self), - json: Schema.optionalWith(self, { as: "Option" }), - jsonCreate: Schema.optionalWith(self, { as: "Option", nullable: true }), - jsonUpdate: Schema.optionalWith(self, { as: "Option", nullable: true }) - }) as any - } - return VariantSchema.Field(Record.map(self.schemas, (schema, variant) => { - switch (variant) { - case "select": - case "insert": - case "update": - return Schema.OptionFromNullOr(schema as any) - case "json": - return Schema.optionalWith(schema as any, { as: "Option" }) - default: - return Schema.optionalWith(schema as any, { as: "Option", nullable: true }) - } - }) as any) -} + } + > : + never = fieldEvolve({ + select: Schema.OptionFromNullOr, + insert: Schema.OptionFromNullOr, + update: Schema.OptionFromNullOr, + json: Schema.optionalWith({ as: "Option" }), + jsonCreate: Schema.optionalWith({ as: "Option", nullable: true }), + jsonUpdate: Schema.optionalWith({ as: "Option", nullable: true }) + }) as any /** * @since 1.0.0