Created
January 16, 2026 10:38
-
-
Save kristojorg/42e3f7925971c39c7309e3e8c17cb64b to your computer and use it in GitHub Desktop.
Modified Model.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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