[server] metadata API for native access

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7476
Co-authored-by: Tom Harding <6302310+i-am-tom@users.noreply.github.com>
GitOrigin-RevId: 781c29666e92004dc82918c2292fdacc27fded4c
This commit is contained in:
Daniel Harvey 2023-01-16 17:19:45 +00:00 committed by hasura-bot
parent 4175b53395
commit 06b284cf33
30 changed files with 602 additions and 16 deletions

View File

@ -25012,6 +25012,7 @@
]
}
],
"custom_sql": null,
"functions": [
{
"function": {
@ -26645,4 +26646,4 @@
}
}
}
}
}

View File

@ -12990,6 +12990,7 @@ sources:
_in: X-Hasura-Conference-Ids
- subconferenceId:
_in: X-Hasura-Subconference-Ids
custom_sql: null
functions:
- function:
schema: collection

View File

@ -706,6 +706,7 @@ library
, Hasura.Server.Telemetry.Types
, Hasura.Server.Telemetry.Counters
, Hasura.Server.Auth.JWT
, Hasura.CustomSQL
, Hasura.GC
, Hasura.Incremental.Internal.Cache
, Hasura.Incremental.Internal.Dependency
@ -793,6 +794,7 @@ library
, Hasura.RQL.DDL.Headers
, Hasura.RQL.DDL.Metadata
, Hasura.RQL.DDL.Metadata.Types
, Hasura.RQL.DDL.CustomSQL
, Hasura.RQL.DDL.OpenTelemetry
, Hasura.RQL.DDL.Permission
, Hasura.RQL.DDL.Permission.Internal

View File

@ -74,6 +74,7 @@ library
Test.API.Metadata.SuggestRelationshipsSpec
Test.API.Metadata.InconsistentSpec
Test.API.Metadata.TransparentDefaultsSpec
Test.API.Metadata.NativeAccessSpec
Test.API.Schema.RunSQLSpec
Test.Auth.Authorization.DisableRootFields.Common
Test.Auth.Authorization.DisableRootFields.DefaultRootFieldsSpec

View File

@ -0,0 +1,197 @@
{-# LANGUAGE QuasiQuotes #-}
-- | Access to the SQL
module Test.API.Metadata.NativeAccessSpec (spec) where
import Data.List.NonEmpty qualified as NE
import Harness.Backend.Postgres qualified as Postgres
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Yaml (yaml)
import Harness.Test.Fixture qualified as Fixture
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment)
import Harness.Yaml (shouldReturnYaml)
import Hasura.Prelude
import Test.Hspec (SpecWith, it)
-- ** Preamble
--
-- We currently don't need the table to exist in order to set up a custom SQL
-- stanza.
spec :: SpecWith GlobalTestEnvironment
spec =
Fixture.run
( NE.fromList
[ (Fixture.fixture $ Fixture.Backend Postgres.backendTypeMetadata)
{ Fixture.setupTeardown = \(testEnv, _) ->
[ Postgres.setupTablesAction [] testEnv
],
Fixture.customOptions =
Just $
Fixture.defaultOptions
{ Fixture.skipTests =
Just "Disabled until we can dynamically switch on Native Access with a command line option in NDAT-452"
}
}
]
)
tests
-- ** Setup and teardown
tests :: Fixture.Options -> SpecWith TestEnvironment
tests opts = do
let query :: Text
query = "SELECT thing / {{denominator}} AS divided FROM stuff WHERE date = {{target_date}}"
it "Adds a native access function and returns a 200" $ \testEnv -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_track_custom_sql
args:
type: query
source: postgres
root_field_name: divided_stuff
sql: *query
parameters:
- name: denominator
type: int
- name: target_date
type: date
returns: already_tracked_return_type
|]
)
[yaml|
message: success
|]
it "Checks for the native access function" $ \testEnv -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_track_custom_sql
args:
type: query
source: postgres
root_field_name: divided_stuff
sql: *query
parameters:
- name: denominator
type: int
- name: target_date
type: date
returns: already_tracked_return_type
|]
)
[yaml|
message: success
|]
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_get_custom_sql
args:
source: postgres
|]
)
[yaml|
divided_stuff:
type: query
root_field_name: divided_stuff
sql: *query
parameters:
- name: denominator
type: int
- name: target_date
type: date
returns:
name: already_tracked_return_type
schema: public
|]
it "Drops a native access function and returns a 200" $ \testEnv -> do
_ <-
GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_track_custom_sql
args:
type: query
source: postgres
root_field_name: divided_stuff
sql: *query
parameters:
- name: denominator
type: int
- name: target_date
type: date
returns: already_tracked_return_type
|]
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_untrack_custom_sql
args:
source: postgres
root_field_name: divided_stuff
|]
)
[yaml|
message: success
|]
it "Checks the native access function can be deleted" $ \testEnv -> do
_ <-
GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_track_custom_sql
args:
type: query
source: postgres
root_field_name: divided_stuff
sql: *query
parameters:
- name: denominator
type: int
- name: target_date
type: date
returns: already_tracked_return_type
|]
_ <-
GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_untrack_custom_sql
args:
root_field_name: divided_stuff
source: postgres
|]
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnv
[yaml|
type: pg_get_custom_sql
args:
source: postgres
|]
)
[yaml|
{}
|]

View File

@ -58,6 +58,7 @@ library
build-depends:
, aeson >=1.5
, attoparsec >=0.14
, autodocodec
, base >=4.7
, bytestring >=0.10
, deepseq >=1.4

View File

@ -24,6 +24,7 @@ where
-------------------------------------------------------------------------------
import Autodocodec (HasCodec (codec), bimapCodec)
import Control.DeepSeq (NFData)
import Data.Aeson qualified as J
import Data.Char qualified as C
@ -31,6 +32,7 @@ import Data.Coerce (coerce)
import Data.Hashable (Hashable)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
import Instances.TH.Lift ()
import Language.Haskell.TH.Syntax (Lift)
import Language.Haskell.TH.Syntax.Compat (SpliceQ, examineSplice, liftSplice)
@ -44,6 +46,9 @@ newtype Name = Name {unName :: Text}
deriving stock (Eq, Lift, Ord, Show)
deriving newtype (Semigroup, Hashable, NFData, Pretty, J.ToJSONKey, J.ToJSON)
instance HasCodec Name where
codec = bimapCodec (J.eitherDecodeStrict . encodeUtf8) unName codec
-- | @NameSuffix@ is essentially a GQL identifier that can be used as Suffix
-- It is slightely different from @Name@ as it relaxes the criteria that a
-- @Name@ cannot start with a digit.

View File

@ -860,7 +860,7 @@ mkHGEServer setupHook env ServeOptions {..} serverCtx@ServerCtx {..} ekgStore =
-- event triggers should be tied to the life cycle of a source
lockedEvents <- readTVarIO leEvents
forM_ sources $ \backendSourceInfo -> do
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo sourceName _ _ sourceConfig _ _ :: SourceInfo b) -> do
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo sourceName _ _ _ sourceConfig _ _ :: SourceInfo b) -> do
let sourceNameText = sourceNameToText sourceName
logger $ mkGenericLog LevelInfo "event_triggers" $ "unlocking events of source: " <> sourceNameText
for_ (HM.lookup sourceName lockedEvents) $ \sourceLockedEvents -> do

View File

@ -74,7 +74,7 @@ runSQL ::
MSSQLRunSQL ->
m EncJSON
runSQL mssqlRunSQL@MSSQLRunSQL {..} = do
SourceInfo _ tableCache _ sourceConfig _ _ <- askSourceInfo @'MSSQL _mrsSource
SourceInfo _ tableCache _ _ sourceConfig _ _ <- askSourceInfo @'MSSQL _mrsSource
results <-
-- If the SQL modifies the schema of the database then check for any metadata changes
if isSchemaCacheBuildRequiredRunSQL mssqlRunSQL

View File

@ -256,7 +256,7 @@ withMetadataCheck ::
PG.TxET QErr m a ->
m a
withMetadataCheck source cascade txAccess runSQLQuery = do
SourceInfo _ tableCache functionCache sourceConfig _ _ <- askSourceInfo @('Postgres pgKind) source
SourceInfo _ tableCache functionCache _customSQL sourceConfig _ _ <- askSourceInfo @('Postgres pgKind) source
-- Run SQL query and metadata checker in a transaction
(queryResult, metadataUpdater) <- runTxWithMetadataCheck source sourceConfig txAccess tableCache functionCache cascade runSQLQuery

View File

@ -176,6 +176,7 @@ resolveDatabaseMetadata sourceMetadata sourceConfig sourceCustomization = runExc
name <- afold @(Either QErr) $ mkScalarTypeName scalar
pure (name, scalar)
pure (tablesMeta, functionsMeta, scalarsMap)
pure $ ResolvedSource sourceConfig sourceCustomization tablesMeta functionsMeta (ScalarMap pgScalars)
where
-- A helper function to list all functions underpinning computed fields from a table metadata

View File

@ -23,6 +23,7 @@ instance BackendAPI ('Postgres 'Vanilla) where
remoteRelationshipCommands @('Postgres 'Vanilla),
eventTriggerCommands @('Postgres 'Vanilla),
computedFieldCommands @('Postgres 'Vanilla),
nativeAccessCommands @('Postgres 'Vanilla),
[ commandParser
"set_table_is_enum"
( RMPgSetTableIsEnum

View File

@ -0,0 +1,52 @@
{-# LANGUAGE UndecidableInstances #-}
-- | Types concerned with user-specified custom SQL fragments.
module Hasura.CustomSQL
( CustomSQLParameter (..),
)
where
import Autodocodec (HasCodec (codec))
import Autodocodec qualified as AC
import Data.Aeson
import Hasura.Prelude
----------------------------------
newtype CustomSQLParameterName = CustomSQLParameterName {cspnName :: Text}
deriving newtype (Show, Eq, FromJSON, ToJSON)
instance HasCodec CustomSQLParameterName where
codec = AC.dimapCodec CustomSQLParameterName cspnName codec
newtype CustomSQLParameterType = CustomSQLParameterType {cspnType :: Text}
deriving newtype (Show, Eq, FromJSON, ToJSON)
instance HasCodec CustomSQLParameterType where
codec = AC.dimapCodec CustomSQLParameterType cspnType codec
data CustomSQLParameter = CustomSQLParameter
{ cspName :: CustomSQLParameterName,
cspType :: CustomSQLParameterType
}
deriving (Show, Eq)
instance FromJSON CustomSQLParameter where
parseJSON = withObject "CustomSQLParameter" $ \o -> do
cspName <- o .: "name"
cspType <- o .: "type"
pure CustomSQLParameter {..}
instance ToJSON CustomSQLParameter where
toJSON CustomSQLParameter {..} =
object
[ "name" .= cspName,
"type" .= cspType
]
instance HasCodec CustomSQLParameter where
codec =
AC.object "CustomSQLParameter" $
CustomSQLParameter
<$> AC.requiredField' "name" AC..= cspName
<*> AC.requiredField' "type" AC..= cspType

View File

@ -248,7 +248,7 @@ processEventQueue logger httpMgr getSchemaCache EventEngineCtx {..} LockedEvents
liftIO . fmap concat $
-- fetch pending events across all the sources asynchronously
LA.forConcurrently (M.toList allSources) \(sourceName, sourceCache) ->
AB.dispatchAnyBackend @BackendEventTrigger sourceCache \(SourceInfo _sourceName tableCache _functionCache sourceConfig _queryTagsConfig _sourceCustomization :: SourceInfo b) -> do
AB.dispatchAnyBackend @BackendEventTrigger sourceCache \(SourceInfo _sourceName tableCache _functionCache _customSQLCache sourceConfig _queryTagsConfig _sourceCustomization :: SourceInfo b) -> do
let tables = M.elems tableCache
triggerMap = _tiEventTriggerInfoMap <$> tables
eventTriggerCount = sum (M.size <$> triggerMap)

View File

@ -328,7 +328,7 @@ buildRoleContext options sources remotes actions customTypes role remoteSchemaPe
[FieldParser P.Parse (NamespacedField (QueryRootField UnpreparedValue))],
[(G.Name, Parser 'Output P.Parse (ApolloFederationParserFunction P.Parse))]
)
buildSource schemaContext schemaOptions sourceInfo@(SourceInfo _ tables functions _ _ sourceCustomization) =
buildSource schemaContext schemaOptions sourceInfo@(SourceInfo _ tables functions _customSQL _ _ sourceCustomization) =
runSourceSchema schemaContext schemaOptions sourceInfo do
let validFunctions = takeValidFunctions functions
validTables = takeValidTables tables
@ -453,7 +453,7 @@ buildRelayRoleContext options sources actions customTypes role expFeatures = do
[FieldParser P.Parse (NamespacedField (MutationRootField UnpreparedValue))],
[FieldParser P.Parse (NamespacedField (QueryRootField UnpreparedValue))]
)
buildSource schemaContext schemaOptions sourceInfo@(SourceInfo _ tables functions _ _ sourceCustomization) = do
buildSource schemaContext schemaOptions sourceInfo@(SourceInfo _ tables functions _customSQL _ _ sourceCustomization) = do
runSourceSchema schemaContext schemaOptions sourceInfo do
let validFunctions = takeValidFunctions functions
validTables = takeValidTables tables

View File

@ -0,0 +1,211 @@
{-# LANGUAGE UndecidableInstances #-}
-- | Metadata V1 commands (and their types) for handling user-specified custom
-- SQL fragments.
module Hasura.RQL.DDL.CustomSQL
( GetCustomSQL (..),
TrackCustomSQL (..),
UntrackCustomSQL (..),
runGetCustomSQL,
runTrackCustomSQL,
runUntrackCustomSQL,
dropCustomSQLInMetadata,
)
where
import Control.Lens ((^?))
import Data.Aeson
import Data.HashMap.Strict.InsOrd qualified as OMap
import Hasura.Base.Error
import Hasura.CustomSQL
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.Types.Backend (Backend, TableName)
import Hasura.RQL.Types.Common (SourceName, successMsg)
import Hasura.RQL.Types.Metadata
import Hasura.RQL.Types.Metadata.Backend
import Hasura.RQL.Types.Metadata.Object
import Hasura.RQL.Types.SchemaCache.Build
import Hasura.SQL.AnyBackend qualified as AB
import Hasura.SQL.Backend
import Hasura.Server.Types (HasServerConfigCtx (..), ServerConfigCtx (..))
import Language.GraphQL.Draft.Syntax qualified as G
---------------------------------
data GetCustomSQL (b :: BackendType) = GetCustomSQL
{ gcsSource :: SourceName
}
deriving instance Backend b => Show (GetCustomSQL b)
deriving instance Backend b => Eq (GetCustomSQL b)
instance Backend b => FromJSON (GetCustomSQL b) where
parseJSON = withObject "GetCustomSQL" $ \o -> do
gcsSource <- o .: "source"
pure GetCustomSQL {..}
instance Backend b => ToJSON (GetCustomSQL b) where
toJSON GetCustomSQL {..} =
object
[ "source" .= gcsSource
]
runGetCustomSQL ::
forall b m.
( BackendMetadata b,
MetadataM m,
HasServerConfigCtx m,
MonadIO m,
MonadError QErr m
) =>
GetCustomSQL b ->
m EncJSON
runGetCustomSQL q = do
throwIfFeatureDisabled
metadata <- getMetadata
let customSQL :: Maybe (CustomSQLFields b)
customSQL = metadata ^? metaSources . ix (gcsSource q) . toSourceMetadata . smCustomSQL
pure (encJFromJValue customSQL)
----------------------------------
data TrackCustomSQL (b :: BackendType) = TrackCustomSQL
{ tcsSource :: SourceName,
tcsType :: Text,
tcsRootFieldName :: G.Name,
tcsSql :: Text,
tcsParameters :: NonEmpty CustomSQLParameter,
tcsReturns :: TableName b
}
deriving instance Backend b => Show (TrackCustomSQL b)
deriving instance Backend b => Eq (TrackCustomSQL b)
instance Backend b => FromJSON (TrackCustomSQL b) where
parseJSON = withObject "TrackCustomSQL" $ \o -> do
tcsSource <- o .: "source"
tcsType <- o .: "type"
tcsRootFieldName <- o .: "root_field_name"
tcsSql <- o .: "sql"
tcsParameters <- o .: "parameters"
tcsReturns <- o .: "returns"
pure TrackCustomSQL {..}
instance Backend b => ToJSON (TrackCustomSQL b) where
toJSON TrackCustomSQL {..} =
object
[ "source" .= tcsSource,
"type" .= tcsType,
"root_field_name" .= tcsRootFieldName,
"sql" .= tcsSql,
"parameters" .= tcsParameters,
"returns" .= tcsReturns
]
runTrackCustomSQL ::
forall b m.
( BackendMetadata b,
CacheRWM m,
MetadataM m,
MonadError QErr m,
HasServerConfigCtx m,
MonadIO m
) =>
TrackCustomSQL b ->
m EncJSON
runTrackCustomSQL q = do
throwIfFeatureDisabled
let metadataObj =
MOSourceObjId source $
AB.mkAnyBackend $
SMOCustomSQL @b fieldName
metadata =
CustomSQLMetadata
{ _csmType = tcsType q,
_csmRootFieldName = tcsRootFieldName q,
_csmSql = tcsSql q,
_csmParameters = tcsParameters q,
_csmReturns = tcsReturns q
}
buildSchemaCacheFor metadataObj $
MetadataModifier $
(metaSources . ix source . toSourceMetadata @b . smCustomSQL)
%~ OMap.insert fieldName metadata
pure successMsg
where
source = tcsSource q
fieldName = tcsRootFieldName q
---------------------------------
data UntrackCustomSQL (b :: BackendType) = UntrackCustomSQL
{ utcsSource :: SourceName,
utcsRootFieldName :: G.Name
}
deriving instance Backend b => Show (UntrackCustomSQL b)
deriving instance Backend b => Eq (UntrackCustomSQL b)
instance Backend b => FromJSON (UntrackCustomSQL b) where
parseJSON = withObject "UntrackCustomSQL" $ \o -> do
utcsSource <- o .: "source"
utcsRootFieldName <- o .: "root_field_name"
pure UntrackCustomSQL {..}
instance Backend b => ToJSON (UntrackCustomSQL b) where
toJSON UntrackCustomSQL {..} =
object
[ "source" .= utcsSource,
"root_field_name" .= utcsRootFieldName
]
runUntrackCustomSQL ::
forall b m.
( BackendMetadata b,
MonadError QErr m,
CacheRWM m,
MetadataM m,
HasServerConfigCtx m,
MonadIO m
) =>
UntrackCustomSQL b ->
m EncJSON
runUntrackCustomSQL q = do
throwIfFeatureDisabled
let metadataObj =
MOSourceObjId source $
AB.mkAnyBackend $
SMOCustomSQL @b fieldName
buildSchemaCacheFor metadataObj $
dropCustomSQLInMetadata @b source (fieldName :: G.Name)
pure successMsg
where
source = utcsSource q
fieldName = utcsRootFieldName q
dropCustomSQLInMetadata :: forall b. BackendMetadata b => SourceName -> G.Name -> MetadataModifier
dropCustomSQLInMetadata source rootFieldName =
MetadataModifier $
metaSources . ix source . toSourceMetadata @b . smCustomSQL %~ OMap.delete rootFieldName
-- | check feature flag is enabled before carrying out any actions
throwIfFeatureDisabled :: (HasServerConfigCtx m, MonadIO m, MonadError QErr m) => m ()
throwIfFeatureDisabled = do
configCtx <- askServerConfigCtx
enableCustomSQL <- liftIO (_sccUsePQNP configCtx)
unless enableCustomSQL (throw500 "CustomSQL is disabled!")

View File

@ -605,7 +605,7 @@ toggleEventTriggerCleanupAction conf cleanupSwitch = do
case tlcs of
TriggerAllSource -> do
ifor_ (scSources schemaCache) $ \sourceName backendSourceInfo -> do
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo _ tableCache _ _ _ _ :: SourceInfo b) -> do
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo _ tableCache _ _customSQLCache _ _ _ :: SourceInfo b) -> do
traverseTableHelper tableCache cleanupSwitch sourceName
TriggerSource sourceNameLst -> do
forM_ sourceNameLst $ \sourceName -> do
@ -613,7 +613,7 @@ toggleEventTriggerCleanupAction conf cleanupSwitch = do
HM.lookup sourceName (scSources schemaCache)
`onNothing` throw400 NotExists ("source with name " <> sourceNameToText sourceName <> " does not exists")
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo _ tableCache _ _ _ _ :: SourceInfo b) -> do
AB.dispatchAnyBackend @BackendEventTrigger backendSourceInfo \(SourceInfo _ tableCache _ _customSQLCache _ _ _ :: SourceInfo b) -> do
traverseTableHelper tableCache cleanupSwitch sourceName
TriggerQualifier qualifierLst -> do
forM_ qualifierLst $ \qualifier -> do

View File

@ -46,6 +46,7 @@ import Hasura.Metadata.Class
import Hasura.Prelude hiding (first)
import Hasura.RQL.DDL.Action
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.CustomSQL (dropCustomSQLInMetadata)
import Hasura.RQL.DDL.CustomTypes
import Hasura.RQL.DDL.Endpoint
import Hasura.RQL.DDL.EventTrigger
@ -142,6 +143,7 @@ runClearMetadata _ = do
(_smKind @b s)
mempty
mempty
mempty
(_smConfiguration @b s)
Nothing
emptySourceCustomization
@ -514,7 +516,7 @@ runReplaceMetadataV2 ReplaceMetadataV2 {..} = do
<<> "' as it is inconsistent"
)
J.Null
Just sourceInfo@(SourceInfo _ _ _ sourceConfig _ _) -> do
Just sourceInfo@(SourceInfo _ _ _ _ sourceConfig _ _) -> do
let getEventMapWithCC sourceMeta = Map.fromList $ concatMap (getAllETWithCleanupConfigInTableMetadata . snd) $ OMap.toList $ _smTables sourceMeta
oldEventTriggersWithCC = getEventMapWithCC oldSourceMetadata
newEventTriggersWithCC = getEventMapWithCC newSourceMetadata
@ -689,6 +691,7 @@ purgeMetadataObj = \case
SMOTable qt -> dropTableInMetadata @b source qt
SMOFunction qf -> dropFunctionInMetadata @b source qf
SMOFunctionPermission qf rn -> dropFunctionPermissionInMetadata @b source qf rn
SMOCustomSQL qc -> dropCustomSQLInMetadata @b source qc
SMOTableObj qt tableObj ->
MetadataModifier $
tableMetadataSetter @b source qt %~ case tableObj of

View File

@ -608,7 +608,7 @@ buildSchemaCacheRule logger env = proc (metadataNoDefaults, invalidationKeys) ->
)
`arr` (SourceInfo b)
buildSource = proc (allSources, sourceMetadata, sourceConfig, tablesRawInfo, eventTriggerInfoMaps, _dbTables, dbFunctions, remoteSchemaMap, orderedRoles) -> do
let SourceMetadata sourceName _backendKind tables functions _ queryTagsConfig sourceCustomization _healthCheckConfig = sourceMetadata
let SourceMetadata sourceName _backendKind tables functions customSQL _ queryTagsConfig sourceCustomization _healthCheckConfig = sourceMetadata
tablesMetadata = OMap.elems tables
(_, nonColumnInputs, permissions) = unzip3 $ map mkTableInputs tablesMetadata
alignTableMap :: HashMap (TableName b) a -> HashMap (TableName b) c -> HashMap (TableName b) (a, c)
@ -687,9 +687,10 @@ buildSchemaCacheRule logger env = proc (metadataNoDefaults, invalidationKeys) ->
(functionInfo, dep) <- buildFunctionInfo sourceName qf systemDefined config permissionsMap rawfunctionInfo comment namingConv
recordDependenciesM metadataObject schemaObject (Seq.singleton dep)
pure functionInfo
let functionCache = mapFromL _fiSQLName $ catMaybes functionCacheMaybes
returnA -< SourceInfo sourceName tableCache functionCache sourceConfig queryTagsConfig resolvedCustomization
returnA -< SourceInfo sourceName tableCache functionCache customSQL sourceConfig queryTagsConfig resolvedCustomization
buildAndCollectInfo ::
forall arr m.
@ -719,7 +720,7 @@ buildSchemaCacheRule logger env = proc (metadataNoDefaults, invalidationKeys) ->
HS.fromList $
concat $
OMap.elems sources >>= \(BackendSourceMetadata e) ->
AB.dispatchAnyBackend @Backend e \(SourceMetadata _ _ tables _functions _ _ _ _) -> do
AB.dispatchAnyBackend @Backend e \(SourceMetadata _ _ tables _functions _customSQL _ _ _ _) -> do
table <- OMap.elems tables
pure $
OMap.keys (_tmInsertPermissions table)

View File

@ -273,6 +273,7 @@ deleteMetadataObject = \case
SMOFunction name -> siFunctions %~ M.delete name
SMOFunctionPermission functionName role ->
siFunctions . ix functionName . fiPermissions %~ M.delete role
SMOCustomSQL name -> siCustomSQL %~ OMap.delete name
SMOTableObj tableName tableObjectId ->
siTables . ix tableName %~ case tableObjectId of
MTORel name _ -> tiCoreInfo . tciFieldInfoMap %~ M.delete (fromRel name)

View File

@ -14,6 +14,8 @@ module Hasura.RQL.Types.Metadata.Common
ComputedFieldMetadata (..),
ComputedFields,
CronTriggers,
CustomSQLFields,
CustomSQLMetadata (..),
Endpoints,
EventTriggers,
FunctionMetadata (..),
@ -46,6 +48,7 @@ module Hasura.RQL.Types.Metadata.Common
smQueryTags,
smTables,
smCustomization,
smCustomSQL,
smHealthCheckConfig,
sourcesCodec,
tmArrayRelationships,
@ -61,6 +64,11 @@ module Hasura.RQL.Types.Metadata.Common
tmSelectPermissions,
tmTable,
tmUpdatePermissions,
csmParameters,
csmReturns,
csmRootFieldName,
csmSql,
csmType,
toSourceMetadata,
)
where
@ -79,6 +87,7 @@ import Data.List.Extended qualified as L
import Data.Maybe (fromJust)
import Data.Text qualified as T
import Data.Text.Extended qualified as T
import Hasura.CustomSQL (CustomSQLParameter)
import Hasura.Metadata.DTO.Placeholder (placeholderCodecViaJSON)
import Hasura.Metadata.DTO.Utils (codecNamePrefix)
import Hasura.Prelude
@ -108,6 +117,7 @@ import Hasura.SQL.AnyBackend qualified as AB
import Hasura.SQL.Backend
import Hasura.SQL.Tag (BackendTag, HasTag (backendTag))
import Hasura.Session
import Language.GraphQL.Draft.Syntax qualified as G
-- | Parse a list of objects into a map from a derived key,
-- failing if the list has duplicates.
@ -359,6 +369,59 @@ instance (Backend b) => HasCodec (FunctionMetadata b) where
nameDoc = "Name of the SQL function"
configDoc = "Configuration for the SQL function"
-- | This is everything we are passed from the metadata call currently,
-- there is probably some sort of checking/refinement that needs to happen
-- before we store it, but for now, YOLO.
data CustomSQLMetadata b = CustomSQLMetadata
{ _csmType :: Text,
_csmRootFieldName :: G.Name,
_csmSql :: Text,
_csmParameters :: NonEmpty CustomSQLParameter,
_csmReturns :: TableName b
}
deriving (Generic)
deriving instance (Backend b) => Show (CustomSQLMetadata b)
deriving instance (Backend b) => Eq (CustomSQLMetadata b)
instance (Backend b) => ToJSON (CustomSQLMetadata b) where
toJSON = genericToJSON hasuraJSON
$(makeLenses ''CustomSQLMetadata)
instance (Backend b) => FromJSON (CustomSQLMetadata b) where
parseJSON = withObject "CustomSQLMetadata" $ \o ->
CustomSQLMetadata
<$> o .: "type"
<*> o .: "root_field_name"
<*> o .: "sql"
<*> o .: "parameters"
<*> o .: "returns"
instance (Backend b) => HasCodec (CustomSQLMetadata b) where
codec =
CommentCodec
( T.unlines
[ "A custom SQL function to add to the GraphQL schema with configuration.",
"",
"https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/custom-functions.html#args-syntax"
]
)
$ AC.object (codecNamePrefix @b <> "CustomSQLMetadata")
$ CustomSQLMetadata
<$> requiredField "type" typeDoc AC..= _csmType
<*> requiredField "root_field_name" fieldNameDoc AC..= _csmRootFieldName
<*> requiredField "sql" sqlDoc AC..= _csmSql
<*> requiredField "parameters" paramDoc AC..= _csmParameters
<*> requiredField "returns" returnsDoc AC..= _csmReturns
where
typeDoc = "Type of SQL statement to run"
fieldNameDoc = "Field name for custom SQL"
sqlDoc = "SQL to run"
paramDoc = "Function parameters and their types"
returnsDoc = "Return type of function"
type RemoteSchemaMetadata = RemoteSchemaMetadataG RemoteRelationshipDefinition
type RemoteSchemas = InsOrdHashMap RemoteSchemaName RemoteSchemaMetadata
@ -367,6 +430,8 @@ type Tables b = InsOrdHashMap (TableName b) (TableMetadata b)
type Functions b = InsOrdHashMap (FunctionName b) (FunctionMetadata b)
type CustomSQLFields b = InsOrdHashMap G.Name (CustomSQLMetadata b)
type Endpoints = InsOrdHashMap EndpointName CreateEndpoint
type Actions = InsOrdHashMap ActionName ActionMetadata
@ -381,6 +446,7 @@ data SourceMetadata b = SourceMetadata
_smKind :: BackendSourceKind b,
_smTables :: Tables b,
_smFunctions :: Functions b,
_smCustomSQL :: CustomSQLFields b,
_smConfiguration :: SourceConnConfiguration b,
_smQueryTags :: Maybe QueryTagsConfig,
_smCustomization :: SourceCustomization,
@ -399,6 +465,7 @@ instance (Backend b) => FromJSONWithContext (BackendSourceKind b) (SourceMetadat
_smName <- o .: "name"
_smTables <- oMapFromL _tmTable <$> o .: "tables"
_smFunctions <- oMapFromL _fmFunction <$> o .:? "functions" .!= []
_smCustomSQL <- oMapFromL _csmRootFieldName <$> o .:? "custom_sql" .!= []
_smConfiguration <- o .: "configuration"
_smQueryTags <- o .:? "query_tags"
_smCustomization <- o .:? "customization" .!= emptySourceCustomization
@ -453,6 +520,7 @@ instance Backend b => HasCodec (SourceMetadata b) where
<*> requiredField' "kind" .== _smKind
<*> requiredFieldWith' "tables" (sortedElemsCodec _tmTable) .== _smTables
<*> optionalFieldOrNullWithOmittedDefaultWith' "functions" (sortedElemsCodec _fmFunction) mempty .== _smFunctions
<*> optionalFieldOrNullWithOmittedDefaultWith' "custom_sql" (sortedElemsCodec _csmRootFieldName) mempty .== _smCustomSQL
<*> requiredField' "configuration" .== _smConfiguration
<*> optionalFieldOrNull' "query_tags" .== _smQueryTags
<*> optionalFieldWithOmittedDefault' "customization" emptySourceCustomization .== _smCustomization
@ -485,6 +553,7 @@ mkSourceMetadata name backendSourceKind config customization healthCheckConfig =
backendSourceKind
mempty
mempty
mempty
config
Nothing
customization

View File

@ -74,6 +74,7 @@ data SourceMetadataObjId b
| SMOFunction (FunctionName b)
| SMOFunctionPermission (FunctionName b) RoleName
| SMOTableObj (TableName b) TableMetadataObjId
| SMOCustomSQL G.Name
deriving (Generic)
deriving instance (Backend b) => Show (SourceMetadataObjId b)
@ -134,6 +135,7 @@ moiTypeName = \case
handleSourceObj = \case
SMOTable _ -> "table"
SMOFunction _ -> "function"
SMOCustomSQL _ -> "custom_sql"
SMOFunctionPermission _ _ -> "function_permission"
SMOTableObj _ tableObjectId -> case tableObjectId of
MTORel _ relType -> relTypeToTxt relType <> "_relation"
@ -185,6 +187,7 @@ moiName objectId =
<> toTxt functionName
<> " in source "
<> toTxt source
SMOCustomSQL name -> toTxt name <> " in source " <> toTxt source
SMOTableObj tableName tableObjectId ->
let tableObjectName = case tableObjectId of
MTORel name _ -> toTxt name

View File

@ -60,6 +60,7 @@ import Hasura.RQL.Types.Metadata.Common
BackendSourceMetadata (..),
ComputedFieldMetadata (..),
CronTriggers,
CustomSQLMetadata (..),
Endpoints,
FunctionMetadata (..),
InheritedRoles,
@ -114,11 +115,12 @@ sourcesToOrdJSONList sources =
where
sourceMetaToOrdJSON :: BackendSourceMetadata -> AO.Value
sourceMetaToOrdJSON (BackendSourceMetadata exists) =
AB.dispatchAnyBackend @Backend exists $ \(SourceMetadata {..} :: SourceMetadata b) ->
AB.dispatchAnyBackend @Backend exists $ \(SourceMetadata _smName _smKind _smTables _smFunctions _smCustomSQL _smConfiguration _smQueryTags _smCustomization _smHealthCheckConfig :: SourceMetadata b) ->
let sourceNamePair = ("name", AO.toOrdered _smName)
sourceKindPair = ("kind", AO.toOrdered _smKind)
tablesPair = ("tables", AO.array $ map tableMetaToOrdJSON $ sortOn _tmTable $ OM.elems _smTables)
functionsPair = listToMaybeOrdPairSort "functions" functionMetadataToOrdJSON _fmFunction _smFunctions
customSQLPair = listToMaybeOrdPairSort "custom_sql" customSQLMetaToOrdJSON _csmRootFieldName _smCustomSQL
configurationPair = [("configuration", AO.toOrdered _smConfiguration)]
queryTagsConfigPair = maybe [] (\queryTagsConfig -> [("query_tags", AO.toOrdered queryTagsConfig)]) _smQueryTags
@ -129,11 +131,15 @@ sourcesToOrdJSONList sources =
in AO.object $
[sourceNamePair, sourceKindPair, tablesPair]
<> maybeToList functionsPair
<> maybeToList customSQLPair
<> configurationPair
<> queryTagsConfigPair
<> customizationPair
<> healthCheckPair
customSQLMetaToOrdJSON :: (Backend b) => CustomSQLMetadata b -> AO.Value
customSQLMetaToOrdJSON = AO.toOrdered . toJSON
tableMetaToOrdJSON :: (Backend b) => TableMetadata b -> AO.Value
tableMetaToOrdJSON
( TableMetadata

View File

@ -12,6 +12,7 @@ module Hasura.RQL.Types.Source
unsafeSourceName,
unsafeSourceTables,
siConfiguration,
siCustomSQL,
siFunctions,
siName,
siQueryTagsConfig,
@ -50,6 +51,7 @@ import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Function
import Hasura.RQL.Types.HealthCheck
import Hasura.RQL.Types.Instances ()
import Hasura.RQL.Types.Metadata.Common (CustomSQLFields)
import Hasura.RQL.Types.QueryTags
import Hasura.RQL.Types.SourceCustomization
import Hasura.RQL.Types.Table
@ -66,6 +68,7 @@ data SourceInfo b = SourceInfo
{ _siName :: SourceName,
_siTables :: TableCache b,
_siFunctions :: FunctionCache b,
_siCustomSQL :: CustomSQLFields b,
_siConfiguration :: ~(SourceConfig b),
_siQueryTagsConfig :: Maybe QueryTagsConfig,
_siCustomization :: ResolvedSourceCustomization
@ -108,7 +111,7 @@ unsafeSourceInfo = AB.unpackAnyBackend
unsafeSourceName :: BackendSourceInfo -> SourceName
unsafeSourceName bsi = AB.dispatchAnyBackend @Backend bsi go
where
go (SourceInfo name _ _ _ _ _) = name
go (SourceInfo name _ _ _ _ _ _) = name
unsafeSourceTables :: forall b. HasTag b => BackendSourceInfo -> Maybe (TableCache b)
unsafeSourceTables = fmap _siTables . unsafeSourceInfo @b

View File

@ -21,6 +21,7 @@ module Hasura.Server.API.Backend
tableCommands,
tablePermissionsCommands,
computedFieldCommands,
nativeAccessCommands,
)
where
@ -159,3 +160,10 @@ computedFieldCommands =
[ commandParser "add_computed_field" $ RMAddComputedField . mkAnyBackend @b,
commandParser "drop_computed_field" $ RMDropComputedField . mkAnyBackend @b
]
nativeAccessCommands :: forall (b :: BackendType). Backend b => [CommandParser b]
nativeAccessCommands =
[ commandParser "get_custom_sql" $ RMGetCustomSQL . mkAnyBackend @b,
commandParser "track_custom_sql" $ RMTrackCustomSQL . mkAnyBackend @b,
commandParser "untrack_custom_sql" $ RMUntrackCustomSQL . mkAnyBackend @b
]

View File

@ -26,6 +26,7 @@ import Hasura.Prelude hiding (first)
import Hasura.RQL.DDL.Action
import Hasura.RQL.DDL.ApiLimit
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.CustomSQL qualified as CustomSQL
import Hasura.RQL.DDL.CustomTypes
import Hasura.RQL.DDL.DataConnector
import Hasura.RQL.DDL.Endpoint
@ -127,6 +128,10 @@ data RQLMetadataV1
| -- Computed fields
RMAddComputedField !(AnyBackend AddComputedField)
| RMDropComputedField !(AnyBackend DropComputedField)
| -- Native access
RMGetCustomSQL !(AnyBackend CustomSQL.GetCustomSQL)
| RMTrackCustomSQL !(AnyBackend CustomSQL.TrackCustomSQL)
| RMUntrackCustomSQL !(AnyBackend CustomSQL.UntrackCustomSQL)
| -- Tables event triggers
RMCreateEventTrigger !(AnyBackend (Unvalidated1 CreateEventTriggerQuery))
| RMDeleteEventTrigger !(AnyBackend DeleteEventTriggerQuery)
@ -478,6 +483,9 @@ queryModifiesMetadata = \case
RMGetSourceTables _ -> False
RMGetTableInfo _ -> False
RMSuggestRelationships _ -> False
RMGetCustomSQL _ -> False
RMTrackCustomSQL _ -> True
RMUntrackCustomSQL _ -> True
RMBulk qs -> any queryModifiesMetadata qs
-- We used to assume that the fallthrough was True,
-- but it is better to be explicit here to warn when new constructors are added.
@ -656,6 +664,9 @@ runMetadataQueryV1M env currentResourceVersion = \case
RMDropFunctionPermission q -> dispatchMetadata runDropFunctionPermission q
RMAddComputedField q -> dispatchMetadata runAddComputedField q
RMDropComputedField q -> dispatchMetadata runDropComputedField q
RMGetCustomSQL q -> dispatchMetadata CustomSQL.runGetCustomSQL q
RMTrackCustomSQL q -> dispatchMetadata CustomSQL.runTrackCustomSQL q
RMUntrackCustomSQL q -> dispatchMetadata CustomSQL.runUntrackCustomSQL q
RMCreateEventTrigger q ->
dispatchMetadataAndEventTrigger
( validateTransforms

View File

@ -5,6 +5,7 @@ where
import Hasura.RQL.DDL.Action
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.CustomSQL qualified as CustomSQL
import Hasura.RQL.DDL.DataConnector
import Hasura.RQL.DDL.EventTrigger
import Hasura.RQL.DDL.Metadata
@ -82,6 +83,10 @@ data RQLMetadataV1
| -- Computed fields
RMAddComputedField !(AnyBackend AddComputedField)
| RMDropComputedField !(AnyBackend DropComputedField)
| -- Native access
RMGetCustomSQL !(AnyBackend CustomSQL.GetCustomSQL)
| RMTrackCustomSQL !(AnyBackend CustomSQL.TrackCustomSQL)
| RMUntrackCustomSQL !(AnyBackend CustomSQL.UntrackCustomSQL)
| -- Tables event triggers
RMCreateEventTrigger !(AnyBackend (Unvalidated1 CreateEventTriggerQuery))
| RMDeleteEventTrigger !(AnyBackend DeleteEventTriggerQuery)

View File

@ -161,6 +161,7 @@ migrateCatalog maybeDefaultSourceConfig extensionsSchema maintenanceMode migrati
PostgresVanillaKind
mempty
mempty
mempty
defaultSourceConfig
Nothing
emptySourceCustomization
@ -334,7 +335,7 @@ migrations maybeDefaultSourceConfig dryRun maintenanceMode =
defaultSourceMetadata =
BackendSourceMetadata $
AB.mkAnyBackend $
SourceMetadata defaultSource PostgresVanillaKind _mnsTables _mnsFunctions defaultSourceConfig Nothing emptySourceCustomization Nothing
SourceMetadata defaultSource PostgresVanillaKind _mnsTables _mnsFunctions mempty defaultSourceConfig Nothing emptySourceCustomization Nothing
in Metadata
(OMap.singleton defaultSource defaultSourceMetadata)
_mnsRemoteSchemas

View File

@ -320,6 +320,7 @@ spec = do
{ _siName = SNDefault,
_siTables = makeTableCache [albumTableInfo, trackTableInfo],
_siFunctions = mempty,
_siCustomSQL = mempty,
_siConfiguration = notImplementedYet "SourceConfig",
_siQueryTagsConfig = Nothing,
_siCustomization = ResolvedSourceCustomization mempty mempty HasuraCase Nothing

View File

@ -99,6 +99,7 @@ runUpdateFieldTest UpdateTestSetup {..} =
{ _siName = SNDefault,
_siTables = HM.singleton table tableInfo,
_siFunctions = mempty,
_siCustomSQL = mempty,
_siConfiguration = notImplementedYet "SourceConfig",
_siQueryTagsConfig = Nothing,
_siCustomization = ResolvedSourceCustomization mempty mempty HasuraCase Nothing