2021-02-14 09:07:52 +03:00
|
|
|
|
module Hasura.Backends.Postgres.DDL.Table
|
|
|
|
|
( createTableEventTrigger
|
|
|
|
|
, buildEventTriggerInfo
|
|
|
|
|
, updateColumnInEventTrigger
|
|
|
|
|
, fetchAndValidateEnumValues
|
|
|
|
|
, delTriggerQ
|
|
|
|
|
, mkAllTriggersQ
|
|
|
|
|
, getHeaderInfosFromConf
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
|
2021-03-15 16:02:58 +03:00
|
|
|
|
import Hasura.Prelude
|
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import qualified Data.Environment as Env
|
|
|
|
|
import qualified Data.HashMap.Strict as Map
|
|
|
|
|
import qualified Data.List.NonEmpty as NE
|
|
|
|
|
import qualified Data.Sequence as Seq
|
|
|
|
|
import qualified Data.Sequence.NonEmpty as NESeq
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import qualified Data.Text.Lazy as TL
|
|
|
|
|
import qualified Database.PG.Query as Q
|
|
|
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
|
import qualified Text.Shakespeare.Text as ST
|
|
|
|
|
|
|
|
|
|
import Control.Monad.Trans.Control (MonadBaseControl)
|
|
|
|
|
import Control.Monad.Validate
|
2021-03-16 20:35:35 +03:00
|
|
|
|
import Data.FileEmbed (makeRelativeToProject)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Data.List (delete)
|
|
|
|
|
import Data.Text.Extended
|
|
|
|
|
|
2021-03-15 16:02:58 +03:00
|
|
|
|
import qualified Hasura.SQL.AnyBackend as AB
|
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Hasura.Backends.Postgres.Connection
|
|
|
|
|
import Hasura.Backends.Postgres.SQL.DML
|
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
2021-05-11 18:18:31 +03:00
|
|
|
|
import Hasura.Base.Error
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Hasura.RQL.DDL.Headers
|
2021-04-22 00:44:37 +03:00
|
|
|
|
import Hasura.RQL.Types.Backend
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Hasura.RQL.Types.Column
|
|
|
|
|
import Hasura.RQL.Types.Common
|
|
|
|
|
import Hasura.RQL.Types.EventTrigger
|
|
|
|
|
import Hasura.RQL.Types.SchemaCache
|
|
|
|
|
import Hasura.RQL.Types.SchemaCacheTypes
|
|
|
|
|
import Hasura.RQL.Types.Table
|
2021-04-22 00:44:37 +03:00
|
|
|
|
import Hasura.SQL.Backend
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
import Hasura.Server.Types
|
|
|
|
|
import Hasura.Server.Utils
|
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-- | Create the table event trigger in the database in a @'/v1/query' API
|
|
|
|
|
-- transaction as soon as after @'runCreateEventTriggerQuery' is called and
|
|
|
|
|
-- in building schema cache.
|
|
|
|
|
createTableEventTrigger
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: (Backend ('Postgres pgKind), MonadIO m, MonadBaseControl IO m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> ServerConfigCtx
|
|
|
|
|
-> PGSourceConfig
|
|
|
|
|
-> QualifiedTable
|
2021-04-22 00:44:37 +03:00
|
|
|
|
-> [ColumnInfo ('Postgres pgKind)]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-> TriggerName
|
|
|
|
|
-> TriggerOpsDef
|
|
|
|
|
-> m (Either QErr ())
|
|
|
|
|
createTableEventTrigger serverConfigCtx sourceConfig table columns triggerName opsDefinition = runPgSourceWriteTx sourceConfig $ do
|
|
|
|
|
-- Clean all existing triggers
|
|
|
|
|
liftTx $ delTriggerQ triggerName -- executes DROP IF EXISTS.. sql
|
|
|
|
|
-- Create the given triggers
|
|
|
|
|
flip runReaderT serverConfigCtx $
|
|
|
|
|
mkAllTriggersQ triggerName table columns opsDefinition
|
|
|
|
|
|
|
|
|
|
delTriggerQ :: TriggerName -> Q.TxE QErr ()
|
|
|
|
|
delTriggerQ trn =
|
|
|
|
|
mapM_ (\op -> Q.unitQE
|
|
|
|
|
defaultTxErrorHandler
|
|
|
|
|
(Q.fromText $ getDropFuncSql op) () False) [INSERT, UPDATE, DELETE]
|
|
|
|
|
where
|
|
|
|
|
getDropFuncSql :: Ops -> T.Text
|
|
|
|
|
getDropFuncSql op =
|
|
|
|
|
"DROP FUNCTION IF EXISTS"
|
|
|
|
|
<> " hdb_catalog." <> pgIdenTrigger op trn <> "()"
|
|
|
|
|
<> " CASCADE"
|
|
|
|
|
|
|
|
|
|
-- pgIdenTrigger is a method used to construct the name of the pg function
|
|
|
|
|
-- used for event triggers which are present in the hdb_catalog schema.
|
|
|
|
|
pgIdenTrigger:: Ops -> TriggerName -> Text
|
|
|
|
|
pgIdenTrigger op trn = pgFmtIdentifier . qualifyTriggerName op $ triggerNameToTxt trn
|
|
|
|
|
where
|
|
|
|
|
qualifyTriggerName op' trn' = "notify_hasura_" <> trn' <> "_" <> tshow op'
|
|
|
|
|
|
|
|
|
|
mkAllTriggersQ
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: forall pgKind m
|
|
|
|
|
. (Backend ('Postgres pgKind), MonadTx m, MonadReader ServerConfigCtx m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> TriggerName
|
|
|
|
|
-> QualifiedTable
|
2021-04-22 00:44:37 +03:00
|
|
|
|
-> [ColumnInfo ('Postgres pgKind)]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-> TriggerOpsDef
|
|
|
|
|
-> m ()
|
|
|
|
|
mkAllTriggersQ trn qt allCols fullspec = do
|
|
|
|
|
onJust (tdInsert fullspec) (mkTriggerQ trn qt allCols INSERT)
|
|
|
|
|
onJust (tdUpdate fullspec) (mkTriggerQ trn qt allCols UPDATE)
|
|
|
|
|
onJust (tdDelete fullspec) (mkTriggerQ trn qt allCols DELETE)
|
|
|
|
|
|
|
|
|
|
data OpVar = OLD | NEW deriving (Show)
|
|
|
|
|
|
2021-05-20 10:23:21 +03:00
|
|
|
|
-- | Formats each columns to appropriate SQL expression
|
2021-04-22 00:44:37 +03:00
|
|
|
|
toJSONableExp :: Bool -> ColumnType ('Postgres pgKind) -> Bool -> SQLExp -> SQLExp
|
2021-05-20 10:23:21 +03:00
|
|
|
|
toJSONableExp strfyNum colTy asText expn
|
|
|
|
|
-- If its a numeric column greater than a 32-bit integer, we have to stringify it as JSON spec doesn't support >32-bit integers
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| asText || (isScalarColumnWhere isBigNum colTy && strfyNum) =
|
|
|
|
|
expn `SETyAnn` textTypeAnn
|
2021-05-20 10:23:21 +03:00
|
|
|
|
-- If the column is either a `Geometry` or `Geography` then apply the `ST_AsGeoJSON` function to convert it into GeoJSON format
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| isScalarColumnWhere isGeoType colTy =
|
|
|
|
|
SEFnApp "ST_AsGeoJSON"
|
|
|
|
|
[ expn
|
|
|
|
|
, SEUnsafe "15" -- max decimal digits
|
|
|
|
|
, SEUnsafe "4" -- to print out crs
|
2021-05-20 10:23:21 +03:00
|
|
|
|
] Nothing `SETyAnn` jsonTypeAnn
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| otherwise = expn
|
|
|
|
|
|
2021-05-20 10:23:21 +03:00
|
|
|
|
-- | Define the pgSQL trigger functions on database events.
|
2021-02-14 09:07:52 +03:00
|
|
|
|
mkTriggerQ
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: forall pgKind m
|
|
|
|
|
. (Backend ('Postgres pgKind), MonadTx m, MonadReader ServerConfigCtx m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> TriggerName
|
|
|
|
|
-> QualifiedTable
|
2021-04-22 00:44:37 +03:00
|
|
|
|
-> [ColumnInfo ('Postgres pgKind)]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-> Ops
|
|
|
|
|
-> SubscribeOpSpec
|
|
|
|
|
-> m ()
|
2021-05-20 10:23:21 +03:00
|
|
|
|
mkTriggerQ trn qt@(QualifiedObject schema table) allCols op (SubscribeOpSpec listenColumns deliveryColumns') = do
|
2021-02-14 09:07:52 +03:00
|
|
|
|
strfyNum <- stringifyNum . _sccSQLGenCtx <$> ask
|
|
|
|
|
liftTx $ Q.multiQE defaultTxErrorHandler $ Q.fromText . TL.toStrict $
|
2021-05-20 10:23:21 +03:00
|
|
|
|
let
|
|
|
|
|
-- If there are no specific delivery columns selected by user then all the columns will be delivered
|
|
|
|
|
-- in payload hence 'SubCStar'.
|
|
|
|
|
deliveryColumns = fromMaybe SubCStar deliveryColumns'
|
|
|
|
|
getApplicableColumns = \case
|
|
|
|
|
SubCStar -> allCols
|
|
|
|
|
SubCArray cols -> getColInfos cols allCols
|
|
|
|
|
|
|
|
|
|
-- Columns that should be present in the payload. By default, all columns are present.
|
|
|
|
|
applicableDeliveryCols = getApplicableColumns deliveryColumns
|
|
|
|
|
getRowExpression opVar = applyRowToJson' $ mkRowExpression opVar strfyNum applicableDeliveryCols
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-05-20 10:23:21 +03:00
|
|
|
|
-- Columns that user subscribed to listen for changes. By default, we listen on all columns.
|
|
|
|
|
applicableListenCols = getApplicableColumns listenColumns
|
|
|
|
|
renderRow opVar = applyRow $ mkRowExpression opVar strfyNum applicableListenCols
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
oldDataExp = case op of
|
|
|
|
|
INSERT -> SENull
|
|
|
|
|
UPDATE -> getRowExpression OLD
|
|
|
|
|
DELETE -> getRowExpression OLD
|
|
|
|
|
MANUAL -> SENull
|
|
|
|
|
newDataExp = case op of
|
|
|
|
|
INSERT -> getRowExpression NEW
|
|
|
|
|
UPDATE -> getRowExpression NEW
|
|
|
|
|
DELETE -> SENull
|
|
|
|
|
MANUAL -> SENull
|
|
|
|
|
|
|
|
|
|
name = triggerNameToTxt trn
|
|
|
|
|
qualifiedTriggerName = pgIdenTrigger op trn
|
|
|
|
|
qualifiedTable = toSQLTxt qt
|
|
|
|
|
schemaName = pgFmtLit $ getSchemaTxt schema
|
|
|
|
|
tableName = pgFmtLit $ getTableTxt table
|
|
|
|
|
|
|
|
|
|
operation = tshow op
|
|
|
|
|
oldRow = toSQLTxt $ renderRow OLD
|
|
|
|
|
newRow = toSQLTxt $ renderRow NEW
|
|
|
|
|
oldPayloadExpression = toSQLTxt oldDataExp
|
|
|
|
|
newPayloadExpression = toSQLTxt newDataExp
|
|
|
|
|
|
2021-03-16 20:35:35 +03:00
|
|
|
|
in $(makeRelativeToProject "src-rsr/trigger.sql.shakespeare" >>= ST.stextFile )
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
|
|
|
|
applyRowToJson' e = SEFnApp "row_to_json" [e] Nothing
|
|
|
|
|
applyRow e = SEFnApp "row" [e] Nothing
|
|
|
|
|
opToQual = QualVar . tshow
|
|
|
|
|
|
2021-05-20 10:23:21 +03:00
|
|
|
|
mkRowExpression opVar strfyNum columns
|
|
|
|
|
= mkRowExp $ map (\col -> toExtractor (mkQId opVar strfyNum col) col) columns
|
|
|
|
|
|
|
|
|
|
mkQId opVar strfyNum colInfo = toJSONableExp strfyNum (pgiType colInfo) False $
|
|
|
|
|
SEQIdentifier $ QIdentifier (opToQual opVar) $ toIdentifier $ pgiColumn colInfo
|
|
|
|
|
|
|
|
|
|
-- Generate the SQL expression
|
|
|
|
|
toExtractor sqlExp column
|
|
|
|
|
-- If the column type is either 'Geography' or 'Geometry', then after applying the 'ST_AsGeoJSON' function
|
|
|
|
|
-- to the column, alias the value of the expression with the column name else it uses `st_asgeojson` as
|
|
|
|
|
-- the column name.
|
|
|
|
|
| isScalarColumnWhere isGeoType (pgiType column) = Extractor sqlExp (Just $ getAlias column)
|
|
|
|
|
| otherwise = Extractor sqlExp Nothing
|
|
|
|
|
getAlias col = toAlias $ Identifier $ getPGColTxt (pgiColumn col)
|
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
|
buildEventTriggerInfo
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: forall (pgKind :: PostgresKind) m
|
|
|
|
|
. (Backend ('Postgres pgKind), QErrM m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> Env.Environment
|
|
|
|
|
-> SourceName
|
|
|
|
|
-> QualifiedTable
|
|
|
|
|
-> EventTriggerConf
|
2021-03-24 05:55:41 +03:00
|
|
|
|
-> m (EventTriggerInfo, [SchemaDependency])
|
2021-02-14 09:07:52 +03:00
|
|
|
|
buildEventTriggerInfo env source qt (EventTriggerConf name def webhook webhookFromEnv rconf mheaders) = do
|
|
|
|
|
webhookConf <- case (webhook, webhookFromEnv) of
|
|
|
|
|
(Just w, Nothing) -> return $ WCValue w
|
|
|
|
|
(Nothing, Just wEnv) -> return $ WCEnv wEnv
|
|
|
|
|
_ -> throw500 "expected webhook or webhook_from_env"
|
|
|
|
|
let headerConfs = fromMaybe [] mheaders
|
|
|
|
|
webhookInfo <- getWebhookInfoFromConf env webhookConf
|
|
|
|
|
headerInfos <- getHeaderInfosFromConf env headerConfs
|
2021-03-24 05:55:41 +03:00
|
|
|
|
let eTrigInfo = EventTriggerInfo name def rconf webhookInfo headerInfos
|
2021-03-15 16:02:58 +03:00
|
|
|
|
tabDep = SchemaDependency
|
|
|
|
|
(SOSourceObj source
|
|
|
|
|
$ AB.mkAnyBackend
|
2021-04-22 00:44:37 +03:00
|
|
|
|
$ SOITable @('Postgres pgKind) qt)
|
2021-03-15 16:02:58 +03:00
|
|
|
|
DRParent
|
2021-04-22 00:44:37 +03:00
|
|
|
|
pure (eTrigInfo, tabDep:getTrigDefDeps @pgKind source qt def)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
getTrigDefDeps
|
|
|
|
|
:: forall (pgKind :: PostgresKind)
|
|
|
|
|
. (Backend ('Postgres pgKind))
|
|
|
|
|
=> SourceName
|
|
|
|
|
-> QualifiedTable
|
|
|
|
|
-> TriggerOpsDef
|
|
|
|
|
-> [SchemaDependency]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
getTrigDefDeps source qt (TriggerOpsDef mIns mUpd mDel _) =
|
|
|
|
|
mconcat $ catMaybes [ subsOpSpecDeps <$> mIns
|
|
|
|
|
, subsOpSpecDeps <$> mUpd
|
|
|
|
|
, subsOpSpecDeps <$> mDel
|
|
|
|
|
]
|
|
|
|
|
where
|
|
|
|
|
subsOpSpecDeps :: SubscribeOpSpec -> [SchemaDependency]
|
|
|
|
|
subsOpSpecDeps os =
|
|
|
|
|
let cols = getColsFromSub $ sosColumns os
|
|
|
|
|
colDeps = flip map cols $ \col ->
|
2021-03-15 16:02:58 +03:00
|
|
|
|
SchemaDependency
|
|
|
|
|
(SOSourceObj source
|
|
|
|
|
$ AB.mkAnyBackend
|
2021-04-22 00:44:37 +03:00
|
|
|
|
$ SOITableObj @('Postgres pgKind) qt (TOCol @('Postgres pgKind) col))
|
2021-03-15 16:02:58 +03:00
|
|
|
|
DRColumn
|
2021-02-14 09:07:52 +03:00
|
|
|
|
payload = maybe [] getColsFromSub (sosPayload os)
|
|
|
|
|
payloadDeps = flip map payload $ \col ->
|
2021-03-15 16:02:58 +03:00
|
|
|
|
SchemaDependency
|
|
|
|
|
(SOSourceObj source
|
|
|
|
|
$ AB.mkAnyBackend
|
2021-04-22 00:44:37 +03:00
|
|
|
|
$ SOITableObj qt (TOCol @('Postgres pgKind) col))
|
2021-03-15 16:02:58 +03:00
|
|
|
|
DRPayload
|
2021-02-14 09:07:52 +03:00
|
|
|
|
in colDeps <> payloadDeps
|
|
|
|
|
getColsFromSub sc = case sc of
|
|
|
|
|
SubCStar -> []
|
|
|
|
|
SubCArray pgcols -> pgcols
|
|
|
|
|
|
|
|
|
|
getHeaderInfosFromConf
|
|
|
|
|
:: QErrM m
|
|
|
|
|
=> Env.Environment
|
|
|
|
|
-> [HeaderConf]
|
|
|
|
|
-> m [EventHeaderInfo]
|
|
|
|
|
getHeaderInfosFromConf env = mapM getHeader
|
|
|
|
|
where
|
|
|
|
|
getHeader :: QErrM m => HeaderConf -> m EventHeaderInfo
|
|
|
|
|
getHeader hconf = case hconf of
|
|
|
|
|
(HeaderConf _ (HVValue val)) -> return $ EventHeaderInfo hconf val
|
|
|
|
|
(HeaderConf _ (HVEnv val)) -> do
|
|
|
|
|
envVal <- getEnv env val
|
|
|
|
|
return $ EventHeaderInfo hconf envVal
|
|
|
|
|
|
|
|
|
|
getWebhookInfoFromConf
|
|
|
|
|
:: QErrM m
|
|
|
|
|
=> Env.Environment
|
|
|
|
|
-> WebhookConf
|
|
|
|
|
-> m WebhookConfInfo
|
|
|
|
|
getWebhookInfoFromConf env wc = case wc of
|
|
|
|
|
WCValue w -> do
|
|
|
|
|
resolvedWebhook <- resolveWebhook env w
|
|
|
|
|
return $ WebhookConfInfo wc $ unResolvedWebhook resolvedWebhook
|
|
|
|
|
WCEnv we -> do
|
|
|
|
|
envVal <- getEnv env we
|
|
|
|
|
return $ WebhookConfInfo wc envVal
|
|
|
|
|
|
|
|
|
|
updateColumnInEventTrigger
|
|
|
|
|
:: QualifiedTable
|
|
|
|
|
-> PGCol
|
|
|
|
|
-> PGCol
|
|
|
|
|
-> QualifiedTable
|
|
|
|
|
-> EventTriggerConf -> EventTriggerConf
|
|
|
|
|
updateColumnInEventTrigger table oCol nCol refTable = rewriteEventTriggerConf
|
|
|
|
|
where
|
|
|
|
|
rewriteSubsCols = \case
|
|
|
|
|
SubCStar -> SubCStar
|
|
|
|
|
SubCArray cols -> SubCArray $ map getNewCol cols
|
2021-05-20 10:23:21 +03:00
|
|
|
|
rewriteOpSpec (SubscribeOpSpec listenColumns deliveryColumns) =
|
2021-02-14 09:07:52 +03:00
|
|
|
|
SubscribeOpSpec
|
2021-05-20 10:23:21 +03:00
|
|
|
|
(rewriteSubsCols listenColumns)
|
|
|
|
|
(rewriteSubsCols <$> deliveryColumns)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
rewriteTrigOpsDef (TriggerOpsDef ins upd del man) =
|
|
|
|
|
TriggerOpsDef
|
|
|
|
|
(rewriteOpSpec <$> ins)
|
|
|
|
|
(rewriteOpSpec <$> upd)
|
|
|
|
|
(rewriteOpSpec <$> del)
|
|
|
|
|
man
|
|
|
|
|
rewriteEventTriggerConf etc =
|
|
|
|
|
etc { etcDefinition =
|
|
|
|
|
rewriteTrigOpsDef $ etcDefinition etc
|
|
|
|
|
}
|
|
|
|
|
getNewCol col =
|
|
|
|
|
if table == refTable && oCol == col then nCol else col
|
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
data EnumTableIntegrityError (b :: BackendType)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
= EnumTablePostgresError !Text
|
|
|
|
|
| EnumTableMissingPrimaryKey
|
|
|
|
|
| EnumTableMultiColumnPrimaryKey ![PGCol]
|
2021-04-22 00:44:37 +03:00
|
|
|
|
| EnumTableNonTextualPrimaryKey !(RawColumnInfo b)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| EnumTableNoEnumValues
|
|
|
|
|
| EnumTableInvalidEnumValueNames !(NE.NonEmpty Text)
|
2021-04-22 00:44:37 +03:00
|
|
|
|
| EnumTableNonTextualCommentColumn !(RawColumnInfo b)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| EnumTableTooManyColumns ![PGCol]
|
|
|
|
|
|
|
|
|
|
fetchAndValidateEnumValues
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: forall pgKind m
|
|
|
|
|
. (Backend ('Postgres pgKind), MonadIO m, MonadBaseControl IO m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> PGSourceConfig
|
|
|
|
|
-> QualifiedTable
|
2021-04-22 00:44:37 +03:00
|
|
|
|
-> Maybe (PrimaryKey ('Postgres pgKind) (RawColumnInfo ('Postgres pgKind)))
|
|
|
|
|
-> [RawColumnInfo ('Postgres pgKind)]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-> m (Either QErr EnumValues)
|
|
|
|
|
fetchAndValidateEnumValues pgSourceConfig tableName maybePrimaryKey columnInfos = runExceptT $
|
|
|
|
|
either (throw400 ConstraintViolation . showErrors) pure =<< runValidateT fetchAndValidate
|
|
|
|
|
where
|
|
|
|
|
fetchAndValidate
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: (MonadIO n, MonadBaseControl IO n, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] n)
|
|
|
|
|
=> n EnumValues
|
2021-02-14 09:07:52 +03:00
|
|
|
|
fetchAndValidate = do
|
|
|
|
|
maybePrimaryKeyColumn <- tolerate validatePrimaryKey
|
|
|
|
|
maybeCommentColumn <- validateColumns maybePrimaryKeyColumn
|
|
|
|
|
case maybePrimaryKeyColumn of
|
|
|
|
|
Nothing -> refute mempty
|
|
|
|
|
Just primaryKeyColumn -> do
|
|
|
|
|
result <- runPgSourceReadTx pgSourceConfig $ runValidateT $
|
|
|
|
|
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn
|
|
|
|
|
case result of
|
|
|
|
|
Left e -> (refute . pure . EnumTablePostgresError . qeError) e
|
|
|
|
|
Right (Left vErrs) -> refute vErrs
|
|
|
|
|
Right (Right r) -> pure r
|
|
|
|
|
where
|
|
|
|
|
validatePrimaryKey = case maybePrimaryKey of
|
|
|
|
|
Nothing -> refute [EnumTableMissingPrimaryKey]
|
|
|
|
|
Just primaryKey -> case _pkColumns primaryKey of
|
|
|
|
|
column NESeq.:<|| Seq.Empty -> case prciType column of
|
|
|
|
|
PGText -> pure column
|
|
|
|
|
_ -> refute [EnumTableNonTextualPrimaryKey column]
|
|
|
|
|
columns -> refute [EnumTableMultiColumnPrimaryKey $ map prciName (toList columns)]
|
|
|
|
|
|
|
|
|
|
validateColumns primaryKeyColumn = do
|
|
|
|
|
let nonPrimaryKeyColumns = maybe columnInfos (`delete` columnInfos) primaryKeyColumn
|
|
|
|
|
case nonPrimaryKeyColumns of
|
|
|
|
|
[] -> pure Nothing
|
|
|
|
|
[column] -> case prciType column of
|
|
|
|
|
PGText -> pure $ Just column
|
|
|
|
|
_ -> dispute [EnumTableNonTextualCommentColumn column] $> Nothing
|
|
|
|
|
columns -> dispute [EnumTableTooManyColumns $ map prciName columns] $> Nothing
|
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
showErrors :: [EnumTableIntegrityError ('Postgres pgKind)] -> Text
|
2021-02-14 09:07:52 +03:00
|
|
|
|
showErrors allErrors =
|
|
|
|
|
"the table " <> tableName <<> " cannot be used as an enum " <> reasonsMessage
|
|
|
|
|
where
|
|
|
|
|
reasonsMessage = makeReasonMessage allErrors showOne
|
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
showOne :: EnumTableIntegrityError ('Postgres pgKind) -> Text
|
2021-02-14 09:07:52 +03:00
|
|
|
|
showOne = \case
|
|
|
|
|
EnumTablePostgresError err -> "postgres error: " <> err
|
|
|
|
|
EnumTableMissingPrimaryKey -> "the table must have a primary key"
|
|
|
|
|
EnumTableMultiColumnPrimaryKey cols ->
|
|
|
|
|
"the table’s primary key must not span multiple columns ("
|
|
|
|
|
<> commaSeparated (sort cols) <> ")"
|
|
|
|
|
EnumTableNonTextualPrimaryKey colInfo -> typeMismatch "primary key" colInfo PGText
|
|
|
|
|
EnumTableNoEnumValues -> "the table must have at least one row"
|
|
|
|
|
EnumTableInvalidEnumValueNames values ->
|
|
|
|
|
let pluralString = " are not valid GraphQL enum value names"
|
|
|
|
|
valuesString = case NE.reverse (NE.sort values) of
|
|
|
|
|
value NE.:| [] -> "value " <> value <<> " is not a valid GraphQL enum value name"
|
|
|
|
|
value2 NE.:| [value1] -> "values " <> value1 <<> " and " <> value2 <<> pluralString
|
|
|
|
|
lastValue NE.:| otherValues ->
|
|
|
|
|
"values " <> commaSeparated (reverse otherValues) <> ", and "
|
|
|
|
|
<> lastValue <<> pluralString
|
|
|
|
|
in "the " <> valuesString
|
|
|
|
|
EnumTableNonTextualCommentColumn colInfo -> typeMismatch "comment column" colInfo PGText
|
|
|
|
|
EnumTableTooManyColumns cols ->
|
|
|
|
|
"the table must have exactly one primary key and optionally one comment column, not "
|
|
|
|
|
<> tshow (length cols) <> " columns ("
|
|
|
|
|
<> commaSeparated (sort cols) <> ")"
|
|
|
|
|
where
|
|
|
|
|
typeMismatch description colInfo expected =
|
|
|
|
|
"the table’s " <> description <> " (" <> prciName colInfo <<> ") must have type "
|
|
|
|
|
<> expected <<> ", not type " <>> prciType colInfo
|
|
|
|
|
|
|
|
|
|
fetchEnumValuesFromDb
|
2021-04-22 00:44:37 +03:00
|
|
|
|
:: forall pgKind m
|
|
|
|
|
. (MonadTx m, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] m)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
=> QualifiedTable
|
2021-04-22 00:44:37 +03:00
|
|
|
|
-> RawColumnInfo ('Postgres pgKind)
|
|
|
|
|
-> Maybe (RawColumnInfo ('Postgres pgKind))
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-> m EnumValues
|
|
|
|
|
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn = do
|
|
|
|
|
let nullExtr = Extractor SENull Nothing
|
|
|
|
|
commentExtr = maybe nullExtr (mkExtr . prciName) maybeCommentColumn
|
|
|
|
|
query = Q.fromBuilder $ toSQL mkSelect
|
|
|
|
|
{ selFrom = Just $ mkSimpleFromExp tableName
|
|
|
|
|
, selExtr = [mkExtr (prciName primaryKeyColumn), commentExtr] }
|
|
|
|
|
rawEnumValues <- liftTx $ Q.withQE defaultTxErrorHandler query () True
|
|
|
|
|
when (null rawEnumValues) $ dispute [EnumTableNoEnumValues]
|
|
|
|
|
let enumValues = flip map rawEnumValues $
|
|
|
|
|
\(enumValueText, comment) ->
|
|
|
|
|
case mkValidEnumValueName enumValueText of
|
|
|
|
|
Nothing -> Left enumValueText
|
|
|
|
|
Just enumValue -> Right (EnumValue enumValue, EnumValueInfo comment)
|
|
|
|
|
badNames = lefts enumValues
|
|
|
|
|
validEnums = rights enumValues
|
|
|
|
|
case NE.nonEmpty badNames of
|
|
|
|
|
Just someBadNames -> refute [EnumTableInvalidEnumValueNames someBadNames]
|
|
|
|
|
Nothing -> pure $ Map.fromList validEnums
|
|
|
|
|
where
|
|
|
|
|
-- https://graphql.github.io/graphql-spec/June2018/#EnumValue
|
|
|
|
|
mkValidEnumValueName name =
|
|
|
|
|
if name `elem` ["true", "false", "null"] then Nothing
|
|
|
|
|
else G.mkName name
|