packages feed

poppy-codegen-1.0.0: src/Poppy/Codegen/IR.hs

-- | Schema value types. Application code builds Schemas with "Poppy.Codegen.Schema", not this module.
module Poppy.Codegen.IR
  ( Schema (..),
    Model (..),
    EnumSpec (..),
    EnumVariant (..),
    FieldSpec (..),
    FieldType (..),
    FieldDefault (..),
    RelationSpec (..),
    RelationKind (..),
    JoinKind (..),
    UniqueConstraint (..),
    field,
    uuid,
    text,
    int,
    numeric,
    jsonb,
    timestamptz,
    bool,
    enumField,
    pk,
    nullable,
    updatedAt,
    withDefault,
    column,
    enum_,
    enumImportFrom,
    variant,
    variantMap,
    hasMany,
    belongsTo,
    unique_,
  )
where

import Data.Text (Text)

-- | Enums, models, and uniques.
data Schema = Schema
  { schemaEnums :: [EnumSpec],
    schemaModels :: [Model],
    schemaUniques :: [UniqueConstraint]
  }
  deriving (Show, Eq)

-- | Unique constraint: model name plus field names (not columns).
data UniqueConstraint = UniqueConstraint
  { uniqueModel :: Text,
    uniqueFields :: [Text]
  }
  deriving (Show, Eq)

-- | Postgres enum in the Schema.
data EnumSpec = EnumSpec
  { enumName :: Text,
    enumVariants :: [EnumVariant],
    enumImport :: Maybe Text
  }
  deriving (Show, Eq)

-- | Haskell constructor and optional distinct Postgres label.
data EnumVariant = EnumVariant
  { variantName :: Text,
    variantDbValue :: Maybe Text
  }
  deriving (Show, Eq)

-- | One table: fields and relations.
data Model = Model
  { modelName :: Text,
    modelTable :: Text,
    modelFields :: [FieldSpec],
    modelRelations :: [RelationSpec]
  }
  deriving (Show, Eq)

-- | Column types Codegen emits and drift-checks.
data FieldType
  = TyText
  | TyUuid
  | TyInt
  | TyNumeric
  | TyJsonb
  | TyTimestamptz
  | TyBool
  | TyEnum Text
  deriving (Show, Eq)

-- | Application-side default; the SQL column must have a matching @DEFAULT@.
data FieldDefault
  = DefaultUuidV4
  | DefaultNow
  deriving (Show, Eq)

-- | One column on a model.
data FieldSpec = FieldSpec
  { fieldName :: Text,
    fieldColumn :: Text,
    fieldType :: FieldType,
    fieldNullable :: Bool,
    fieldIsPrimaryKey :: Bool,
    fieldDefault :: Maybe FieldDefault,
    fieldUpdatedAt :: Bool
  }
  deriving (Show, Eq)

-- | @RelHasMany@ or @RelBelongsTo@.
data RelationKind = RelHasMany | RelBelongsTo
  deriving (Show, Eq)

-- | @LEFT@ / @INNER@ / @RIGHT@ for ad-hoc join emission.
data JoinKind = JoinLeft | JoinInner | JoinRight
  deriving (Show, Eq)

-- | @hasMany@ or @belongsTo@ edge on a model.
data RelationSpec = RelationSpec
  { relName :: Text,
    relKind :: RelationKind,
    relFromModel :: Text,
    relToModel :: Text,
    relLocalField :: Text,
    relForeignField :: Text,
    relJoin :: JoinKind
  }
  deriving (Show, Eq)

field :: Text -> FieldType -> FieldSpec
field name ty =
  FieldSpec
    { fieldName = name,
      fieldColumn = name,
      fieldType = ty,
      fieldNullable = False,
      fieldIsPrimaryKey = False,
      fieldDefault = Nothing,
      fieldUpdatedAt = False
    }

uuid :: Text -> FieldSpec
uuid name = field name TyUuid

text :: Text -> FieldSpec
text name = field name TyText

int :: Text -> FieldSpec
int name = field name TyInt

numeric :: Text -> FieldSpec
numeric name = field name TyNumeric

jsonb :: Text -> FieldSpec
jsonb name = field name TyJsonb

timestamptz :: Text -> FieldSpec
timestamptz name = field name TyTimestamptz

bool :: Text -> FieldSpec
bool name = field name TyBool

enumField :: Text -> Text -> FieldSpec
enumField name enumName = field name (TyEnum enumName)

-- | Exactly one primary key per model.
pk :: FieldSpec -> FieldSpec
pk f = f {fieldIsPrimaryKey = True}

-- | @Maybe@ on the row; create/update use 'Poppy.NullableValue'.
nullable :: FieldSpec -> FieldSpec
nullable f = f {fieldNullable = True}

-- | Client writes set this column to now.
updatedAt :: FieldSpec -> FieldSpec
updatedAt f = f {fieldUpdatedAt = True}

withDefault :: FieldDefault -> FieldSpec -> FieldSpec
withDefault d f = f {fieldDefault = Just d}

column :: Text -> FieldSpec -> FieldSpec
column col f = f {fieldColumn = col}

-- | Postgres enum. Labels default to the Haskell constructor names.
enum_ :: Text -> [EnumVariant] -> EnumSpec
enum_ name variants =
  EnumSpec {enumName = name, enumVariants = variants, enumImport = Nothing}

-- | Skip generating the sum type; import this module instead.
enumImportFrom :: Text -> EnumSpec -> EnumSpec
enumImportFrom moduleName e = e {enumImport = Just moduleName}

-- | Enum constructor; Postgres label is the same spelling.
variant :: Text -> EnumVariant
variant name = EnumVariant {variantName = name, variantDbValue = Nothing}

-- | Enum constructor with a different Postgres label.
variantMap :: Text -> Text -> EnumVariant
variantMap name dbValue =
  EnumVariant {variantName = name, variantDbValue = Just dbValue}

hasMany :: Text -> Text -> Text -> Text -> Text -> RelationSpec
hasMany name fromModel toModel localFld foreignFld =
  RelationSpec
    { relName = name,
      relKind = RelHasMany,
      relFromModel = fromModel,
      relToModel = toModel,
      relLocalField = localFld,
      relForeignField = foreignFld,
      relJoin = JoinLeft
    }

belongsTo :: Text -> Text -> Text -> Text -> Text -> RelationSpec
belongsTo name fromModel toModel foreignFld referencedFld =
  RelationSpec
    { relName = name,
      relKind = RelBelongsTo,
      relFromModel = fromModel,
      relToModel = toModel,
      relLocalField = referencedFld,
      relForeignField = foreignFld,
      relJoin = JoinLeft
    }

-- | Unique constraint (field names, not columns). @findUnique@ and @upsert@ conflict use these.
unique_ :: Text -> [Text] -> UniqueConstraint
unique_ model fields = UniqueConstraint {uniqueModel = model, uniqueFields = fields}