Skip to content

Instantly share code, notes, and snippets.

@kristojorg
Created January 16, 2026 10:38
Show Gist options
  • Select an option

  • Save kristojorg/42e3f7925971c39c7309e3e8c17cb64b to your computer and use it in GitHub Desktop.

Select an option

Save kristojorg/42e3f7925971c39c7309e3e8c17cb64b to your computer and use it in GitHub Desktop.
Modified Model.ts
import * as VariantSchema from "@effect/experimental/VariantSchema";
import { flow, identity, Option } from "effect";
import type { Brand } from "effect/Brand";
import * as Schema from "effect/Schema";
import { TemporalSchema } from "./temporal";
import {
headSingleton,
NullToOptional,
nullToOptional,
OmitUndefinedAndNull,
omitUndefinedAndNull,
preserveNull,
PreserveNull,
} from "./util";
const {
Class,
Field,
FieldExcept,
FieldOnly,
Struct,
Union,
extract,
fieldEvolve,
fieldFromKey,
} = VariantSchema.make({
variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"],
defaultVariant: "select",
});
/**
* @since 1.0.0
* @category models
*/
export type Any = Schema.Schema.Any & {
readonly fields: Schema.Struct.Fields;
readonly insert: Schema.Schema.Any;
readonly update: Schema.Schema.Any;
readonly json: Schema.Schema.Any;
readonly jsonCreate: Schema.Schema.Any;
readonly jsonUpdate: Schema.Schema.Any;
};
/**
* @since 1.0.0
* @category models
*/
export type AnyNoContext = Schema.Schema.AnyNoContext & {
readonly fields: Schema.Struct.Fields;
readonly insert: Schema.Schema.AnyNoContext;
readonly update: Schema.Schema.AnyNoContext;
readonly json: Schema.Schema.AnyNoContext;
readonly jsonCreate: Schema.Schema.AnyNoContext;
readonly jsonUpdate: Schema.Schema.AnyNoContext;
};
/**
* @since 1.0.0
* @category models
*/
export type VariantsDatabase = "select" | "insert" | "update";
/**
* @since 1.0.0
* @category models
*/
export type VariantsJson = "json" | "jsonCreate" | "jsonUpdate";
export {
/**
* A base class used for creating domain model schemas.
*
* It supports common variants for database and JSON apis.
*
* @since 1.0.0
* @category constructors
* @example
* ```ts
* import { Schema } from "effect"
* import { Model } from "#com/index.ts"
*
* export const GroupId = Schema.Number.pipe(Schema.brand("GroupId"))
*
* export class Group extends Model.Class<Group>("Group")({
* id: Model.Generated(GroupId),
* name: Schema.NonEmptyTrimmedString,
* createdAt: Model.DateTimeInsertFromDate,
* updatedAt: Model.DateTimeUpdateFromDate
* }) {}
*
* // schema used for selects
* Group
*
* // schema used for inserts
* Group.insert
*
* // schema used for updates
* Group.update
*
* // schema used for json api
* Group.json
* Group.jsonCreate
* Group.jsonUpdate
*
* // you can also turn them into classes
* class GroupJson extends Schema.Class<GroupJson>("GroupJson")(Group.json) {
* get upperName() {
* return this.name.toUpperCase()
* }
* }
* ```
*/
Class,
/**
* @since 1.0.0
* @category extraction
*/
extract,
/**
* @since 1.0.0
* @category fields
*/
Field,
/**
* Transforms a field by applying different schema transformations to each variant.
* This is the most flexible helper for customizing how fields behave across different contexts.
*
* @since 1.0.0
* @category fields
* @example
* ```ts
* // Make a field optional only for JSON variants
* const flexibleField = fieldEvolve({
* select: Schema.String,
* insert: Schema.String,
* update: Schema.String,
* json: Schema.optional(Schema.String),
* jsonCreate: Schema.optional(Schema.String),
* jsonUpdate: Schema.optional(Schema.String)
* })(baseField)
* ```
*/
fieldEvolve,
/**
* Creates a field variant that excludes specific variants from a base field.
* Useful for removing certain database or JSON variants when they're not applicable.
*
* @since 1.0.0
* @category fields
* @example
* ```ts
* // Exclude the field from insert operations (e.g., for auto-generated fields)
* const readOnlyField = FieldExcept(Schema.String, ["insert"])
* ```
*/
FieldExcept,
/**
* Creates a new field by deriving it from a key of an existing model's fields.
* This allows reusing field definitions from other models while maintaining type safety.
*
* @since 1.0.0
* @category fields
* @example
* ```ts
* // Reuse the 'name' field from User model
* const userNameField = fieldFromKey(User, "name")
* ```
*/
fieldFromKey,
/**
* Creates a field variant that only includes specific variants from a base field.
* This is the opposite of FieldExcept - it whitelists variants instead of blacklisting them.
*
* @since 1.0.0
* @category fields
* @example
* ```ts
* // Only allow this field in select and json variants
* const selectOnlyField = FieldOnly(Schema.String, ["select", "json"])
* ```
*/
FieldOnly,
/**
* @since 1.0.0
* @category constructors
*/
Struct,
/**
* @since 1.0.0
* @category constructors
*/
Union,
};
/**
* @since 1.0.0
* @category fields
*/
export const fields: <A extends VariantSchema.Struct<any>>(
self: A,
) => A[VariantSchema.TypeId] = VariantSchema.fields;
/**
* @since 1.0.0
* @category overrideable
*/
export const Override: <A>(value: A) => A & Brand<"Override"> =
VariantSchema.Override;
/**
* @since 1.0.0
* @category generated
*/
export interface Generated<
S extends Schema.Schema.All | Schema.PropertySignature.All,
> extends VariantSchema.Field<{
readonly select: S;
// readonly update: S;
readonly json: S;
}> {}
/**
* A field that represents a column that is generated by the database.
*
* It is available for selection and update, but not for insertion.
*
* @since 1.0.0
* @category generated
*/
export const Generated = <
S extends Schema.Schema.All | Schema.PropertySignature.All,
>(
schema: S,
): Generated<S> =>
Field({
select: schema,
// update: schema,
json: schema,
});
/**
* A PropertySignature that is present on the Type side but completely
* absent on the Encoded side (encoded type is `never`).
*
* Using Schema.Never as the "from" schema makes the Encoded type `never`.
*/
type OmittedOnEncode<S extends Schema.Schema.AnyNoContext> =
Schema.PropertySignature<
":",
Schema.Schema.Type<S>,
never,
"?:",
never,
false,
Schema.Schema.Context<S>
>;
export interface Tag<S extends Schema.Schema.AnyNoContext>
extends VariantSchema.Field<{
readonly select: Schema.transform<typeof Schema.String, S>;
readonly insert: S;
readonly update: OmittedOnEncode<S>;
readonly json: S;
readonly jsonCreate: S;
readonly jsonUpdate: S;
}> {}
/**
* A field that represents a discriminator tag for STI (single-table inheritance).
*
* - **select**: Transforms from DB string to literal type
* - **insert/update**: Present in domain for discrimination, but encoded type
* is `never` (omitted during encoding - discriminators shouldn't be mutated)
* - **json**: The literal type as-is
*
* @since 1.0.0
* @category generated
*/
export const Tag = <S extends Schema.Schema.AnyNoContext>(
schema: S,
): Tag<S> => {
// Using Schema.Never as "from" makes Encoded type `never`.
// decode is never called (from type is never), encode always omits.
const omitOnEncode = Schema.optionalToRequired(Schema.Never, schema, {
decode: identity,
encode: Option.none,
});
return Field({
select: Schema.compose(Schema.String, schema),
insert: schema,
update: omitOnEncode,
json: schema,
jsonCreate: schema,
jsonUpdate: schema,
});
};
/**
* @since 1.0.0
* @category generated
*/
export interface GeneratedByApp<
S extends Schema.Schema.All | Schema.PropertySignature.All,
> extends VariantSchema.Field<{
readonly select: S;
readonly insert: S;
readonly update: S;
readonly json: S;
}> {}
/**
* A field that represents a column that is generated by the application.
*
* It is required by the database, but not by the JSON variants.
*
* @since 1.0.0
* @category generated
*/
export const GeneratedByApp = <
S extends Schema.Schema.All | Schema.PropertySignature.All,
>(
schema: S,
): GeneratedByApp<S> =>
Field({
select: schema,
insert: schema,
update: schema,
json: schema,
});
export interface GeneratedOverrideable<S extends Schema.Schema.Any>
extends VariantSchema.Field<{
readonly select: S;
readonly insert: OmitUndefinedAndNull<Schema.mutable<S>>;
readonly update: PreserveNull<Schema.mutable<S>>;
readonly json: S;
readonly jsonCreate: OmitUndefinedAndNull<S>;
readonly jsonUpdate: PreserveNull<S>;
}> {}
/**
* A field that represents a column that is normally generated by the database
* but can be overridden during insertion by providing a value.
*
* It is available for selection and update, and optional for insertion.
*
* ### Semantics for Optional Fields
*
* **Insert variants** use `omitUndefinedAndNull`:
* - `undefined` | `null` | missing → property omitted (DB generates/defaults)
* - `value` → property set to value
*
* **Update variants** use `preserveNull`:
* - `undefined` | missing → property omitted (field not modified)
* - `null` → property set to null (clears the field)
* - `value` → property set to value
*
* This allows updates to distinguish between "don't change" (undefined)
* and "clear this field" (null).
*
* @since 1.0.0
* @category generated
* @example
* ```ts
* class User extends Model.Class<User>("User")({
* id: Model.Generated(Schema.String),
* slug: Model.GeneratedOverrideable(Schema.String),
* }) {}
*
* Insert without slug - database generates it
* const insert1 = { name: "John" } // slug omitted, DB generates
*
* Insert with custom slug - override generation
* const insert2 = { name: "John", slug: "custom-slug" }
*
* Update without slug - preserve existing value
* const update1 = { name: "Jane" } // slug undefined, not updated
*
* Update with null slug - clear the field
* const update2 = { name: "Jane", slug: null } // explicitly clear
*
* Update with new slug - change the value
* const update3 = { name: "Jane", slug: "new-slug" }
* ```
*/
export const GeneratedOverrideable = <S extends Schema.Schema.Any>(
schema: S,
): GeneratedOverrideable<S> => {
return Field({
select: schema,
insert: omitUndefinedAndNull(Schema.mutable(schema)),
update: preserveNull(Schema.mutable(schema)),
json: schema,
jsonCreate: omitUndefinedAndNull(schema),
jsonUpdate: preserveNull(schema),
});
};
export interface GeneratedOverrideableRequired<S extends Schema.Schema.Any>
extends VariantSchema.Field<{
readonly select: S;
readonly insert: Schema.optional<Schema.mutable<S>>;
readonly update: Schema.optional<Schema.mutable<S>>;
readonly json: S;
readonly jsonCreate: Schema.optional<S>;
readonly jsonUpdate: Schema.optional<S>;
}> {}
/**
* A field that represents a required column that is normally generated by the
* database but can be overridden during insertion or update.
*
* Unlike `GeneratedOverrideable`, this does NOT allow setting the field to null.
* Use this for required fields like `slug` that cannot be cleared.
*
* ### Semantics for All Mutation Variants
*
* All variants use `Schema.optional`:
* - `undefined` | missing → property omitted (DB generates/preserves)
* - `value` → property set to value
* - `null` → ❌ Type error (not allowed)
*
* @since 1.0.0
* @category generated
* @example
* ```ts
* class User extends Model.Class<User>("User")({
* id: Model.Generated(Schema.String),
* slug: Model.GeneratedOverrideableRequired(Schema.String),
* }) {}
*
* // Insert without slug - database generates it
* const insert1 = { name: "John" } // slug omitted, DB generates
*
* // Insert with custom slug - override generation
* const insert2 = { name: "John", slug: "custom-slug" }
*
* // Update without slug - preserve existing value
* const update1 = { name: "Jane" } // slug undefined, not updated
*
* // Update with new slug - change the value
* const update2 = { name: "Jane", slug: "new-slug" }
*
* // NOTE: Setting slug to null is NOT allowed (type error)
* // const update3 = { name: "Jane", slug: null } // ❌ Type error
* ```
*/
export const GeneratedOverrideableRequired = <S extends Schema.Schema.Any>(
schema: S,
): GeneratedOverrideableRequired<S> => {
return Field({
select: schema,
insert: Schema.optional(Schema.mutable(schema)),
update: Schema.optional(Schema.mutable(schema)),
json: schema,
jsonCreate: Schema.optional(schema),
jsonUpdate: Schema.optional(schema),
});
};
/**
* @since 1.0.0
* @category sensitive
*/
export interface Sensitive<
S extends Schema.Schema.All | Schema.PropertySignature.All,
> extends VariantSchema.Field<{
readonly select: S;
readonly insert: S;
readonly update: S;
}> {}
/**
* A field that represents a sensitive value that should not be exposed in the
* JSON variants.
*
* @since 1.0.0
* @category sensitive
*/
export const Sensitive = <
S extends Schema.Schema.All | Schema.PropertySignature.All,
>(
schema: S,
): Sensitive<S> =>
Field({
select: schema,
insert: schema,
update: schema,
});
/**
* 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 interface FieldOption<S extends Schema.Schema.Any>
extends VariantSchema.Field<{
readonly select: Schema.OptionFromNullOr<S>;
readonly insert: Schema.OptionFromNullOr<S>;
readonly update: Schema.OptionFromNullOr<S>;
readonly json: Schema.optionalWith<S, { as: "Option" }>;
readonly jsonCreate: Schema.optionalWith<
S,
{ as: "Option"; nullable: true }
>;
readonly jsonUpdate: Schema.optionalWith<
S,
{ as: "Option"; nullable: true }
>;
}> {}
/**
* 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: <
Field extends VariantSchema.Field<any> | Schema.Schema.Any,
>(
self: Field,
) => Field extends Schema.Schema.Any
? FieldOption<Field>
: Field extends VariantSchema.Field<infer S>
? VariantSchema.Field<{
readonly [K in keyof S]: S[K] extends Schema.Schema.Any
? K extends VariantsDatabase
? Schema.OptionFromNullOr<S[K]>
: Schema.optionalWith<S[K], { as: "Option"; nullable: true }>
: never;
}>
: 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;
/**
* Convert a field to one that is nullable for all read variants and optional
* for mutation variants.
*
* ### Design Rationale
*
* The `select` and `json` variants use `NullOr` (producing `T | null`) rather
* than converting null to undefined. This preserves the database's null
* semantics in the domain model, making it easier to work with code that
* expects nullable values (like session schemas). While this means JSON
* responses include `"field": null` instead of omitting the field, gzip
* compression makes this overhead negligible.
*
* ### Semantics by Variant
*
* **Read variants** (`select`, `json`): `T | null`
* - Database nulls are preserved as null in the domain model
* - JSON responses include null values (gzip handles compression)
*
* **Mutation variants** use nullish semantics to distinguish "not provided"
* from "explicitly clear":
*
* - **`insert`/`jsonCreate`**: `T | null | undefined` → omit when null/undefined
* - Missing/null/undefined means "use database default"
* - **`update`/`jsonUpdate`**: `T | null | undefined` → preserve null, omit undefined
* - `undefined` = "don't change this field"
* - `null` = "clear this field"
* - `value` = "set to this value"
*
* @since 1.0.0
* @category optional
*/
export interface FieldOptional<S extends Schema.Schema.Any>
extends VariantSchema.Field<{
readonly select: Schema.NullOr<S>;
readonly insert: OmitUndefinedAndNull<Schema.mutable<S>>;
readonly update: PreserveNull<Schema.mutable<S>>;
readonly json: Schema.NullOr<S>;
readonly jsonCreate: OmitUndefinedAndNull<S>;
readonly jsonUpdate: PreserveNull<S>;
}> {}
/**
* Convert a field to omit `undefined`/`null` on update.
*
* This keeps select/insert/json variants unchanged and only affects the update
* variant to provide patch-style semantics.
*
* @since 1.0.0
* @category optional
*/
export interface UpdateOptional<S extends Schema.Schema.Any>
extends VariantSchema.Field<{
readonly select: S;
readonly insert: S;
readonly update: Schema.optional<Schema.mutable<S>>;
readonly json: S;
readonly jsonCreate: S;
readonly jsonUpdate: S;
}> {}
/**
* Convert a field to be optional on update (omit `undefined` / missing).
*
* @since 1.0.0
* @category optional
*/
export const updateOptional: <S extends Schema.Schema.Any>(
self: S,
) => UpdateOptional<S> = fieldEvolve({
update: flow(Schema.mutable, Schema.optional),
}) as never;
/**
* Convert a field to one that is nullable for all read variants and optional
* for mutation variants.
*
* See the interface documentation above for full semantics.
*
* @since 1.0.0
* @category optional
*/
export const FieldOptional: <
Field extends VariantSchema.Field<any> | Schema.Schema.Any,
>(
self: Field,
) => Field extends Schema.Schema.Any
? FieldOptional<Field>
: Field extends VariantSchema.Field<infer S>
? VariantSchema.Field<{
readonly [K in keyof S]: S[K] extends Schema.Schema.Any
? K extends "select"
? Schema.NullOr<S[K]>
: K extends "insert"
? OmitUndefinedAndNull<Schema.mutable<S[K]>>
: K extends "update"
? PreserveNull<Schema.mutable<S[K]>>
: K extends "jsonCreate"
? OmitUndefinedAndNull<S[K]>
: K extends "jsonUpdate"
? PreserveNull<S[K]>
: K extends "json"
? Schema.NullOr<S[K]>
: never
: never;
}>
: never = (self) => {
if (VariantSchema.isField(self)) {
// Handle Field case: directly construct new Field with transformed variants
const newSchemas: any = {};
if (self.schemas.select) {
newSchemas.select = Schema.NullOr(self.schemas.select);
}
if (self.schemas.insert) {
newSchemas.insert = omitUndefinedAndNull(
Schema.mutable(self.schemas.insert),
);
}
if (self.schemas.update) {
newSchemas.update = preserveNull(Schema.mutable(self.schemas.update));
}
if (self.schemas.json) {
newSchemas.json = Schema.NullOr(self.schemas.json);
}
if (self.schemas.jsonCreate) {
newSchemas.jsonCreate = omitUndefinedAndNull(self.schemas.jsonCreate);
}
if (self.schemas.jsonUpdate) {
newSchemas.jsonUpdate = preserveNull(self.schemas.jsonUpdate);
}
return Field(newSchemas) as any;
} else {
// Handle Schema case: create new Field with all variants
const schema = self as Schema.Schema.Any;
return Field({
select: Schema.NullOr(schema),
insert: omitUndefinedAndNull(Schema.mutable(schema)),
update: preserveNull(Schema.mutable(schema)),
json: Schema.NullOr(schema),
jsonCreate: omitUndefinedAndNull(schema),
jsonUpdate: preserveNull(schema),
}) as any;
}
};
// ===== TEMPORAL DATE & TIME FIELDS =====
/**
* @since 1.0.0
* @category date & time
*/
export interface InstantGenerated
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.InstantFromDate;
// readonly update: typeof TemporalSchema.InstantFromDate;
readonly json: typeof TemporalSchema.InstantFromString;
}> {}
/**
* A field that represents a timestamp that is generated by the database (like createdAt).
* It is stored as a JS Date in Gel and transformed to Temporal.Instant for the application.
* Serialized as a string for JSON.
*
* It is omitted from inserts (database generates it) and available for selection and updates.
*
* @since 1.0.0
* @category date & time
*/
export const InstantGenerated: InstantGenerated = Field({
select: TemporalSchema.InstantFromDate,
// update: TemporalSchema.InstantFromDate,
json: TemporalSchema.InstantFromString,
});
export const Instant = Field({
select: TemporalSchema.InstantFromString,
insert: TemporalSchema.InstantFromString,
update: TemporalSchema.InstantFromString,
json: TemporalSchema.InstantFromString,
jsonCreate: TemporalSchema.InstantFromString,
jsonUpdate: TemporalSchema.InstantFromString,
});
/**
* @since 1.0.0
* @category date & time
*/
export interface PlainDateField
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.PlainDateFromString;
readonly insert: typeof TemporalSchema.PlainDateFromString;
readonly update: typeof TemporalSchema.PlainDateFromString;
readonly json: typeof TemporalSchema.PlainDateFromString;
readonly jsonCreate: typeof TemporalSchema.PlainDateFromString;
readonly jsonUpdate: typeof TemporalSchema.PlainDateFromString;
}> {}
/**
* A field that represents a date value (without time).
* Stored as PostgreSQL DATE, queried as ISO string, decoded to Temporal.PlainDate.
*
* @since 1.0.0
* @category date & time
*/
export const PlainDateField: PlainDateField = Field({
select: TemporalSchema.PlainDateFromString,
insert: TemporalSchema.PlainDateFromString,
update: TemporalSchema.PlainDateFromString,
json: TemporalSchema.PlainDateFromString,
jsonCreate: TemporalSchema.PlainDateFromString,
jsonUpdate: TemporalSchema.PlainDateFromString,
});
/**
* @since 1.0.0
* @category date & time
*/
export interface PlainDateGenerated
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.PlainDateFromString;
readonly json: typeof TemporalSchema.PlainDateFromString;
}> {}
/**
* A generated date field (read-only from database).
* Stored as PostgreSQL DATE, queried as ISO string, decoded to Temporal.PlainDate.
*
* @since 1.0.0
* @category date & time
*/
export const PlainDateGenerated: PlainDateGenerated = Field({
select: TemporalSchema.PlainDateFromString,
json: TemporalSchema.PlainDateFromString,
});
/**
* @since 1.0.0
* @category date & time
*/
export interface PlainTimeField
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.PlainTimeFromString;
readonly insert: typeof TemporalSchema.PlainTimeFromString;
readonly update: typeof TemporalSchema.PlainTimeFromString;
readonly json: typeof TemporalSchema.PlainTimeFromString;
}> {}
/**
* A field that represents a time value (without date).
* Stored as a string in the database and decoded to Temporal.PlainTime.
*
* @since 1.0.0
* @category date & time
*/
export const PlainTimeField: PlainTimeField = Field({
select: TemporalSchema.PlainTimeFromString,
insert: TemporalSchema.PlainTimeFromString,
update: TemporalSchema.PlainTimeFromString,
json: TemporalSchema.PlainTimeFromString,
});
/**
* @since 1.0.0
* @category date & time
*/
export interface PlainDateTimeField
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.PlainDateTimeFromString;
readonly insert: typeof TemporalSchema.PlainDateTimeFromString;
readonly update: typeof TemporalSchema.PlainDateTimeFromString;
readonly json: typeof TemporalSchema.PlainDateTimeFromString;
}> {}
/**
* A field that represents a date-time value (without timezone).
* Stored as a string in the database and decoded to Temporal.PlainDateTime.
*
* @since 1.0.0
* @category date & time
*/
export const PlainDateTimeField: PlainDateTimeField = Field({
select: TemporalSchema.PlainDateTimeFromString,
insert: TemporalSchema.PlainDateTimeFromString,
update: TemporalSchema.PlainDateTimeFromString,
json: TemporalSchema.PlainDateTimeFromString,
});
/**
* @since 1.0.0
* @category date & time
*/
export interface ZonedDateTimeField
extends VariantSchema.Field<{
readonly select: typeof TemporalSchema.ZonedDateTimeFromString;
readonly insert: typeof TemporalSchema.ZonedDateTimeFromString;
readonly update: typeof TemporalSchema.ZonedDateTimeFromString;
readonly json: typeof TemporalSchema.ZonedDateTimeFromString;
}> {}
/**
* A field that represents a date-time value with timezone.
* Stored as a string in the database and decoded to Temporal.ZonedDateTime.
*
* @since 1.0.0
* @category date & time
*/
export const ZonedDateTimeField: ZonedDateTimeField = Field({
select: TemporalSchema.ZonedDateTimeFromString,
insert: TemporalSchema.ZonedDateTimeFromString,
update: TemporalSchema.ZonedDateTimeFromString,
json: TemporalSchema.ZonedDateTimeFromString,
});
/**
* @since 1.0.0
* @category json
*/
export interface JsonFromString<
S extends Schema.Schema.All | Schema.PropertySignature.All,
> extends VariantSchema.Field<{
readonly select: Schema.Schema<
Schema.Schema.Type<S>,
string,
Schema.Schema.Context<S>
>;
readonly insert: Schema.Schema<
Schema.Schema.Type<S>,
string,
Schema.Schema.Context<S>
>;
readonly update: Schema.Schema<
Schema.Schema.Type<S>,
string,
Schema.Schema.Context<S>
>;
readonly json: S;
readonly jsonCreate: S;
readonly jsonUpdate: S;
}> {}
/**
* A field that represents a JSON value stored as text in the database.
*
* The "json" variants will use the object schema directly.
*
* @since 1.0.0
* @category json
*/
export const JsonFromString = <
S extends Schema.Schema.All | Schema.PropertySignature.All,
>(
schema: S,
): JsonFromString<S> => {
const parsed = Schema.parseJson(schema as any);
return Field({
select: parsed,
insert: parsed,
update: parsed,
json: schema,
jsonCreate: schema,
jsonUpdate: schema,
}) as any;
};
/**
* @since 1.0.0
* @category relations
*/
export interface BelongsToOptional<M extends AnyNoContext>
extends VariantSchema.Field<{
readonly select: Schema.NullOr<M>;
readonly insert: Schema.optional<M["insert"]>;
readonly update: Schema.optional<M["update"]>;
readonly json: NullToOptional<M["json"]>;
readonly jsonCreate: Schema.optional<M["jsonCreate"]>;
readonly jsonUpdate: Schema.optional<M["jsonUpdate"]>;
}> {}
/**
* An optional "belongs-to" relation where this model has a nullable FK column.
*
* Use when: **You have a nullable FK column pointing to a parent model.**
*
* ```
* ┌─────────────────┐ ┌─────────────────┐
* │ RaceEdition │ │ FirstCycling │
* ├─────────────────┤ ├─────────────────┤
* │ fc_id (FK,NULL) │─ ─ ─ ─ ▶│ id (PK) │
* └─────────────────┘ └─────────────────┘
* YOU (child) PARENT
* has nullable FK
* ```
*
* - **select**: `M | null` — Drizzle `r.one.*` returns single object or null
* - **insert/update**: optional
* - **json**: omits property when null
*
* @since 1.0.0
* @category relations
* @example
* ```ts
* export class RaceEdition extends Model.Class<RaceEdition>("RaceEdition")({
* id: Model.Generated(Schema.String),
* name: Schema.String,
* // Optional FK - fc_id column can be null
* fc: Model.BelongsToOptional(Source.FirstCycling),
* }) {}
* ```
*/
export const BelongsToOptional = <M extends AnyNoContext>(
model: M,
): BelongsToOptional<M> =>
Field({
select: Schema.NullOr(model),
insert: Schema.optional(model.insert),
update: Schema.optional(model.update),
// json variant: Type is T | null (matches select), Encoded omits when null
json: nullToOptional(model.json),
jsonCreate: Schema.optional(model.jsonCreate),
jsonUpdate: Schema.optional(model.jsonUpdate),
});
/**
* Type helper to extract variant properties from a model or union.
* Used to relax the constraint on HasOne/BelongsTo to accept both
* Model.Class and Model.Union results.
*
* @since 1.0.0
* @category models
*/
export type ModelWithVariants = Schema.Schema.AnyNoContext & {
readonly insert: Schema.Schema.AnyNoContext;
readonly update: Schema.Schema.AnyNoContext;
readonly json: Schema.Schema.AnyNoContext;
readonly jsonCreate: Schema.Schema.AnyNoContext;
readonly jsonUpdate: Schema.Schema.AnyNoContext;
};
/**
* @since 1.0.0
* @category relations
*/
export interface HasOne<M extends ModelWithVariants>
extends VariantSchema.Field<{
// Drizzle returns 1:1 relations as arrays, so select Encoded is regular array
// The headSingleton transform extracts single element, converting readonly I[] -> A
readonly select: Schema.transformOrFail<
Schema.Array$<M>,
Schema.SchemaClass<Schema.Schema.Type<M>>
>;
readonly insert: M["insert"];
readonly update: M["update"];
readonly json: M["json"];
readonly jsonCreate: M["jsonCreate"];
readonly jsonUpdate: M["jsonUpdate"];
}> {}
/**
* A required "has-one" relation where a child model has an FK pointing to you.
*
* Use when: **A child table has an FK column pointing to your PK, and you expect
* exactly one child.**
*
* ```
* ┌─────────────────┐ ┌─────────────────┐
* │ RaceEdition │ │ RaceDay │
* ├─────────────────┤ ├─────────────────┤
* │ id (PK) │◀────────│ edition_id (FK) │
* └─────────────────┘ └─────────────────┘
* YOU (parent) CHILD
* no FK here has the FK to you
* ```
*
* - **select**: Drizzle `r.many.*` returns array; `HasOne` validates exactly 1 element
* and extracts it (fails if 0 or >1)
* - **insert/update**: required
* - **json**: required
*
* @since 1.0.0
* @category relations
* @example
* ```ts
* export class OneDayRace extends Model.Class<OneDayRace>("OneDayRace")({
* id: Model.Generated(Schema.String),
* name: Schema.String,
* // Parent looking at child - child has edition_id FK
* // Drizzle returns array, HasOne extracts single element
* raceDay: Model.HasOne(RaceDayBase.OneDay.Base).pipe(
* Model.fieldFromKey({ select: "raceDays" }),
* ),
* }) {}
* ```
*/
export function HasOne<M extends ModelWithVariants>(model: M): HasOne<M> {
return Field({
// Drizzle returns 1:1 relations as arrays; extract single element
select: model.pipe(headSingleton()),
insert: model.insert,
update: model.update,
json: model.json,
jsonCreate: model.jsonCreate,
jsonUpdate: model.jsonUpdate,
});
}
/**
* @since 1.0.0
* @category relations
*/
export interface BelongsTo<M extends ModelWithVariants>
extends VariantSchema.Field<{
// Drizzle r.one returns single object (not array) for belongs-to relations
readonly select: M;
readonly insert: M["insert"];
readonly update: M["update"];
readonly json: M["json"];
readonly jsonCreate: M["jsonCreate"];
readonly jsonUpdate: M["jsonUpdate"];
}> {}
/**
* A required "belongs-to" relation where this model has an FK column.
*
* Use when: **You have an FK column pointing to a parent model.**
*
* ```
* ┌─────────────────┐ ┌─────────────────┐
* │ RaceDay │ │ RaceEdition │
* ├─────────────────┤ ├─────────────────┤
* │ edition_id (FK) │────────▶│ id (PK) │
* └─────────────────┘ └─────────────────┘
* YOU (child) PARENT
* has the FK
* ```
*
* - **select**: `M` — Drizzle `r.one.*` returns single object directly
* - **insert/update**: required
* - **json**: required
*
* @since 1.0.0
* @category relations
* @example
* ```ts
* export class RaceDay extends Model.Class<RaceDay>("RaceDay")({
* id: Model.Generated(Schema.String),
* date: Model.PlainDateField,
* // Child looking at parent - you have edition_id FK
* // Drizzle returns single object
* edition: Model.BelongsTo(RaceEditionBase.Any),
* }) {}
* ```
*/
export function BelongsTo<M extends ModelWithVariants>(model: M): BelongsTo<M> {
return Field({
// Drizzle r.one returns single object for belongs-to
select: model,
insert: model.insert,
update: model.update,
json: model.json,
jsonCreate: model.jsonCreate,
jsonUpdate: model.jsonUpdate,
});
}
/**
* @since 1.0.0
* @category relations
*/
export interface HasMany<M extends AnyNoContext>
extends VariantSchema.Field<{
readonly select: Schema.Array$<M>;
readonly insert: Schema.optional<Schema.Array$<M["insert"]>>;
readonly update: Schema.optional<Schema.Array$<M["update"]>>;
readonly json: Schema.Array$<M["json"]>;
readonly jsonCreate: Schema.optional<Schema.Array$<M["jsonCreate"]>>;
readonly jsonUpdate: Schema.optional<Schema.Array$<M["jsonUpdate"]>>;
}> {}
/**
* A "has-many" relation where child models have FK columns pointing to you.
*
* Use when: **Child tables have FK columns pointing to your PK.**
*
* ```
* ┌─────────────────┐ ┌─────────────────┐
* │ RaceEdition │ │ RaceDay │
* ├─────────────────┤ ├─────────────────┤
* │ id (PK) │◀────────│ edition_id (FK) │
* │ │◀────────│ edition_id (FK) │
* │ │◀────────│ edition_id (FK) │
* └─────────────────┘ └─────────────────┘
* YOU (parent) CHILDREN
* no FK here have FKs to you
* ```
*
* - **select**: `M[]` — Drizzle `r.many.*` returns array
* - **insert/update**: optional array
* - **json**: array, optional for create/update
*
* @since 1.0.0
* @category relations
* @example
* ```ts
* export class StageRace extends Model.Class<StageRace>("StageRace")({
* id: Model.Generated(Schema.String),
* name: Schema.String,
* // Parent looking at children - children have edition_id FK
* stages: Model.HasMany(RaceDayBase.Stage.Base).pipe(
* Model.fieldFromKey({ select: "raceDays" }),
* ),
* }) {}
* ```
*/
export const HasMany = <M extends AnyNoContext>(model: M): HasMany<M> =>
Field({
select: Schema.Array(model),
insert: Schema.optional(Schema.Array(model.insert)),
update: Schema.optional(Schema.Array(model.update)),
json: Schema.Array(model.json),
jsonCreate: Schema.optional(Schema.Array(model.jsonCreate)),
jsonUpdate: Schema.optional(Schema.Array(model.jsonUpdate)),
});
/**
* A boolean parsed from 0 or 1
*
* @since 1.0.0
* @category uuid
*/
export class BooleanFromNumber extends Schema.transform(
Schema.Literal(0, 1),
Schema.Boolean,
{
decode: (n) => n === 1,
encode: (b) => (b ? 1 : 0),
},
) {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment