chore(server): more expressive logical models

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9197
GitOrigin-RevId: 3471c68d4e59310bdb62ecb3694ab30a2094916e
This commit is contained in:
Daniel Harvey 2023-05-19 17:12:51 +01:00 committed by hasura-bot
parent dcbe163a88
commit a73a009031
6 changed files with 639 additions and 155 deletions

View File

@ -771,6 +771,28 @@
],
"type": "object"
},
"BigqueryLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"BigqueryLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -782,7 +804,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/BigqueryLogicalModelField"
},
{
"$ref": "#/components/schemas/BigqueryLogicalModelField"
}
]
},
"type": "array"
},
@ -1685,6 +1714,28 @@
],
"type": "object"
},
"CitusLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"CitusLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -1696,7 +1747,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/CitusLogicalModelField"
},
{
"$ref": "#/components/schemas/CitusLogicalModelField"
}
]
},
"type": "array"
},
@ -2643,6 +2701,28 @@
],
"type": "object"
},
"CockroachLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"CockroachLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -2654,7 +2734,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/CockroachLogicalModelField"
},
{
"$ref": "#/components/schemas/CockroachLogicalModelField"
}
]
},
"type": "array"
},
@ -3756,6 +3843,28 @@
],
"type": "object"
},
"DataconnectorLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"DataconnectorLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -3767,7 +3876,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/DataconnectorLogicalModelField"
},
{
"$ref": "#/components/schemas/DataconnectorLogicalModelField"
}
]
},
"type": "array"
},
@ -5391,6 +5507,28 @@
],
"type": "object"
},
"MssqlLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"MssqlLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -5402,7 +5540,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/MssqlLogicalModelField"
},
{
"$ref": "#/components/schemas/MssqlLogicalModelField"
}
]
},
"type": "array"
},
@ -6623,6 +6768,28 @@
],
"type": "object"
},
"PostgresLogicalModelField": {
"description": "A field of a logical model",
"properties": {
"description": {
"description": "Optional description of this field",
"type": "string"
},
"name": {
"description": "Name of the field",
"type": "string"
},
"type": {
"additionalProperties": true,
"description": "Type of the field\nA type used in a Logical Model field\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"PostgresLogicalModelMetadata": {
"description": "A return type.",
"properties": {
@ -6634,7 +6801,14 @@
"description": "Return types for the logical model",
"items": {
"additionalProperties": true,
"description": "A scalar type or reference to another logical model\nvalue with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
"anyOf": [
{
"$ref": "#/components/schemas/PostgresLogicalModelField"
},
{
"$ref": "#/components/schemas/PostgresLogicalModelField"
}
]
},
"type": "array"
},

View File

@ -97,9 +97,10 @@ testImplementation = do
description: hello
fields:
- name: divided
type: #{scalarTypeToText testEnvironment Schema.TInt}
nullable: false
description: "a divided thing"
type:
scalar: #{scalarTypeToText testEnvironment Schema.TInt}
nullable: false
|]
it "Checks the logical model is deleted again" $ \testEnvironment -> do
@ -188,8 +189,9 @@ testPermissions = do
fields:
- description: a divided thing
name: divided
nullable: false
type: integer
type:
nullable: false
scalar: integer
select_permissions:
- role: "test"
permission:

View File

@ -35,6 +35,10 @@ instance J.ToJSON ReferenceType where
toJSON ArrayReference = "array"
toJSON ObjectReference = "object"
-- | this no longer matches the internal shape of logical models, where arrays
-- can nest objects OR scalars
-- however, we can defer changing this abstraction until we need to express
-- that in our tests
data LogicalModelColumn
= LogicalModelScalar
{ logicalModelColumnName :: Text,
@ -91,23 +95,49 @@ logicalModel logicalModelName =
trackLogicalModelCommand :: String -> BackendTypeConfig -> LogicalModel -> Value
trackLogicalModelCommand sourceName backendTypeConfig (LogicalModel {logicalModelDescription, logicalModelName, logicalModelColumns}) =
-- return type is an array of items
let returnTypeToJson =
J.Array
. V.fromList
. fmap
( \case
LogicalModelReference {..} ->
J.object $
[ ("logical_model" .= logicalModelColumnReference),
("link_type" .= logicalModelColumnReferenceType),
("name" .= logicalModelColumnName)
]
LogicalModelReference
{ logicalModelColumnReferenceType = ObjectReference,
logicalModelColumnReference,
logicalModelColumnName
} ->
J.object $
[ ("name" .= logicalModelColumnName),
( "type",
J.object
[ ("logical_model" .= logicalModelColumnReference)
]
)
]
LogicalModelReference
{ logicalModelColumnReferenceType = ArrayReference,
logicalModelColumnReference,
logicalModelColumnName
} ->
J.object $
[ ("name" .= logicalModelColumnName),
( "type",
J.object $
[ ( "array",
J.object $
[ ("logical_model" .= logicalModelColumnReference)
]
)
]
)
]
LogicalModelScalar {..} ->
let descriptionPair = case logicalModelColumnDescription of
Just desc -> [("description" .= desc)]
Nothing -> []
in J.object $
in -- this is the old way to encode these, but we'll keep using
-- in the tests for now to ensure we remain backwards
-- compatible
J.object $
[ ("name" .= logicalModelColumnName),
("type" .= (BackendType.backendScalarType backendTypeConfig) logicalModelColumnType),
("nullable" .= logicalModelColumnNullable)

View File

@ -26,6 +26,8 @@ module Hasura.GraphQL.Schema.Select
tablePermissionsInfo,
tableSelectionList,
logicalModelSelectionList,
logicalModelArrayRelationshipField,
logicalModelObjectRelationshipField,
)
where
@ -63,8 +65,16 @@ import Hasura.GraphQL.Schema.Parser qualified as P
import Hasura.GraphQL.Schema.Table
import Hasura.GraphQL.Schema.Typename
import Hasura.LogicalModel.Cache (LogicalModelInfo (..))
import Hasura.LogicalModel.Types (LogicalModelField (..), LogicalModelName (..), LogicalModelReferenceType (..))
import Hasura.LogicalModel.Types
( LogicalModelField (..),
LogicalModelName (..),
LogicalModelType (..),
LogicalModelTypeArray (..),
LogicalModelTypeReference (..),
LogicalModelTypeScalar (..),
)
import Hasura.Name qualified as Name
import Hasura.NativeQuery.Cache (NativeQueryInfo (..))
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.IR.BoolExp
@ -495,10 +505,95 @@ columnToRelName :: forall b. (Backend b) => Column b -> Maybe RelName
columnToRelName column =
RelName <$> mkNonEmptyText (toTxt column)
-- | parse a single logical model field. Currently the only way to 'fulfil' a
-- non-scalar field is with a relationship that provides the nested
-- object/array information.
parseLogicalModelField ::
forall b r m n.
( MonadBuildSchema b r m n,
BackendNativeQuerySelectSchema b
) =>
InsOrdHashMap RelName (RelInfo b) ->
Column b ->
LogicalModelField b ->
MaybeT (SchemaT r m) (IP.FieldParser MetadataObjId n (AnnotatedField b))
parseLogicalModelField
_
column
( LogicalModelField
{ lmfDescription,
lmfType = LogicalModelTypeScalar (LogicalModelTypeScalarC {lmtsScalar, lmtsNullable})
}
) = do
columnName <- hoistMaybe (G.mkName (toTxt column))
-- We have not yet worked out what providing permissions here enables
let caseBoolExpUnpreparedValue = Nothing
columnType = ColumnScalar lmtsScalar
pathArg = scalarSelectionArgumentsParser columnType
field <- lift $ columnParser columnType (G.Nullability lmtsNullable)
pure $!
P.selection columnName (G.Description <$> lmfDescription) pathArg field
<&> IR.mkAnnColumnField column columnType caseBoolExpUnpreparedValue
parseLogicalModelField
relationshipInfo
column
( LogicalModelField
{ lmfType =
LogicalModelTypeReference
(LogicalModelTypeReferenceC {lmtrReference})
}
) = do
-- we currently ignore nullability and assume the field is nullable
relName <- hoistMaybe $ columnToRelName @b column
-- lookup the reference in the data source
relationship <- hoistMaybe $ InsOrdHashMap.lookup relName relationshipInfo
logicalModelObjectRelationshipField @b @r @m @n lmtrReference relationship
parseLogicalModelField
relationshipInfo
column
( LogicalModelField
{ lmfType =
LogicalModelTypeArray
( LogicalModelTypeArrayC
{ lmtaArray =
LogicalModelTypeReference (LogicalModelTypeReferenceC {lmtrReference})
}
)
}
) = do
-- we currently ignore nullability and assume the field is
-- non-nullable, as are the contents
relName <- hoistMaybe $ columnToRelName @b column
-- lookup the reference in the data source
relationship <- hoistMaybe $ InsOrdHashMap.lookup relName relationshipInfo
logicalModelArrayRelationshipField @b @r @m @n lmtrReference relationship
parseLogicalModelField
_
_
( LogicalModelField
{ lmfType =
LogicalModelTypeArray
(LogicalModelTypeArrayC {lmtaArray = LogicalModelTypeScalar _})
}
) =
throw500 "Arrays of scalar types are not currently implemented"
parseLogicalModelField
_
_
( LogicalModelField
{ lmfType =
LogicalModelTypeArray
(LogicalModelTypeArrayC {lmtaArray = LogicalModelTypeArray _})
}
) =
throw500 "Nested arrays are not currently implemented"
defaultLogicalModelSelectionSet ::
forall b r m n.
( MonadBuildSchema b r m n,
BackendTableSelectSchema b,
BackendNativeQuerySelectSchema b
) =>
InsOrdHashMap RelName (RelInfo b) ->
@ -514,37 +609,6 @@ defaultLogicalModelSelectionSet relationshipInfo logicalModel = runMaybeT $ do
Permission.PCStar -> True
Permission.PCCols cols -> column `elem` cols
let parseField ::
Column b ->
LogicalModelField b ->
MaybeT (SchemaT r m) (IP.FieldParser MetadataObjId n (AnnotatedField b))
parseField column inputField = do
columnName <- hoistMaybe (G.mkName (toTxt column))
-- We have not yet worked out what providing permissions here enables
let caseBoolExpUnpreparedValue = Nothing
case inputField of
LogicalModelScalarField {..} -> do
let columnType = ColumnScalar lmfType
pathArg = scalarSelectionArgumentsParser columnType
field <- lift $ columnParser columnType (G.Nullability lmfNullable)
pure $!
P.selection columnName (G.Description <$> lmfDescription) pathArg field
<&> IR.mkAnnColumnField column columnType caseBoolExpUnpreparedValue
LogicalModelReference {..} -> do
relName <- hoistMaybe $ columnToRelName @b column
-- fetch the nested custom return type for comparison purposes
_nestedLogicalModel <- lift $ askLogicalModelInfo @b lmfLogicalModel
-- lookup the reference in the data source
relationship <- hoistMaybe $ InsOrdHashMap.lookup relName relationshipInfo
-- check the types match
-- return IR for the actual data source lookup (ie, the table
-- lookup for a relationship)
logicalModelRelationshipField @b @r @m @n lmfReferenceType relationship
let fieldName = getLogicalModelName (_lmiName logicalModel)
-- which columns are we allowed to access given permissions?
@ -553,7 +617,7 @@ defaultLogicalModelSelectionSet relationshipInfo logicalModel = runMaybeT $ do
(isSelectable . fst)
(InsOrdHashMap.toList (_lmiFields logicalModel))
parsers <- traverse (uncurry parseField) allowedColumns
parsers <- traverse (uncurry (parseLogicalModelField relationshipInfo)) allowedColumns
let description = G.Description <$> _lmiDescription logicalModel
@ -1612,57 +1676,83 @@ tablePermissionsInfo selectPermissions =
IR._tpLimit = spiLimit selectPermissions
}
-- | Field parsers for a logical model relationship
logicalModelRelationshipField ::
-- | Field parsers for a logical model object relationship
logicalModelObjectRelationshipField ::
forall b r m n.
( BackendTableSelectSchema b,
BackendNativeQuerySelectSchema b,
( BackendNativeQuerySelectSchema b,
MonadBuildSchema b r m n
) =>
LogicalModelReferenceType ->
LogicalModelName ->
RelInfo b ->
MaybeT (SchemaT r m) (FieldParser n (AnnotatedField b))
logicalModelRelationshipField relationshipType ri =
case (relationshipType, riType ri) of
(ObjectReference, ObjRel) ->
case riTarget ri of
RelTargetNativeQuery nativeQueryName -> do
nativeQueryInfo <- lift $ askNativeQueryInfo nativeQueryName
relFieldName <- lift $ textToName $ relNameToTxt $ riName ri
logicalModelObjectRelationshipField logicalModelName ri | riType ri == ObjRel =
case riTarget ri of
RelTargetNativeQuery nativeQueryName -> do
nativeQueryInfo <- lift $ askNativeQueryInfo nativeQueryName
let objectRelDesc = Just $ G.Description "An object relationship"
-- not sure if this the correct way to report mismatches, or if it
-- even possible for this to be an issue at this point
when
(logicalModelName /= _lmiName (_nqiReturns nativeQueryInfo))
( throw500 $
"Expected object relationship to return "
<> toTxt logicalModelName
<> " but it returns "
<> toTxt (_lmiName (_nqiReturns nativeQueryInfo))
<> "."
)
nativeQueryParser <- MaybeT $ selectNativeQueryObject nativeQueryInfo relFieldName objectRelDesc
relFieldName <- lift $ textToName $ relNameToTxt $ riName ri
pure $
nativeQueryParser <&> \selectExp ->
IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (riMapping ri) selectExp)
RelTargetTable _otherTableName -> do
throw500 "Object relationships from logical models to tables are not implemented"
(ArrayReference, ArrRel) ->
case riTarget ri of
RelTargetNativeQuery nativeQueryName -> do
nativeQueryInfo <- lift $ askNativeQueryInfo nativeQueryName
relFieldName <- lift $ textToName $ relNameToTxt $ riName ri
let objectRelDesc = Just $ G.Description "An object relationship"
let objectRelDesc = Just $ G.Description "An array relationship"
nativeQueryParser <- MaybeT $ selectNativeQueryObject nativeQueryInfo relFieldName objectRelDesc
nativeQueryParser <- MaybeT $ selectNativeQuery nativeQueryInfo relFieldName objectRelDesc
pure $
nativeQueryParser <&> \selectExp ->
IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (riMapping ri) selectExp)
RelTargetTable _otherTableName -> do
throw500 "Object relationships from logical models to tables are not implemented"
logicalModelObjectRelationshipField _ _ =
hoistMaybe Nothing -- the target logical model expected an object relationship, but this was an array
pure $
nativeQueryParser <&> \selectExp ->
IR.AFArrayRelation $
IR.ASSimple $
IR.AnnRelationSelectG (riName ri) (riMapping ri) selectExp
RelTargetTable otherTableName -> do
otherTableInfo <- lift $ askTableInfo otherTableName
relFieldName <- lift $ textToName $ relNameToTxt $ riName ri
-- | Field parsers for a logical model relationship
logicalModelArrayRelationshipField ::
forall b r m n.
( BackendNativeQuerySelectSchema b,
MonadBuildSchema b r m n
) =>
LogicalModelName ->
RelInfo b ->
MaybeT (SchemaT r m) (FieldParser n (AnnotatedField b))
logicalModelArrayRelationshipField logicalModelName ri | riType ri == ArrRel =
case riTarget ri of
RelTargetNativeQuery nativeQueryName -> do
nativeQueryInfo <- lift $ askNativeQueryInfo nativeQueryName
relFieldName <- lift $ textToName $ relNameToTxt $ riName ri
let arrayRelDesc = Just $ G.Description "An array relationship"
otherTableParser <- MaybeT $ selectTable otherTableInfo relFieldName arrayRelDesc
pure $
otherTableParser <&> \selectExp ->
IR.AFArrayRelation $
IR.ASSimple $
IR.AnnRelationSelectG (riName ri) (riMapping ri) selectExp
_ -> hoistMaybe Nothing -- mismatch between relationship type expected on Logical Model, and in the source of data
-- not sure if this the correct way to report mismatches, or if it
-- even possible for this to be an issue at this point
when
(logicalModelName /= _lmiName (_nqiReturns nativeQueryInfo))
( throw500 $
"Expected array relationship to return "
<> toTxt logicalModelName
<> " but it returns "
<> toTxt (_lmiName (_nqiReturns nativeQueryInfo))
<> "."
)
let objectRelDesc = Just $ G.Description "An array relationship"
nativeQueryParser <- MaybeT $ selectNativeQuery nativeQueryInfo relFieldName objectRelDesc
pure $
nativeQueryParser <&> \selectExp ->
IR.AFArrayRelation $
IR.ASSimple $
IR.AnnRelationSelectG (riName ri) (riMapping ri) selectExp
RelTargetTable _otherTableName -> do
throw500 "Array relationships from logical models to tables are not implemented"
logicalModelArrayRelationshipField _ _ =
hoistMaybe Nothing -- the target logical model expected an array relationship, but this was an object

View File

@ -10,7 +10,7 @@ import Data.HashMap.Strict qualified as HashMap
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Text.Extended (ToTxt (toTxt))
import Hasura.LogicalModel.NullableScalarType (NullableScalarType (..))
import Hasura.LogicalModel.Types (LogicalModelField (..))
import Hasura.LogicalModel.Types (LogicalModelField (..), LogicalModelType (..), LogicalModelTypeScalar (..))
import Hasura.Prelude
import Hasura.RQL.Types.Backend (Backend (..))
import Hasura.RQL.Types.Column (ColumnInfo (..), ColumnMutability (..), ColumnType (..), fromCol)
@ -23,9 +23,14 @@ columnsFromFields ::
columnsFromFields =
InsOrdHashMap.mapMaybe
( \case
LogicalModelScalarField
{ lmfType = nstType,
lmfNullable = nstNullable,
LogicalModelField
{ lmfType =
LogicalModelTypeScalar
( LogicalModelTypeScalarC
{ lmtsScalar = nstType,
lmtsNullable = nstNullable
}
),
lmfDescription = nstDescription
} ->
Just (NullableScalarType {..})

View File

@ -1,8 +1,13 @@
{-# LANGUAGE DeriveAnyClass #-}
-- | A name for a logical model as it is recognized by the graphql schema.
module Hasura.LogicalModel.Types
( LogicalModelName (..),
LogicalModelReferenceType (..),
LogicalModelField (..),
LogicalModelType (..),
LogicalModelTypeScalar (..),
LogicalModelTypeArray (..),
LogicalModelTypeReference (..),
logicalModelFieldMapCodec,
)
where
@ -12,12 +17,13 @@ import Autodocodec
dimapCodec,
)
import Autodocodec qualified as AC
import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..), ToJSONKey, Value, object, withObject, (.!=), (.:), (.:?), (.=))
import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..), ToJSONKey, Value)
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Text.Extended (ToTxt)
import Hasura.Metadata.DTO.Placeholder (placeholderCodecViaJSON)
import Hasura.Prelude hiding (first)
import Hasura.RQL.Types.Backend (Backend (..))
import Hasura.RQL.Types.BackendTag (backendPrefix)
import Language.GraphQL.Draft.Syntax qualified as G
import Language.Haskell.TH.Syntax (Lift)
@ -35,77 +41,254 @@ instance ToJSONKey LogicalModelName
----
-- | Type of reference for another Logical Model - object or array
data LogicalModelReferenceType
= ArrayReference
| ObjectReference
deriving stock (Eq, Ord, Generic, Show)
deriving (FromJSON, ToJSON) via (AC.Autodocodec LogicalModelReferenceType)
data LogicalModelTypeScalar b = LogicalModelTypeScalarC
{ lmtsScalar :: ScalarType b,
lmtsNullable :: Bool
}
deriving (Generic)
instance HasCodec LogicalModelReferenceType where
deriving stock instance (Backend b) => Eq (LogicalModelTypeScalar b)
deriving stock instance (Backend b) => Show (LogicalModelTypeScalar b)
instance (Backend b) => Hashable (LogicalModelTypeScalar b)
instance (Backend b) => NFData (LogicalModelTypeScalar b)
instance (Backend b) => HasCodec (LogicalModelTypeScalar b) where
codec =
AC.CommentCodec
("Link type of reference, it refers to a single object or an array of objects")
$ AC.stringConstCodec
$ (ArrayReference, "array") :| [(ObjectReference, "object")]
("A scalar type used in a Logical Model.")
$ AC.object (backendPrefix @b <> "LogicalModelTypeScalar")
$ LogicalModelTypeScalarC
<$> AC.requiredField "scalar" scalarDoc
AC..= lmtsScalar
<*> AC.optionalFieldWithDefault "nullable" False nullableDoc
AC..= lmtsNullable
where
scalarDoc = "Name of the scalar type"
nullableDoc = "Whether this field is allowed to contain null values or not"
instance Hashable LogicalModelReferenceType
deriving via
(AC.Autodocodec (LogicalModelTypeScalar b))
instance
(Backend b) => (FromJSON (LogicalModelTypeScalar b))
instance NFData LogicalModelReferenceType
deriving via
(AC.Autodocodec (LogicalModelTypeScalar b))
instance
(Backend b) => (ToJSON (LogicalModelTypeScalar b))
----
-- | either a scalar type or a reference to another logical model
data LogicalModelField b
= LogicalModelScalarField
{ lmfName :: Column b,
lmfType :: ScalarType b, -- int, string, blah
lmfNullable :: Bool,
lmfDescription :: Maybe Text
}
| LogicalModelReference
{ lmfName :: Column b,
lmfLogicalModel :: LogicalModelName, -- name of another logical model
lmfReferenceType :: LogicalModelReferenceType
}
data LogicalModelTypeArray b = LogicalModelTypeArrayC
{ lmtaArray :: LogicalModelType b,
lmtaNullable :: Bool
}
deriving (Generic)
instance (Backend b) => HasCodec (LogicalModelField b) where
deriving stock instance (Backend b) => Eq (LogicalModelTypeArray b)
deriving stock instance (Backend b) => Show (LogicalModelTypeArray b)
instance (Backend b) => Hashable (LogicalModelTypeArray b)
instance (Backend b) => NFData (LogicalModelTypeArray b)
instance (Backend b) => HasCodec (LogicalModelTypeArray b) where
codec =
AC.CommentCodec
("A scalar type or reference to another logical model")
("An array type used in a Logical Model.")
$ AC.object (backendPrefix @b <> "LogicalModelTypeArray")
$ LogicalModelTypeArrayC
<$> AC.requiredField "array" arrayDoc
AC..= lmtaArray
<*> AC.optionalFieldWithDefault "nullable" False nullableDoc
AC..= lmtaNullable
where
arrayDoc = "Type of items inside array"
nullableDoc = "Whether this field can be null or not"
deriving via
(AC.Autodocodec (LogicalModelTypeArray b))
instance
(Backend b) => (FromJSON (LogicalModelTypeArray b))
deriving via
(AC.Autodocodec (LogicalModelTypeArray b))
instance
(Backend b) => (ToJSON (LogicalModelTypeArray b))
----
data LogicalModelTypeReference = LogicalModelTypeReferenceC
{ lmtrReference :: LogicalModelName,
lmtrNullable :: Bool
}
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, NFData)
instance HasCodec LogicalModelTypeReference where
codec =
AC.CommentCodec
("A reference to another Logical Model.")
$ AC.object "LogicalModelTypeReference"
$ LogicalModelTypeReferenceC
<$> AC.requiredField "logical_model" referenceDoc
AC..= lmtrReference
<*> AC.optionalFieldWithDefault "nullable" False nullableDoc
AC..= lmtrNullable
where
referenceDoc = "Name of another Logical Model to nest"
nullableDoc = "Whether this field can be null or not"
deriving via
(AC.Autodocodec LogicalModelTypeReference)
instance
(FromJSON LogicalModelTypeReference)
deriving via
(AC.Autodocodec LogicalModelTypeReference)
instance
(ToJSON LogicalModelTypeReference)
----
data LogicalModelType b
= LogicalModelTypeScalar (LogicalModelTypeScalar b)
| LogicalModelTypeArray (LogicalModelTypeArray b)
| LogicalModelTypeReference LogicalModelTypeReference
deriving (Generic)
deriving stock instance (Backend b) => Eq (LogicalModelType b)
deriving stock instance (Backend b) => Show (LogicalModelType b)
instance (Backend b) => Hashable (LogicalModelType b)
instance (Backend b) => NFData (LogicalModelType b)
-- | forgive me, I really did try and do this the native Autodocodec way
-- and everything I did kept freezing the whole of HGE
instance (Backend b) => HasCodec (LogicalModelType b) where
codec =
AC.CommentCodec
("A type used in a Logical Model field")
$ placeholderCodecViaJSON
instance (Backend b) => FromJSON (LogicalModelField b) where
parseJSON = withObject "LogicalModelField" \o ->
parseReference o <|> parseScalar o
where
parseScalar obj = do
lmfName <- obj .: "name"
lmfType <- obj .: "type"
lmfNullable <- obj .:? "nullable" .!= False
lmfDescription <- obj .:? "description"
pure (LogicalModelScalarField {..})
parseReference obj = do
lmfLogicalModel <- obj .: "logical_model"
lmfReferenceType <- obj .: "link_type"
lmfName <- obj .: "name"
pure (LogicalModelReference {..})
instance (Backend b) => FromJSON (LogicalModelType b) where
parseJSON j =
(LogicalModelTypeScalar <$> parseJSON j)
<|> (LogicalModelTypeArray <$> parseJSON j)
<|> (LogicalModelTypeReference <$> parseJSON j)
instance (Backend b) => ToJSON (LogicalModelField b) where
toJSON (LogicalModelScalarField {..}) =
object $
[ "name" .= lmfName,
"type" .= lmfType,
"nullable" .= lmfNullable
]
<> maybeDescription
instance (Backend b) => ToJSON (LogicalModelType b) where
toJSON (LogicalModelTypeScalar t) = toJSON t
toJSON (LogicalModelTypeArray t) = toJSON t
toJSON (LogicalModelTypeReference t) = toJSON t
----
-- | a single field in a Logical Model
data LogicalModelField b = LogicalModelField
{ lmfName :: Column b,
lmfType :: LogicalModelType b,
lmfDescription :: Maybe Text
}
deriving (Generic)
data LogicalModelFieldSimple b = LogicalModelFieldSimple
{ lmfsName :: Column b,
lmfsScalar :: ScalarType b,
lmfsNullable :: Bool,
lmfsDescription :: Maybe Text
}
-- | this codec is complicated because we want to support both the old scalar
-- encoded fields and our new separate type
instance (Backend b) => HasCodec (LogicalModelField b) where
codec = AC.parseAlternative newCodec simpleCodecMappedToNew -- we always encode as `newCodec` but we try parsing with both
where
maybeDescription = case lmfDescription of
Just desc -> ["description" .= desc]
Nothing -> []
toJSON (LogicalModelReference {..}) =
object ["name" .= lmfName, "logical_model" .= lmfLogicalModel, "link_type" .= lmfReferenceType]
-- if we parse the old kind, convert it to the new exciting kind
fromSimple :: LogicalModelFieldSimple b -> LogicalModelField b
fromSimple (LogicalModelFieldSimple {lmfsName, lmfsScalar, lmfsNullable, lmfsDescription}) =
LogicalModelField
{ lmfName = lmfsName,
lmfDescription = lmfsDescription,
lmfType =
LogicalModelTypeScalar
( LogicalModelTypeScalarC {lmtsScalar = lmfsScalar, lmtsNullable = lmfsNullable}
)
}
-- try and convert the new kind to the old (this is partial, but
-- shouldn't actually be used)
toSimple
( LogicalModelField
{ lmfName,
lmfDescription,
lmfType = LogicalModelTypeScalar (LogicalModelTypeScalarC {lmtsScalar, lmtsNullable})
}
) =
LogicalModelFieldSimple
{ lmfsName = lmfName,
lmfsScalar = lmtsScalar,
lmfsNullable = lmtsNullable,
lmfsDescription = lmfDescription
}
toSimple _ = error "Could not convert LogicalModelField to LogicalModelFieldSimple"
simpleCodecMappedToNew :: AC.JSONCodec (LogicalModelField b)
simpleCodecMappedToNew = AC.dimapCodec fromSimple toSimple simpleCodec
simpleCodec :: AC.JSONCodec (LogicalModelFieldSimple b)
simpleCodec =
-- this is the simpler old codec that did scalar types only
AC.CommentCodec
("A field of a logical model")
$ AC.object (backendPrefix @b <> "LogicalModelField")
$ LogicalModelFieldSimple
<$> AC.requiredField "name" nameDoc
AC..= lmfsName
<*> AC.requiredField "type" typeDoc
AC..= lmfsScalar
<*> AC.optionalFieldWithDefault "nullable" False nullableDoc
AC..= lmfsNullable
<*> AC.optionalField "description" descriptionDoc
AC..= lmfsDescription
where
nameDoc = "Name of the field"
nullableDoc = "Is field nullable or not?"
typeDoc = "Type of the field"
descriptionDoc = "Optional description of this field"
newCodec =
-- the new codec which defers to LogicalModelType for all the
-- complexities
AC.CommentCodec
("A field of a logical model")
$ AC.object (backendPrefix @b <> "LogicalModelField")
$ LogicalModelField
<$> AC.requiredField "name" nameDoc
AC..= lmfName
<*> AC.requiredField "type" typeDoc
AC..= lmfType
<*> AC.optionalField "description" descriptionDoc
AC..= lmfDescription
where
nameDoc = "Name of the field"
typeDoc = "Type of the field"
descriptionDoc = "Optional description of this field"
deriving via
(AC.Autodocodec (LogicalModelField b))
instance
(Backend b) => (ToJSON (LogicalModelField b))
deriving via
(AC.Autodocodec (LogicalModelField b))
instance
(Backend b) => FromJSON (LogicalModelField b)
deriving stock instance (Backend b) => Eq (LogicalModelField b)