Skip to content

Instantly share code, notes, and snippets.

@tim-smart
Last active August 14, 2024 10:35
Show Gist options
  • Save tim-smart/42ade9c1f210373ddbc757adcfc079a2 to your computer and use it in GitHub Desktop.
Save tim-smart/42ade9c1f210373ddbc757adcfc079a2 to your computer and use it in GitHub Desktop.
import { Schema } from "@effect/schema"
import * as Model from "./Model.js"
import { DateTime } from "effect"
export const GroupId = Schema.Number.pipe(Schema.brand("GroupId"))
export class Group extends Model.Class<Group>("Group")({
id: Model.PrimaryKey(GroupId),
name: Schema.NonEmptyTrimmedString,
createdAt: Model.CreatedAt,
updatedAt: Model.UpdatedAt,
}) {}
const group = Group.make({
id: GroupId.make(1),
name: "Group 1",
createdAt: DateTime.unsafeNow(),
updatedAt: DateTime.unsafeNow(),
})
console.log(Schema.encodeSync(Group.json)(group))
import * as AST from "@effect/schema/AST"
import * as Schema from "@effect/schema/Schema"
import * as ParseResult from "@effect/schema/ParseResult"
import { dual } from "effect/Function"
import * as Data from "effect/Data"
/**
* @since 1.0.0
* @category type ids
*/
export const TypeId: unique symbol = Symbol.for("@effect/schema/VariantSchema")
/**
* @since 1.0.0
* @category type ids
*/
export type TypeId = typeof TypeId
/**
* @since 1.0.0
* @category models
*/
export interface Struct<in out A extends Field.Fields> {
readonly [TypeId]: A
}
/**
* @since 1.0.0
* @category models
*/
export declare namespace Struct {
/**
* @since 1.0.0
* @category models
*/
export type Fields = {
readonly [key: string]:
| Schema.Schema.All
| Schema.PropertySignature.All
| Field<any>
| Struct<any>
}
/**
* @since 1.0.0
* @category models
*/
export type FieldsWithKeys<K extends string> = {
readonly [key: string]:
| Schema.Schema.All
| Schema.PropertySignature.All
| Field<Field.ConfigWithKeys<K>>
| Struct<FieldsWithKeys<K>>
}
}
/**
* @since 1.0.0
* @category type ids
*/
export const FieldTypeId: unique symbol = Symbol.for(
"@effect/schema/VariantSchema/Field",
)
/**
* @since 1.0.0
* @category type ids
*/
export type FieldTypeId = typeof FieldTypeId
/**
* @since 1.0.0
* @category models
*/
export interface Field<in out A extends Field.Config> {
readonly [FieldTypeId]: FieldTypeId
readonly schemas: A
}
/**
* @since 1.0.0
* @category models
*/
export declare namespace Field {
/**
* @since 1.0.0
* @category models
*/
export type Config = Partial<{
readonly [key: string]: Schema.Schema.All | Schema.PropertySignature.All
}>
/**
* @since 1.0.0
* @category models
*/
export type ConfigWithKeys<K extends string> = {
readonly [P in K]?: Schema.Schema.All | Schema.PropertySignature.All
}
/**
* @since 1.0.0
* @category models
*/
export type Fields = {
readonly [key: string]:
| Schema.Schema.All
| Schema.PropertySignature.All
| Field<any>
| Struct<any>
}
}
/**
* @since 1.0.0
* @category constructors
*/
export const Struct = <const A extends Field.Fields>(fields: A): Struct<A> => ({
[TypeId]: fields,
})
/**
* @since 1.0.0
* @category constructors
*/
export const Field = <const A extends Field.Config>(schemas: A): Field<A> => ({
[FieldTypeId]: FieldTypeId,
schemas,
})
/**
* @since 1.0.0
* @category extractors
*/
export type ExtractFields<V extends string, Fields extends Struct.Fields> = {
readonly [K in keyof Fields as [Fields[K]] extends [Field<infer Config>]
? V extends keyof Config
? K
: never
: K]: [Fields[K]] extends [Struct<infer _>]
? Extract<V, Fields[K]>
: [Fields[K]] extends [Field<infer Config>]
? [Config[V]] extends [Schema.Schema.All | Schema.PropertySignature.All]
? Config[V]
: never
: [Fields[K]] extends [Schema.Schema.All | Schema.PropertySignature.All]
? Fields[K]
: never
}
/**
* @since 1.0.0
* @category extractors
*/
export type Extract<V extends string, A extends Struct<any>> = [A] extends [
Struct<infer Fields>,
]
? Schema.Struct<Schema.Simplify<ExtractFields<V, Fields>>>
: never
/**
* @since 1.0.0
* @category extractors
*/
export const extract: {
<V extends string>(
variant: V,
): <A extends Struct<any>>(self: A) => Extract<V, A>
<V extends string, A extends Struct<any>>(self: A, variant: V): Extract<V, A>
} = dual(
2,
<V extends string, A extends Struct<any>>(
self: A,
variant: V,
): Extract<V, A> => {
const fields: Record<string, any> = {}
for (const key of Object.keys(self[TypeId])) {
const value = self[TypeId][key]
if (Schema.TypeId in value) {
fields[key] = value
} else if (FieldTypeId in value) {
if (variant in value.schemas) {
fields[key] = value.schemas[variant]
}
} else {
fields[key] = extract(value, variant)
}
}
return Schema.Struct(fields) as any
},
)
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T]
/**
* @category models
* @since 1.0.0
*/
export interface Class<
Self,
Fields extends Struct.Fields,
SchemaFields extends Schema.Struct.Fields,
A,
I,
R,
C,
> extends Schema.Schema<Self, Schema.Simplify<I>, R>,
Struct<Fields> {
new (
props: RequiredKeys<C> extends never
? void | Schema.Simplify<C>
: Schema.Simplify<C>,
options?: {
readonly disableValidation?: boolean
},
): A
readonly ast: AST.Transformation
make<Args extends Array<any>, X>(
this: { new (...args: Args): X },
...args: Args
): X
annotations(
annotations: Schema.Annotations.Schema<Self>,
): Schema.SchemaClass<Self, Schema.Simplify<I>, R>
readonly identifier: string
readonly fields: SchemaFields
}
type ClassFromFields<
Self,
Fields extends Struct.Fields,
SchemaFields extends Schema.Struct.Fields,
> = Class<
Self,
Fields,
SchemaFields,
Schema.Struct.Type<SchemaFields>,
Schema.Struct.Encoded<SchemaFields>,
Schema.Struct.Context<SchemaFields>,
Schema.Struct.Constructor<SchemaFields>
>
type MissingSelfGeneric<Params extends string = ""> =
`Missing \`Self\` generic - use \`class Self extends Class<Self>()(${Params}{ ... })\``
const schemaVariance = {
/* c8 ignore next */
_A: (_: any) => _,
/* c8 ignore next */
_I: (_: any) => _,
/* c8 ignore next */
_R: (_: never) => _,
}
/**
* @since 1.0.0
* @category constructors
*/
export const factory = <
const Variants extends ReadonlyArray<string>,
const Default extends Variants[number],
>(options: {
readonly variants: Variants
readonly defaultVariant: Default
}): {
readonly Struct: <const A extends Struct.FieldsWithKeys<Variants[number]>>(
fields: A,
) => Struct<A>
readonly Field: <const A extends Field.ConfigWithKeys<Variants[number]>>(
config: A & { readonly [K in Exclude<keyof A, Variants[number]>]: never },
) => Field<A>
readonly Class: <Self = never>(
identifier: string,
) => <Fields extends Struct.Fields>(
fields: Fields,
annotations?: Schema.Annotations.Schema<Self>,
) => [Self] extends [never]
? MissingSelfGeneric
: ClassFromFields<
Self,
Fields,
Schema.Simplify<ExtractFields<Default, Fields>>
> & {
readonly [V in Variants[number]]: Extract<V, Struct<Fields>>
}
} => {
function Class<Self>(identifier: string) {
return function (
fields: Struct.Fields,
annotations?: Schema.Annotations.Schema<Self>,
) {
const variantStruct = Struct(fields)
const schema = extract(variantStruct, options.defaultVariant)
const validate = ParseResult.validateSync(schema)
const klass = Schema.Class<any>(identifier)(schema.fields, annotations)
class Base extends Data.Class {
constructor(
props: any,
options?: { readonly disableValidation?: boolean },
) {
if (options?.disableValidation !== true) {
props = validate(props)
}
super(props)
}
// implement Struct
static [TypeId] = fields
// implement Schema
static [Schema.TypeId] = schemaVariance
static ast = klass.ast
static make = klass.make
static fields = klass.fields
static identifier = klass.identifier
}
for (const variant of options.variants) {
Object.defineProperty(Base, variant, {
value: extract(variantStruct, variant),
})
}
return Base
}
}
return {
Struct,
Field,
Class,
} as any
}
import * as Variant from "./Variant.js"
import * as Schema from "@effect/schema/Schema"
import * as DateTime from "effect/DateTime"
import { LazyArg } from "effect/Function"
export const { Class, Struct, Field } = Variant.factory({
variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"],
defaultVariant: "select",
})
export interface PrimaryKey<S extends Schema.Schema.All>
extends Variant.Field<{
readonly select: S
readonly update: S
readonly json: S
}> {}
export const PrimaryKey = <S extends Schema.Schema.All>(
schema: S,
): PrimaryKey<S> =>
Field({
select: schema,
update: schema,
json: schema,
})
export interface FieldNoInsert<S extends Schema.Schema.All>
extends Variant.Field<{
readonly select: S
readonly update: S
readonly json: S
readonly jsonUpdate: S
}> {}
export const FieldNoInsert = <S extends Schema.Schema.All>(
schema: S,
): FieldNoInsert<S> =>
Field({
select: schema,
update: schema,
json: schema,
jsonUpdate: schema,
})
export interface FieldNoJson<S extends Schema.Schema.All>
extends Variant.Field<{
readonly select: S
readonly insert: S
readonly update: S
}> {}
export const FieldNoJson = <S extends Schema.Schema.All>(
schema: S,
): FieldNoJson<S> =>
Field({
select: schema,
insert: schema,
update: schema,
})
export interface DateTimeFromDate
extends Schema.transform<
typeof Schema.ValidDateFromSelf,
typeof Schema.DateTimeUtcFromSelf
> {}
export const DateTimeFromDate: DateTimeFromDate = Schema.transform(
Schema.ValidDateFromSelf,
Schema.DateTimeUtcFromSelf,
{
decode: DateTime.unsafeFromDate,
encode: DateTime.toDateUtc,
},
)
const DateTimeWithNow = DateTimeFromDate.pipe(
Schema.optionalWith({ default: DateTime.unsafeNow }),
)
export interface CreatedAt
extends Variant.Field<{
readonly select: DateTimeFromDate
readonly insert: Schema.optionalWith<
DateTimeFromDate,
{ default: LazyArg<DateTime.Utc> }
>
readonly json: typeof Schema.DateTimeUtc
}> {}
export const CreatedAt: CreatedAt = Field({
select: DateTimeFromDate,
insert: DateTimeWithNow,
json: Schema.DateTimeUtc,
})
export interface UpdatedAt
extends Variant.Field<{
readonly select: DateTimeFromDate
readonly insert: Schema.optionalWith<
DateTimeFromDate,
{ default: LazyArg<DateTime.Utc> }
>
readonly update: Schema.optionalWith<
DateTimeFromDate,
{ default: LazyArg<DateTime.Utc> }
>
readonly json: typeof Schema.DateTimeUtc
}> {}
export const UpdatedAt: UpdatedAt = Field({
select: DateTimeFromDate,
insert: DateTimeWithNow,
update: DateTimeWithNow,
json: Schema.DateTimeUtc,
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment