graphql-engine/server/src-lib/Hasura/Metadata/DTO/Placeholder.hs
Jesse Hallett 84fd5910b0 server: polymorphic codec for metadata sources
This PR expands the OpenAPI specification generated for metadata to include separate definitions for `SourceMetadata` for each native database type, and for DataConnector.

For the most part the changes add `HasCodec` implementations, and don't modify existing code otherwise.

The generated OpenAPI spec can be used to generate TypeScript definitions that distinguish different source metadata types based on the value of the `kind` properly. There is a problem: because the specified `kind` value for a data connector source is any string, when TypeScript gets a source with a `kind` value of, say, `"postgres"`, it cannot unambiguously determine whether the source is postgres, or a data connector. For example,

```ts
function consumeSourceMetadata(source: SourceMetadata) {
    if (source.kind === "postgres" || source.kind === "pg") {
        // At this point TypeScript infers that `source` is either an instance
        // of `PostgresSourceMetadata`, or `DataconnectorSourceMetadata`. It
        // can't narrow further.
        source
    }
    if (source.kind === "something else") {
        // TypeScript infers that this `source` must be an instance of
        // `DataconnectorSourceMetadata` because `source.kind` does not match
        // any of the other options.
        source
    }
}
```

The simplest way I can think of to fix this would be to add a boolean property to the `SourceMetadata` type along the lines of `isNative` or `isDataConnector`. This could be a field that only exists in serialized data, like the metadata version field. The combination of one of the native database names for `kind`, and a true value for `isNative` would be enough for TypeScript to unambiguously distinguish the source kinds.

But note that in the current state TypeScript is able to reference the short `"pg"` name correctly!

~~Tests are not passing yet due to some discrepancies in DTO serialization vs existing Metadata serialization. I'm working on that.~~

The placeholders that I used for table and function metadata are not compatible with the ordered JSON serialization in use. I think the best solution is to write compatible codecs for those types in another PR. For now I have disabled some DTO tests for this PR.

Here are the generated [OpenAPI spec](https://github.com/hasura/graphql-engine-mono/files/9397333/openapi.tar.gz) based on these changes, and the generated [TypeScript client code](https://github.com/hasura/graphql-engine-mono/files/9397339/client-typescript.tar.gz) based on that spec.

Ticket: [MM-66](https://hasurahq.atlassian.net/browse/MM-66)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5582
GitOrigin-RevId: e1446191c6c832879db04f129daa397a3be03f62
2022-08-25 18:36:02 +00:00

92 lines
4.2 KiB
Haskell

-- | We are in the process of building DTO types incrementally. We use
-- placeholder types in positions in data structures that are not fully-defined
-- yet. For example 'PlaceholderObject' represents some unspecified JSON object,
-- and 'PlaceholderArray' represents an array whose contents are not yet
-- specified.
--
-- We are transitioning from converting 'Hasura.RQL.Types.Metadata' directly to
-- JSON to converting it to 'Hasura.Server.API.DTO.Metadata.MetadataDTO'
-- instead. Serialization and deserialization for placeholder values is
-- delegated to the old JSON serialization code.
module Hasura.Metadata.DTO.Placeholder
( PlaceholderArray (..),
PlaceholderObject (..),
IsPlaceholder (..),
placeholderCodecViaJSON,
)
where
import Autodocodec (Autodocodec, HasCodec (codec), JSONCodec, bimapCodec, codecViaAeson, dimapCodec, valueCodec, vectorCodec, (<?>))
import Autodocodec.OpenAPI ()
import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson qualified as JSON
import Data.Aeson.Ordered qualified as AO
import Data.Aeson.Types qualified as JSON
import Data.OpenApi qualified as OpenApi
import Data.Vector qualified as V
import Hasura.Prelude
-- TODO: Store ordered aeson values in placeholders instead of stock aeson
-- values so that we can preserve order. We want to do that after #4842 is
-- merged so we can use 'toOrderedJSONVia' to produce the appropriate codecs.
-- | Stands in for an array that we have not had time to fully specify yet.
-- Generated OpenAPI documentation for 'PlaceholderArray' will permit an array
-- of values of any type, and a note will be appended to the documentation
-- string for the value explaining that this is a temporary placeholder.
newtype PlaceholderArray = PlaceholderArray JSON.Array
deriving newtype (Show, Eq, FromJSON, ToJSON)
deriving stock (Generic)
deriving (OpenApi.ToSchema) via (Autodocodec PlaceholderArray)
-- | Stands in for an object that we have not had time to fully specify yet.
-- Generated OpenAPI documentation for 'PlaceholderObject' will permit an object
-- with any keys with any types of values. A note will be appended to the
-- documentation string for the value explaining that this is a temporary
-- placeholder.
newtype PlaceholderObject = PlaceholderObject JSON.Object
deriving newtype (Show, Eq, FromJSON, ToJSON)
deriving stock (Generic)
deriving (OpenApi.ToSchema) via (Autodocodec PlaceholderObject)
instance HasCodec PlaceholderArray where
codec = dimapCodec mapOutput mapInput (vectorCodec valueCodec) <?> documentation
where
mapOutput = PlaceholderArray
mapInput (PlaceholderArray a) = a
documentation =
"\n\narray of values of unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
instance HasCodec PlaceholderObject where
codec = codecViaAeson "\n\nobject with unspecified properties - this is a placeholder that will eventually be replaced with a more detailed description"
class IsPlaceholder p a | a -> p where
-- | Use this function to mark an Aeson type (Array or Object) as
-- a temporary placeholder in a larger data structure.
placeholder :: a -> p
instance IsPlaceholder PlaceholderArray JSON.Array where
placeholder = PlaceholderArray
instance IsPlaceholder PlaceholderObject JSON.Object where
placeholder = PlaceholderObject
instance IsPlaceholder PlaceholderArray AO.Array where
placeholder = PlaceholderArray . V.fromList . map AO.fromOrdered . V.toList
instance IsPlaceholder PlaceholderObject AO.Object where
placeholder = PlaceholderObject . AO.fromOrderedObject
-- | This placeholder can be used in a codec to represent any type of data that
-- has `FromJSON` and `ToJSON` instances. Generated OpenAPI specifications based
-- on this codec will not show any information about the internal structure of
-- the type so ideally uses of this placeholder should eventually be replaced
-- with more descriptive codecs.
placeholderCodecViaJSON :: (FromJSON a, ToJSON a) => JSONCodec a
placeholderCodecViaJSON =
bimapCodec dec enc valueCodec
<?> "value with unspecified type - this is a placeholder that will eventually be replaced with a more detailed description"
where
dec = JSON.parseEither JSON.parseJSON
enc = JSON.toJSON