graphql-engine/server/src-lib/Hasura/EncJSON.hs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

197 lines
6.8 KiB
Haskell
Raw Normal View History

-- A module for representing encoded json
-- and efficient operations to construct them
module Hasura.EncJSON
( EncJSON,
encJFromBuilder,
encJToLBS,
encJToBS,
encJFromJValue,
encJFromChar,
encJFromText,
encJFromNonEmptyText,
encJFromBool,
encJFromBS,
encJFromLBS,
encJFromList,
encJFromAssocList,
encJFromInsOrdHashMap,
encJFromOrderedValue,
encJFromBsWithoutSoh,
encJFromLbsWithoutSoh,
)
where
import Data.Aeson qualified as J
import Data.Aeson.Encoding qualified as J
import Data.Aeson.Ordered qualified as JO
import Data.ByteString qualified as B
import Data.ByteString.Builder qualified as BB
import Data.ByteString.Builder.Extra qualified as BB
import Data.ByteString.Builder.Internal qualified as BB
import Data.ByteString.Lazy qualified as BL
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Text.Encoding qualified as TE
import Data.Text.NonEmpty (NonEmptyText)
import Data.Text.NonEmpty qualified as NET
import Data.Vector qualified as V
import Data.Word (Word8)
import Database.PG.Query qualified as PG
import Hasura.Prelude
newtype EncJSON = EncJSON {unEncJSON :: BB.Builder}
-- | JSONB bytestrings start with a @SOH@ header @\x01@ and then
-- follow with a valid JSON string, therefore we should check for this
-- and remove if necessary before decoding as normal
instance PG.FromCol EncJSON where
fromCol = \case
Just bs -> Right $ encJFromBsWithoutSoh bs
-- null values return a JSON null value
Nothing -> Right $ encJFromJValue J.Null
-- | JSONB bytestrings start with a @SOH@ header @\x01@ and then
-- follow with a valid JSON string, therefore we should check for this
-- and remove if necessary before decoding as normal
encJFromBsWithoutSoh :: B.ByteString -> EncJSON
encJFromBsWithoutSoh = encJFromBS . removeSOH B.uncons
-- | JSONB bytestrings start with a @SOH@ header @\x01@ and then
-- follow with a valid JSON string, therefore we should check for this
-- and remove if necessary before decoding as normal
encJFromLbsWithoutSoh :: BL.ByteString -> EncJSON
encJFromLbsWithoutSoh = encJFromLBS . removeSOH BL.uncons
-- | JSONB bytestrings start with a @SOH@ header @\x01@ and then
-- follow with a valid JSON string, therefore we should check for this
-- and remove if necessary before decoding as normal
removeSOH :: (bs -> Maybe (Word8, bs)) -> bs -> bs
removeSOH uncons bs =
case uncons bs of
Just (bsHead, bsTail) ->
if bsHead == 1
then bsTail
else bs
Nothing -> bs
server: plumb `StoredIntrospection` while building the Schema Cache We'd like to be able to build a Schema Cache from only serializable data. We already have Metadata. The data that's missing to build a Schema Cache is referred to as "stored introspection", and this includes: - DB introspection - User-defined enum values (i.e. contents of specific DB tables) - Remote schema introspection This PR introduces a new `StoredIntrospection` container that holds that data, and plumbs it through to the right parts of the schema cache building process, so that stored introspection can be used as a substitute for fresh introspection requests against live data sources. The serialization of `StoredIntrospection` is intended to be straightforward: just take the serialized source introspection results, and put them in an appropriate JSON object. Though I don't think that this PR achieves that entirely. In order for `StoredIntrospection` to be deserializable (through `aeson` instances), while keeping the required code changes low, this piggy-backs off of the `ResolvedSource` data type. `ResolvedSource` is _almost_ exactly what we want, and _almost_ deserializable, so this PR brings it across the finish line by moving a few things out of that type, and adding a `FromJSON (RawFunctionInfo b)` context to the `Backend` type class. [PLAT-270]: https://hasurahq.atlassian.net/browse/PLAT-270?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [PLAT-270]: https://hasurahq.atlassian.net/browse/PLAT-270?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [PLAT-276]: https://hasurahq.atlassian.net/browse/PLAT-276?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [PLAT-276]: https://hasurahq.atlassian.net/browse/PLAT-276?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7053 GitOrigin-RevId: 5001b4ea086195cb5e65886747eac2a0a657b64c
2023-01-20 17:51:11 +03:00
-- NB: this is somewhat wasteful, because the design of the `FromJSON` type
-- class forces that the incoming `ByteString` value is first parsed to an
-- `aeson` `Value`. But we then immediately re-serialize it here into an
-- `EncJSON`.
instance J.FromJSON EncJSON where
parseJSON = pure . encJFromJValue
-- No other instances for `EncJSON`. In particular, because:
--
-- - Having a `Semigroup` or `Monoid` instance allows constructing semantically
-- illegal values of type `EncJSON`. To drive this point home: the derived
-- `Semigroup` and `Monoid` instances always produce illegal JSON. It is
-- merely through an abuse of these APIs that legal JSON can be created.
--
-- - `IsString` would be a footgun because it's not clear what its expected
-- behavior is: does it construct serialized JSON from a serialized `String`,
-- or does it serialize a given `String` into a JSON-encoded string value?
--
-- - `Eq` would also be a footgun: does it compare two serialized values, or
-- does it compare values semantically?
--
-- - `Show`: unused.
2019-04-17 12:48:41 +03:00
encJToLBS :: EncJSON -> BL.ByteString
{-# INLINE encJToLBS #-}
encJToLBS = BB.toLazyByteStringWith outputAllocationStrategy mempty . unEncJSON
where
-- this is a modification of 'untrimmedStrategy' tuned for typical request sizes.
-- There's no point to trimming; that's just going to create more garbage
-- that will be collected immediately.
outputAllocationStrategy =
BB.customStrategy nextBuffer bufSize0 (\_ _ -> False)
where
-- NOTE: on cloud we see uncompressed response body sizes like:
-- P50: 150 bytes
-- P75: 1200
-- P90: 17000
-- P99: 95000
bufSize0 = 200 -- bytes
bufGrowthFactor = 5
{-# INLINE nextBuffer #-}
nextBuffer Nothing = BB.newBuffer bufSize0
-- FYI: minSize == bufSize0, except e.g. where `ensureFree` is used (and maybe other situations)
nextBuffer (Just (prevBuf, minSize)) =
-- nextBufSize grows exponentially, up to defaultChunkSize; but always at least minSize
let nextBufSize = max minSize (min BB.defaultChunkSize (BB.bufferSize prevBuf * bufGrowthFactor))
in BB.newBuffer nextBufSize
encJToBS :: EncJSON -> B.ByteString
encJToBS = BL.toStrict . encJToLBS
{-# INLINE encJToBS #-}
encJFromBuilder :: BB.Builder -> EncJSON
encJFromBuilder = EncJSON
{-# INLINE encJFromBuilder #-}
encJFromBS :: B.ByteString -> EncJSON
encJFromBS = EncJSON . BB.byteString
{-# INLINE encJFromBS #-}
encJFromLBS :: BL.ByteString -> EncJSON
encJFromLBS = EncJSON . BB.lazyByteString
{-# INLINE encJFromLBS #-}
encJFromJValue :: J.ToJSON a => a -> EncJSON
encJFromJValue = encJFromBuilder . J.fromEncoding . J.toEncoding
{-# INLINE encJFromJValue #-}
encJFromChar :: Char -> EncJSON
encJFromChar = EncJSON . BB.charUtf8
{-# INLINE encJFromChar #-}
encJFromText :: Text -> EncJSON
encJFromText = encJFromBuilder . TE.encodeUtf8Builder
{-# INLINE encJFromText #-}
encJFromNonEmptyText :: NonEmptyText -> EncJSON
encJFromNonEmptyText = encJFromBuilder . TE.encodeUtf8Builder . NET.unNonEmptyText
{-# INLINE encJFromNonEmptyText #-}
encJFromBool :: Bool -> EncJSON
encJFromBool = \case
False -> encJFromText "false"
True -> encJFromText "true"
{-# INLINE encJFromBool #-}
encJFromList :: [EncJSON] -> EncJSON
encJFromList =
encJFromBuilder . \case
[] -> "[]"
x : xs -> "[" <> unEncJSON x <> foldr go "]" xs
where
go v b = "," <> unEncJSON v <> b
-- from association list
encJFromAssocList :: [(Text, EncJSON)] -> EncJSON
encJFromAssocList =
encJFromBuilder . \case
[] -> "{}"
x : xs -> "{" <> builder' x <> foldr go "}" xs
where
go v b = "," <> builder' v <> b
-- builds "key":value from (key,value)
builder' (t, v) = J.fromEncoding (J.text t) <> ":" <> unEncJSON v
encJFromInsOrdHashMap :: InsOrdHashMap Text EncJSON -> EncJSON
encJFromInsOrdHashMap = encJFromAssocList . OMap.toList
-- | Encode a 'JO.Value' as 'EncJSON'.
encJFromOrderedValue :: JO.Value -> EncJSON
encJFromOrderedValue = \case
JO.Object obj ->
encJFromAssocList $ (map . second) encJFromOrderedValue $ JO.toList obj
JO.Array vec ->
encJFromList $ map encJFromOrderedValue $ V.toList vec
JO.String s -> encJFromJValue s
JO.Number sci -> encJFromJValue sci
JO.Bool b -> encJFromJValue b
JO.Null -> encJFromJValue J.Null