From fb3a7333bcba539c40885b15b22192b8490b6e62 Mon Sep 17 00:00:00 2001 From: avallete Date: Sat, 20 Jun 2026 16:02:41 +0200 Subject: [PATCH] add: x-postgrest-typegen-metadata OpenAPI extension for typegen Opt-in `openapi-metadata` config that adds a top-level `x-postgrest-typegen-metadata` vendor extension to the OpenAPI output, carrying catalog-grade metadata not otherwise expressible in OpenAPI: object kind, identity/generated columns, the full relationship graph with cardinality, function return types (incl. OUT/TABLE columns), composite types and computed fields. This lets clients (e.g. @supabase/postgrest-typegen) generate faithful types from the OpenAPI document alone, with no database connection. Disabled by default; standard OpenAPI consumers ignore the extra x- key. --- CHANGELOG.md | 1 + docs/postgrest.dict | 4 + docs/references/api/openapi.rst | 13 + docs/references/configuration.rst | 15 ++ src/PostgREST/Config.hs | 3 + src/PostgREST/MainTx.hs | 2 +- src/PostgREST/Query/SqlFragment.hs | 17 +- src/PostgREST/Response/OpenAPI.hs | 237 +++++++++++++++++- src/PostgREST/SchemaCache.hs | 213 ++++++++++++++-- src/PostgREST/SchemaCache/Routine.hs | 49 ++-- src/PostgREST/SchemaCache/Table.hs | 26 +- ...est_schema_cache_snapshot[dbRoutines].yaml | 68 +++++ .../test_schema_cache_snapshot[dbTables].yaml | 38 +++ test/io/configs/expected/aliases.config | 1 + .../configs/expected/boolean-numeric.config | 1 + .../io/configs/expected/boolean-string.config | 1 + test/io/configs/expected/defaults.config | 1 + .../expected/jspath-str-op-dump1.config | 1 + .../expected/jspath-str-op-dump2.config | 1 + .../expected/jspath-str-op-dump3.config | 1 + .../expected/jspath-str-op-dump4.config | 1 + .../expected/jspath-str-op-dump5.config | 1 + ...efaults-with-db-other-authenticator.config | 1 + .../expected/no-defaults-with-db.config | 1 + test/io/configs/expected/no-defaults.config | 1 + test/io/configs/expected/types.config | 1 + test/io/configs/expected/utf-8.config | 1 + test/io/test_cli.py | 8 +- test/observability/ObsHelper.hs | 1 + test/spec/SpecHelper.hs | 1 + 30 files changed, 646 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575c86f68d..a1d904c108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. From versio - Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751 - Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805 - Add GHC runtime metrics to the metrics endpoint by @mkleczek in #4862 +- Add opt-in `openapi-metadata` config that emits an `x-postgrest-typegen-metadata` vendor extension in the OpenAPI output, carrying catalog-grade metadata (object kind, identity/generated columns, full relationships with cardinality, function return types, composite types, computed fields) for faithful client type generation ### Fixed diff --git a/docs/postgrest.dict b/docs/postgrest.dict index 324d51b888..baff9a974e 100644 --- a/docs/postgrest.dict +++ b/docs/postgrest.dict @@ -204,3 +204,7 @@ wfts www debouncing deduplicates +cardinality +lossy +Typegen +updatability diff --git a/docs/references/api/openapi.rst b/docs/references/api/openapi.rst index ec546eb729..c335adb452 100644 --- a/docs/references/api/openapi.rst +++ b/docs/references/api/openapi.rst @@ -49,6 +49,19 @@ Similarly, you can override the API title by commenting the schema. If you need to include the ``security`` and ``securityDefinitions`` options, set the :ref:`openapi-security-active` configuration to ``true``. +Typegen metadata extension +-------------------------- + +PostgREST's OpenAPI is a name-based description of the API surface, which is lossy for client type generation (it cannot distinguish tables from views, identity columns, function return types, composite types, etc.). To support faithful type generation directly from the OpenAPI document, set ``openapi-metadata`` to ``true``. PostgREST then adds a top-level ``x-postgrest-typegen-metadata`` `vendor extension `_ to the output, carrying catalog-grade metadata not otherwise expressible in OpenAPI: + +- object kind (``table`` / ``view`` / ``materialized_view`` / ``foreign_table``) and view updatability +- per-column ``is_identity`` / ``identity_generation`` / ``is_generated`` and pg_catalog type names +- the full relationship graph with constraint names, cardinality (one-to-one detection) and view-expanded relationships +- function argument and return types (including set-returning detection and the returned relation), plus computed fields and computed relationships +- standalone composite types and enum types + +The extension is ``false`` by default, leaving the standard OpenAPI output unchanged. Standard OpenAPI consumers ignore unknown ``x-`` keys. This extension is consumed by `@supabase/postgrest-typegen `_'s ``openApiToGeneratorMetadata`` producer. + You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dashboard. The dashboard allows developers to make requests against a live PostgREST server, and provides guidance with request headers and example request bodies. .. important:: diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 48e1a92753..ab65d96878 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -765,6 +765,21 @@ log-query Logs the SQL query for the corresponding request at the current :ref:`log-level`. See :ref:`sql_query_logs`. +.. _openapi-metadata: + +openapi-metadata +---------------- + + =============== ================================= + **Type** Boolean + **Default** False + **Reloadable** Y + **Environment** PGRST_OPENAPI_METADATA + **In-Database** pgrst.openapi_metadata + =============== ================================= + + When enabled, adds a top-level ``x-postgrest-typegen-metadata`` vendor extension to the :ref:`OpenAPI output ` carrying catalog-grade metadata (object kind, identity/generated columns, full relationships with cardinality, function return types, composite types and computed fields) for faithful client type generation. Disabled by default; standard OpenAPI consumers ignore the extra ``x-`` key. + .. _openapi-mode: openapi-mode diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 47065856a3..2cd0d06a2c 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -115,6 +115,7 @@ data AppConfig = AppConfig , configOpenApiMode :: OpenAPIMode , configOpenApiSecurityActive :: Bool , configOpenApiServerProxyUri :: Maybe Text + , configOpenApiMetadata :: Bool , configServerCorsAllowedOrigins :: Maybe [Text] , configServerHost :: Text , configServerPort :: Int @@ -198,6 +199,7 @@ toText conf = ,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode) ,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) + ,("openapi-metadata", T.toLower . show . configOpenApiMetadata) ,("server-cors-allowed-origins", q . maybe "" (T.intercalate ",") . configServerCorsAllowedOrigins) ,("server-host", q . configServerHost) ,("server-port", show . configServerPort) @@ -313,6 +315,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl = <*> parseOpenAPIMode "openapi-mode" <*> (fromMaybe False <$> optBool "openapi-security-active") <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" + <*> (fromMaybe False <$> optBool "openapi-metadata") <*> parseCORSAllowedOrigins "server-cors-allowed-origins" <*> (defaultServerHost <$> optString "server-host") <*> parseServerPort "server-port" diff --git a/src/PostgREST/MainTx.hs b/src/PostgREST/MainTx.hs index e5b3146717..1cba874e9c 100644 --- a/src/PostgREST/MainTx.hs +++ b/src/PostgREST/MainTx.hs @@ -185,7 +185,7 @@ actionResult MainQuery{mqOpenAPI=(tblsQ, funcsQ, schQ)} (MayUseDb plan@InspectPl case configOpenApiMode of OAFollowPriv -> do tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ decodeAccessibleIdentifiers configDbPreparedStatements - accFuncs <- SQL.statement mempty $ SQL.dynamicallyParameterized funcsQ SchemaCache.decodeFuncs configDbPreparedStatements + accFuncs <- SQL.statement mempty $ SQL.dynamicallyParameterized funcsQ (SchemaCache.decodeFuncs configOpenApiMetadata) configDbPreparedStatements schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements let tbls = HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache diff --git a/src/PostgREST/Query/SqlFragment.hs b/src/PostgREST/Query/SqlFragment.hs index 75cb298213..d312394d30 100644 --- a/src/PostgREST/Query/SqlFragment.hs +++ b/src/PostgREST/Query/SqlFragment.hs @@ -681,6 +681,18 @@ baseFuncSqlQuery = SQL.sql $ encodeUtf8 [trimming| WITH ORDINALITY AS _ (name, type, mode, idx) WHERE type IS NOT NULL -- only input arguments GROUP BY oid + ), + return_columns AS ( + -- OUT / TABLE return columns (mode 't'), name + pg_catalog short type. + SELECT + pp.oid, + array_agg((COALESCE(u.name, ''), rt.typname::text) ORDER BY u.idx) + FILTER (WHERE u.mode = 't') AS cols + FROM pg_proc pp + CROSS JOIN LATERAL unnest(pp.proallargtypes, pp.proargmodes, pp.proargnames) + WITH ORDINALITY AS u (typ_oid, mode, name, idx) + JOIN pg_type rt ON rt.oid = u.typ_oid + GROUP BY pp.oid ) SELECT pn.nspname AS proc_schema, @@ -698,9 +710,12 @@ baseFuncSqlQuery = SQL.sql $ encodeUtf8 [trimming| p.provolatile, p.provariadic > 0 as hasvariadic, 'ignored' AS transaction_isolation_level, - '{}'::text[] as kvs + '{}'::text[] as kvs, + p.prorows::float8 as rows, + coalesce(rc.cols, '{}') as return_columns FROM pg_proc p LEFT JOIN arguments a ON a.oid = p.oid + LEFT JOIN return_columns rc ON rc.oid = p.oid JOIN pg_namespace pn ON pn.oid = p.pronamespace JOIN base_types bt ON bt.oid = p.prorettype JOIN pg_type t ON t.oid = bt.base_type diff --git a/src/PostgREST/Response/OpenAPI.hs b/src/PostgREST/Response/OpenAPI.hs index 6f90f7ad55..120fd949e6 100644 --- a/src/PostgREST/Response/OpenAPI.hs +++ b/src/PostgREST/Response/OpenAPI.hs @@ -3,10 +3,14 @@ Module : PostgREST.OpenAPI Description : Generates the OpenAPI output -} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-} module PostgREST.Response.OpenAPI (encode) where +import Data.Aeson ((.=)) import qualified Data.Aeson as JSON +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy as LBS import qualified Data.HashMap.Strict as HM @@ -27,12 +31,15 @@ import PostgREST.Config (AppConfig (..), Proxy (..), isMalformedProxyUri, toURI) import PostgREST.MediaType import PostgREST.Network (escapeHostName) -import PostgREST.SchemaCache (SchemaCache (..)) +import PostgREST.SchemaCache (CompositeType (..), + ComputedField (..), + SchemaCache (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Relationship (Cardinality (..), Relationship (..), RelationshipsMap) import PostgREST.SchemaCache.Routine (FuncVolatility (..), + PgType (..), RetType (..), Routine (..), RoutineParam (..)) import PostgREST.SchemaCache.Table (Column (..), Table (..), @@ -43,15 +50,31 @@ import Protolude hiding (Proxy, get) encode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString encode versions conf sCache tables procs schemaDescription = - JSON.encode $ - postgrestSpec - versions - (dbRelationships sCache) - (concat $ HM.elems procs) - (snd <$> HM.toList tables) - (proxyUri conf) - schemaDescription - (configOpenApiSecurityActive conf) + JSON.encode withMetadata + where + tablesList = snd <$> HM.toList tables + routines = concat $ HM.elems procs + spec = JSON.toJSON $ + postgrestSpec + versions + (dbRelationships sCache) + routines + tablesList + (proxyUri conf) + schemaDescription + (configOpenApiSecurityActive conf) + -- Opt-in: inject the typegen metadata block as a top-level vendor extension. + -- swagger2 has no typed extensions field, so we post-process the JSON Object. + withMetadata + | configOpenApiMetadata conf = case spec of + JSON.Object o -> + JSON.Object $ + KM.insert (K.fromText "x-postgrest-typegen-metadata") + (typegenMetadata (dbRelationships sCache) routines tablesList + (dbCompositeTypes sCache) (dbComputedFields sCache)) + o + other -> other + | otherwise = spec makeMimeList :: [MediaType] -> MimeList makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs @@ -452,3 +475,197 @@ proxyUri AppConfig{..} = (proxyScheme, proxyHost, proxyPort, proxyPath) Nothing -> ("http", configServerHost, toInteger configServerPort, "/") + +-- | Build the `x-postgrest-typegen-metadata` block: a name-keyed projection of +-- the schema cache mirroring `@supabase/postgrest-typegen`'s GeneratorMetadata, +-- so types can be generated from the OpenAPI document alone. Opt-in via +-- `openapi-metadata`. Type names are emitted as pg_catalog short names +-- (int8, _text, user_status) to match the SQL introspection path. +typegenMetadata :: RelationshipsMap -> [Routine] -> [Table] -> [CompositeType] -> [ComputedField] -> JSON.Value +typegenMetadata relsMap routines tbls comps cfields = JSON.object + [ "schemas" .= [ JSON.object ["name" .= s] | s <- nubOn identity (map tableSchema tbls) ] + , "tables" .= map tableJ tbls + , "relationships" .= concatMap relJ allRels + , "functions" .= map snd (nubOn fst funcEntries) + , "composites" .= map compJ comps + , "enums" .= map enumJ enumsList + ] + where + -- The RelationshipsMap indexes each relationship under multiple keys, so + -- flattening yields duplicates — dedupe before emitting. + allRels = nubOn relKey (concat (HM.elems relsMap)) + relKey r = case r of + Relationship{relTable, relForeignTable, relCardinality} -> + T.intercalate "|" + [ "rel", qiSchema relTable, qiName relTable + , qiSchema relForeignTable, qiName relForeignTable, cardKey relCardinality ] + ComputedRelationship{relFunction, relTable, relForeignTable} -> + T.intercalate "|" + [ "crel", qiSchema relFunction, qiName relFunction + , qiName relTable, qiName relForeignTable ] + colsKey cols = T.intercalate "," (map fst cols) <> ">" <> T.intercalate "," (map snd cols) + cardKey c = case c of + M2O cons cols -> "m2o|" <> cons <> "|" <> colsKey cols + O2O cons cols _ -> "o2o|" <> cons <> "|" <> colsKey cols + O2M cons cols -> "o2m|" <> cons <> "|" <> colsKey cols + M2M _ -> "m2m" + + enumsList = nubOn (\(_, n, _) -> n) + [ (tableSchema t, colFormat c, colEnum c) + | t <- tbls, c <- tableColumnsList t, not (null (colEnum c)) ] + enumJ (s, n, vs) = JSON.object ["schema" .= s, "name" .= n, "values" .= vs] + + tableJ t = JSON.object + [ "schema" .= tableSchema t + , "name" .= tableName t + , "kind" .= tableKind t + , "updatable" .= tableUpdatable t -- drives view Insert/Update generation + , "columns" .= map (colJ (tableUpdatable t)) (tableColumnsList t) + ] + + -- Per-column updatability isn't tracked by PostgREST; fall back to the + -- relation-level flag (correct for tables and non-updatable views). + colJ upd c = JSON.object + [ "name" .= colName c + , "format" .= colFormat c + , "data_type" .= colType c + , "is_nullable" .= colNullable c + , "default_value" .= colDefault c + , "is_identity" .= isJust (colIdentityGeneration c) + , "identity_generation" .= colIdentityGeneration c + , "is_generated" .= colIsGenerated c + , "is_updatable" .= upd + , "enums" .= colEnum c + , "comment" .= colDescription c + ] + + relJ rel = case rel of + Relationship{relTable, relForeignTable, relCardinality} -> + case relCardinality of + M2O cons cols -> [mkRel relTable relForeignTable cons cols False] + -- Only the FK-owner side (isParent = False, per decodeRels). The + -- inverse O2O (isParent = True), the O2M inverse, and M2M are not + -- emitted as relationships by introspect(). + O2O cons cols isParent -> [mkRel relTable relForeignTable cons cols True | not isParent] + O2M _ _ -> [] + M2M _ -> [] + ComputedRelationship{} -> [] + + mkRel src tgt cons cols oneToOne = JSON.object + [ "constraint_name" .= cons + , "schema" .= qiSchema src + , "relation" .= qiName src + , "columns" .= map fst cols + , "is_one_to_one" .= oneToOne + , "referenced_schema" .= qiSchema tgt + , "referenced_relation" .= qiName tgt + , "referenced_columns" .= map snd cols + ] + + routineJ pd = JSON.object + [ "schema" .= pdSchema pd + , "name" .= pdName pd + , "argument_types" .= routineArgTypes pd + -- input args + OUT/TABLE return columns (mode "table") so the generator + -- can render `RETURNS TABLE(...)` columns instead of falling back to record. + , "args" .= (map argJ (pdParams pd) ++ map tableColArg (pdReturnColumns pd)) + , "return" .= retJ (pdReturnType pd) (pdRows pd) + , "volatility" .= volJ (pdVolatility pd) + ] + tableColArg (cname, ctype) = JSON.object + [ "name" .= cname, "type" .= ctype, "mode" .= ("table" :: Text), "has_default" .= False ] + argJ p = JSON.object + [ "name" .= ppName p + , "type" .= shortType (ppType p) + , "mode" .= (if ppVar p then "variadic" else "in" :: Text) + , "has_default" .= not (ppReq p) + ] + retJ rt rows = case rt of + Single pgt -> retObj pgt False rows + SetOf pgt -> retObj pgt True rows + retObj pgt isSet rows = JSON.object $ + [ "type" .= pgTypeName pgt + , "is_set" .= isSet + , "rows" .= rows + ] ++ [ "relation" .= relationOf pgt ] + relationOf pgt = case pgt of + Composite qi _ -> JSON.object ["schema" .= qiSchema qi, "name" .= qiName qi] + Scalar _ -> JSON.Null + pgTypeName pgt = case pgt of + Composite qi _ -> qiName qi + Scalar qi -> qiName qi + + cfieldJ cf = JSON.object + [ "schema" .= cfSchema cf + , "name" .= cfName cf + , "argument_types" .= cfTableName cf + , "args" .= [ tableRowArg (cfTableName cf) ] + , "return" .= JSON.object + [ "type" .= cfReturnType cf + , "is_set" .= cfIsSetOf cf + , "rows" .= cfRows cf + , "relation" .= if cfReturnIsRelation cf + then JSON.object ["schema" .= cfReturnSchema cf, "name" .= cfReturnType cf] + else JSON.Null + ] + , "volatility" .= volJ (cfVolatility cf) + ] + + -- All emitted functions, keyed by (schema, name, argument_types) and deduped + -- so a single named table-row arg function (present in both dbRoutines and + -- the computed-field/relationship sources) is emitted once. RPC routines win. + funcEntries = + [ ((pdSchema pd, pdName pd, routineArgTypes pd), routineJ pd) | pd <- routines ] + ++ [ ((cfSchema cf, cfName cf, cfTableName cf), cfieldJ cf) + | cf <- cfields + , (cfSchema cf, cfName cf, cfTableName cf) `notElem` routineSingleArgSigs ] + -- (schema, name, single-arg-type) of callable routines with exactly one arg: + -- used to skip the computed-field/relationship copy of a function that is + -- already a callable routine (introspect() lists it once, as the routine). + routineSingleArgSigs = + [ (pdSchema pd, pdName pd, shortType (ppType p)) + | pd <- routines, [p] <- [pdParams pd] ] + -- Mirror introspect()'s argument_types: bare type for unnamed args, "name + -- type" for named ones, so only unnamed single-table-arg functions match a + -- table name and attach as computed fields. + routineArgTypes pd = T.intercalate ", " + [ if ppName p == "" then shortType (ppType p) else ppName p <> " " <> shortType (ppType p) + | p <- pdParams pd ] + tableRowArg tname = JSON.object + [ "name" .= ("" :: Text), "type" .= tname, "mode" .= ("in" :: Text), "has_default" .= False ] + + compJ ct = JSON.object + [ "schema" .= ctSchema ct + , "name" .= ctName ct + , "attributes" .= [ JSON.object ["name" .= n, "type" .= ty] | (n, ty) <- ctAttributes ct ] + ] + + volJ v = case v of + Immutable -> "IMMUTABLE" :: Text + Stable -> "STABLE" + Volatile -> "VOLATILE" + +-- | Distinct-by-key, preserving first occurrence. +nubOn :: Eq b => (a -> b) -> [a] -> [a] +nubOn f = go [] + where + go _ [] = [] + go seen (x:xs) + | f x `elem` seen = go seen xs + | otherwise = x : go (f x : seen) xs + +-- | Map a pg regtype text (SQL spelling, possibly schema-qualified) to the +-- pg_catalog short type name the typegen generator expects (int8, _text, …). +shortType :: Text -> Text +shortType raw + | "[]" `T.isSuffixOf` raw = "_" <> shortType (T.dropEnd 2 raw) + | otherwise = maybe bare snd (find ((== bare) . fst) sqlToShort) + where + bare = T.takeWhileEnd (/= '.') raw + sqlToShort = + [ ("integer", "int4"), ("bigint", "int8"), ("smallint", "int2") + , ("boolean", "bool"), ("double precision", "float8"), ("real", "float4") + , ("character varying", "varchar"), ("character", "bpchar") + , ("timestamp with time zone", "timestamptz") + , ("timestamp without time zone", "timestamp") + , ("time with time zone", "timetz"), ("time without time zone", "time") ] diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index 997d16ab23..066b79b85b 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -20,6 +20,8 @@ These queries are executed once at startup or when PostgREST is reloaded. module PostgREST.SchemaCache ( SchemaCache(..) + , CompositeType(..) + , ComputedField(..) , TablesFuzzyIndex , querySchemaCache , showSummary @@ -90,10 +92,38 @@ data SchemaCache = SchemaCache -- Haskell lazy evaluation ensures it's only built on first use and memoized afterwards , dbTablesFuzzyIndex :: TablesFuzzyIndex , dbQueryTimings :: Maybe QueryTimings -- ^ cached time for the time each query took when debugging + -- The following two are only used by the opt-in typegen OpenAPI metadata + -- extension; they carry catalog facts not otherwise tracked by the cache. + , dbCompositeTypes :: [CompositeType] + , dbComputedFields :: [ComputedField] } deriving (Show) +-- | A standalone composite type (typtype = 'c'), with its attributes. Only +-- surfaced for the typegen OpenAPI metadata extension. +data CompositeType = CompositeType + { ctSchema :: Schema + , ctName :: Text + , ctAttributes :: [(Text, Text)] -- ^ (attribute name, pg_catalog short type name) + } deriving (Show, Eq, Generic, JSON.ToJSON) + +-- | A computed field: a function taking a single table/view row and returning a +-- scalar/array/composite (non-relation) value, selectable as a pseudo-column. +-- Not tracked by the normal routine/relationship caches; surfaced only for the +-- typegen OpenAPI metadata extension. +data ComputedField = ComputedField + { cfSchema :: Schema -- ^ schema of both the function and its target relation + , cfTableName :: Text -- ^ the row-type argument's relation name (the owning table) + , cfName :: Text -- ^ field name (= function name) + , cfReturnType :: Text -- ^ pg_catalog short type name of the return + , cfIsSetOf :: Bool + , cfVolatility :: FuncVolatility + , cfRows :: Maybe Double + , cfReturnSchema :: Schema -- ^ schema of the return type + , cfReturnIsRelation :: Bool -- ^ True if the return type is a table/view/composite row type + } deriving (Show, Eq, Generic, JSON.ToJSON) + instance JSON.ToJSON SchemaCache where - toJSON (SchemaCache tabs rels routs reps hdlers tzs _ _) = JSON.object [ + toJSON (SchemaCache tabs rels routs reps hdlers tzs _ _ _ _) = JSON.object [ "dbTables" .= JSON.toJSON tabs , "dbRelationships" .= JSON.toJSON rels , "dbRoutines" .= JSON.toJSON routs @@ -103,7 +133,7 @@ instance JSON.ToJSON SchemaCache where ] showSummary :: SchemaCache -> Text -showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _ _) = +showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _ _ _ _) = T.intercalate ", " [ show (HM.size tbls) <> " Relations" , show (HM.size rels) <> " Relationships" @@ -157,11 +187,18 @@ maxDbTablesForFuzzySearch = 500 querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache querySchemaCache conf@AppConfig{..} = do SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object - tabs <- sqlTimedStmt gucTbls conf allTables + tabs <- sqlTimedStmt gucTbls conf (allTables configOpenApiMetadata) keyDeps <- sqlTimedStmt gucKDeps conf allViewsKeyDependencies m2oRels <- sqlTimedStmt gucRels mempty allM2OandO2ORels - funcs <- sqlTimedStmt gucFuncs conf allFunctions + funcs <- sqlTimedStmt gucFuncs conf (allFunctions configOpenApiMetadata) cRels <- sqlTimedStmt gucCRels mempty allComputedRels + -- Untimed (debug timings machinery untouched): only used by the opt-in + -- typegen OpenAPI metadata extension. Gated on the flag so the resident + -- schema cache stays byte-for-byte baseline when the feature is off + -- (the extra fields/queries otherwise grow the cache ~1.7M and trip the + -- 50M-body memory ceiling even though the feature is unused). + comps <- if configOpenApiMetadata then SQL.statement conf allCompositeTypes else pure [] + cfields <- if configOpenApiMetadata then SQL.statement conf allComputedFields else pure [] reps <- sqlTimedStmt gucDReps conf dataRepresentations mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers tzones <- if configDbTimezoneEnabled @@ -192,6 +229,8 @@ querySchemaCache conf@AppConfig{..} = do -- Fuzzy.FuzzySet is memory heavy we just don't use it for large schemas Fuzzy.fromList <$> HM.filter ((< maxDbTablesForFuzzySearch) . length) (HM.fromListWith (<>) ((qiSchema &&& pure . qiName) <$> HM.keys tabsWViewsPks)) , dbQueryTimings = qsTime + , dbCompositeTypes = comps + , dbComputedFields = cfields } where schemas = toList configDbSchemas @@ -231,6 +270,8 @@ removeInternal schemas dbStruct = , dbTimezones = dbTimezones dbStruct , dbTablesFuzzyIndex = dbTablesFuzzyIndex dbStruct , dbQueryTimings = dbQueryTimings dbStruct + , dbCompositeTypes = dbCompositeTypes dbStruct + , dbComputedFields = dbComputedFields dbStruct } where hasInternalJunction ComputedRelationship{} = False @@ -238,8 +279,14 @@ removeInternal schemas dbStruct = M2M Junction{junTable} -> qiSchema junTable `notElem` schemas _ -> False -decodeTables :: HD.Result TablesMap -decodeTables = +-- | Decode the tables/columns. When @metaOn@ is False (the opt-in typegen +-- OpenAPI metadata feature is disabled) the three extra column fields and the +-- table kind are read off the wire but discarded into shared-empty values, so +-- the resident schema cache is byte-for-byte identical to the pre-feature +-- baseline (keeps the 50M-body memory ceiling intact). The columns are still +-- selected by 'tablesSqlQuery'; only retention differs. +decodeTables :: Bool -> HD.Result TablesMap +decodeTables metaOn = HM.fromList . map (\tbl@Table{tableSchema, tableName} -> (QualifiedIdentifier tableSchema tableName, tbl)) <$> HD.rowList tblRow where tblRow = Table @@ -260,7 +307,11 @@ decodeTables = <*> compositeField HD.text <*> nullableCompositeField HD.int4 <*> nullableCompositeField HD.text - <*> compositeFieldArray HD.text)) + <*> compositeFieldArray HD.text + <*> (if metaOn then nullableCompositeField HD.text else pure Nothing) + <*> (if metaOn then compositeField HD.bool else pure False) + <*> (if metaOn then compositeField HD.text else pure ""))) + <*> (if metaOn then column HD.text else pure "") parseCols :: HD.Row [Column] -> HD.Row ColumnMap @@ -300,8 +351,12 @@ viewKeyDepFromRow (s1,t1,s2,v2,cons,consType,sCols) = ViewKeyDependency (Qualifi | consType == "f" = FKDep | otherwise = FKDepRef -- f_ref, we build this type in the query -decodeFuncs :: HD.Result RoutineMap -decodeFuncs = +-- | Decode the routines. When @metaOn@ is False the two extra fields (rows +-- estimate and TABLE/OUT return columns) used only by the opt-in typegen +-- OpenAPI metadata feature are read off the wire but discarded, keeping the +-- resident schema cache byte-for-byte identical to the pre-feature baseline. +decodeFuncs :: Bool -> HD.Result RoutineMap +decodeFuncs metaOn = -- Duplicate rows for a function means they're overloaded, order these by least args according to Routine Ord instance map sort . HM.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList funcRow where @@ -326,6 +381,8 @@ decodeFuncs = <*> column HD.bool <*> nullableColumn (toIsolationLevel <$> HD.text) <*> compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text) -- function setting + <*> (if metaOn then nullableColumn HD.float8 else pure Nothing) + <*> (if metaOn then compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text) else pure []) -- return columns addKey :: Routine -> (QualifiedIdentifier, Routine) addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd) @@ -381,13 +438,97 @@ dataRepresentations = SQL.Statement sql mempty decodeRepresentations True OR (dst_t.typtype = 'd' AND c.castsource IN ('json'::regtype::oid , 'text'::regtype::oid))) |] -allFunctions :: SQL.Statement AppConfig RoutineMap -allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs True +allFunctions :: Bool -> SQL.Statement AppConfig RoutineMap +allFunctions metaOn = SQL.Statement (funcsSqlQuery metaOn) params (decodeFuncs metaOn) True where params = (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <> (configDbHoistedTxSettings >$< arrayParam HE.text) +-- | Standalone composite types (typtype = 'c') with their attributes. Only used +-- by the opt-in typegen OpenAPI metadata extension. +allCompositeTypes :: SQL.Statement AppConfig [CompositeType] +allCompositeTypes = SQL.Statement sql params decodeCompositeTypes True + where + params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text + sql = encodeUtf8 [trimming| + SELECT + n.nspname::text AS schema, + t.typname::text AS name, + COALESCE( + array_agg(row(a.attname, att.typname) ORDER BY a.attnum) + FILTER (WHERE a.attname IS NOT NULL AND NOT a.attisdropped), + '{}' + ) AS attributes + FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + JOIN pg_class c ON c.oid = t.typrelid + LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 + LEFT JOIN pg_type att ON att.oid = a.atttypid + WHERE c.relkind = 'c' + AND t.typnamespace = ANY($$1::regnamespace[]) + GROUP BY n.nspname, t.typname + ORDER BY n.nspname, t.typname|] + +decodeCompositeTypes :: HD.Result [CompositeType] +decodeCompositeTypes = HD.rowList $ + CompositeType + <$> column HD.text + <*> column HD.text + <*> compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text) + +-- | Computed fields/relationships: single relation-row-arg functions, returning +-- any type (scalar/array/composite/relation). These are not captured by the +-- routine cache (when the arg is unnamed) and carry the proretset/prorows/return +-- relation detail the relationship cache lacks; only used by the typegen +-- OpenAPI metadata extension. +allComputedFields :: SQL.Statement AppConfig [ComputedField] +allComputedFields = SQL.Statement sql params decodeComputedFields True + where + params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text + sql = encodeUtf8 [trimming| + WITH all_relations AS ( + SELECT reltype FROM pg_class WHERE relkind IN ('v','r','m','f','p') + ) + SELECT + pn.nspname::text AS schema, + arg_t.typname::text AS table_name, + p.proname::text AS name, + ret_t.typname::text AS return_type, + p.proretset AS is_setof, + p.provolatile AS volatility, + p.prorows::float8 AS rows, + ret_schema.nspname::text AS return_schema, + ret_t.typrelid <> 0 AS return_is_relation + FROM pg_proc p + JOIN pg_namespace pn ON pn.oid = p.pronamespace + JOIN pg_type arg_t ON arg_t.oid = p.proargtypes[0] + JOIN pg_type ret_t ON ret_t.oid = p.prorettype + JOIN pg_namespace ret_schema ON ret_schema.oid = ret_t.typnamespace + WHERE p.pronargs = 1 + AND p.prokind = 'f' + AND p.proargtypes[0] IN (SELECT reltype FROM all_relations) + AND p.pronamespace = ANY($$1::regnamespace[]) + ORDER BY pn.nspname, arg_t.typname, p.proname|] + +decodeComputedFields :: HD.Result [ComputedField] +decodeComputedFields = HD.rowList $ + ComputedField + <$> column HD.text + <*> column HD.text + <*> column HD.text + <*> column HD.text + <*> column HD.bool + <*> (parseVol <$> column HD.char) + <*> nullableColumn HD.float8 + <*> column HD.text + <*> column HD.bool + where + parseVol :: Char -> FuncVolatility + parseVol v | v == 'i' = Immutable + | v == 's' = Stable + | otherwise = Volatile + baseTypesCte :: Text baseTypesCte = [trimming| -- Recursively get the base types of domains @@ -418,8 +559,8 @@ baseTypesCte = [trimming| ) |] -funcsSqlQuery :: SqlQuery -funcsSqlQuery = encodeUtf8 [trimming| +funcsSqlQuery :: Bool -> SqlQuery +funcsSqlQuery metaOn = encodeUtf8 [trimming| WITH $baseTypesCte, arguments AS ( @@ -448,7 +589,7 @@ funcsSqlQuery = encodeUtf8 [trimming| WITH ORDINALITY AS _ (name, type, mode, idx) WHERE type IS NOT NULL -- only input arguments GROUP BY oid - ) + )$returnColumnsCte SELECT pn.nspname AS proc_schema, p.proname AS proc_name, @@ -465,9 +606,9 @@ funcsSqlQuery = encodeUtf8 [trimming| p.provolatile, p.provariadic > 0 as hasvariadic, lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level, - coalesce(func_settings.kvs, '{}') as kvs + coalesce(func_settings.kvs, '{}') as kvs$rowsReturnCols FROM pg_proc p - LEFT JOIN arguments a ON a.oid = p.oid + LEFT JOIN arguments a ON a.oid = p.oid$returnColumnsJoin JOIN pg_namespace pn ON pn.oid = p.pronamespace JOIN base_types bt ON bt.oid = p.prorettype JOIN pg_type t ON t.oid = bt.base_type @@ -487,6 +628,18 @@ funcsSqlQuery = encodeUtf8 [trimming| WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true) AND prokind = 'f' AND p.pronamespace = ANY($$1::regnamespace[]) |] + where + -- When the typegen metadata feature is off, the OUT/TABLE return-columns CTE + -- (which walks every pg_proc, including all pg_catalog builtins) and the + -- rows/return_columns output columns are omitted entirely, so the + -- schema-cache load allocates at the pre-feature baseline (the decoder + -- mirrors this by only reading these fields when the feature is on). + returnColumnsCte, rowsReturnCols, returnColumnsJoin :: Text + returnColumnsCte = if metaOn + then ",\n return_columns AS (\n SELECT\n pp.oid,\n array_agg((COALESCE(u.name, ''), rt.typname::text) ORDER BY u.idx)\n FILTER (WHERE u.mode = 't') AS cols\n FROM pg_proc pp\n CROSS JOIN LATERAL unnest(pp.proallargtypes, pp.proargmodes, pp.proargnames)\n WITH ORDINALITY AS u (typ_oid, mode, name, idx)\n JOIN pg_type rt ON rt.oid = u.typ_oid\n GROUP BY pp.oid\n )" + else "" + rowsReturnCols = if metaOn then ",\n p.prorows::float8 as rows,\n coalesce(rc.cols, '{}') as return_columns" else "" + returnColumnsJoin = if metaOn then "\n LEFT JOIN return_columns rc ON rc.oid = p.oid" else "" {- Adds M2O and O2O relationships for views to tables, tables to views, and views to views. The example below is taken from the test fixtures, but the views names/colnames were modified. @@ -594,14 +747,14 @@ addViewPrimaryKeys tabs keyDeps = takeFirstPK = mapMaybe (head . snd) indexedDeps = HM.fromListWith (++) $ fmap ((keyDepType &&& keyDepView) &&& pure) keyDeps -allTables :: SQL.Statement AppConfig TablesMap -allTables = SQL.Statement tablesSqlQuery params decodeTables True +allTables :: Bool -> SQL.Statement AppConfig TablesMap +allTables metaOn = SQL.Statement (tablesSqlQuery metaOn) params (decodeTables metaOn) True where params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text -- | Gets tables with their PK cols -tablesSqlQuery :: SqlQuery -tablesSqlQuery = +tablesSqlQuery :: Bool -> SqlQuery +tablesSqlQuery metaOn = -- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views, -- we cannot use those directly as they include the following privilege filter: -- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text)); @@ -641,7 +794,7 @@ tablesSqlQuery = information_schema._pg_truetypmod(a.*, t.*) )::integer AS character_maximum_length, bt.base_type, - a.attnum::integer AS position + a.attnum::integer AS position$extraColumnCols FROM pg_attribute a LEFT JOIN pg_description AS d ON d.objoid = a.attrelid and d.objsubid = a.attnum and d.classoid = 'pg_class'::regclass @@ -653,6 +806,7 @@ tablesSqlQuery = ON a.atttypid = t.oid LEFT JOIN base_types bt ON t.oid = bt.oid + $basePtJoin LEFT JOIN pg_depend seq ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i' WHERE @@ -676,7 +830,7 @@ tablesSqlQuery = coalesce( (SELECT array_agg(enumlabel ORDER BY enumsortorder) FROM pg_enum WHERE enumtypid = base_type), '{}' - ) + )$extraRowFields ) order by position) as columns FROM columns GROUP BY relid @@ -730,7 +884,7 @@ tablesSqlQuery = ) ) AS deletable, coalesce(tpks.pk_cols, '{}') as pk_cols, - coalesce(cols_agg.columns, '{}') as columns + coalesce(cols_agg.columns, '{}') as columns$tableKindCol FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_description d on d.objoid = c.oid and d.objsubid = 0 and d.classoid = 'pg_class'::regclass @@ -740,6 +894,19 @@ tablesSqlQuery = AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace) AND not c.relispartition ORDER BY table_schema, table_name|] + where + -- When the typegen metadata feature is off, the extra per-column fields, the + -- object kind and the base-type join are omitted entirely (not just blanked): + -- the columns are not selected, decoded or aggregated, so the schema-cache + -- load allocates exactly at the pre-feature baseline (the decoder mirrors + -- this by only reading these fields when the feature is on). + extraColumnCols, extraRowFields, basePtJoin, tableKindCol :: Text + extraColumnCols = if metaOn + then ",\n CASE a.attidentity WHEN 'a' THEN 'ALWAYS' WHEN 'd' THEN 'BY DEFAULT' ELSE NULL END AS identity_generation,\n a.attgenerated = 's' AS is_generated,\n COALESCE(base_pt.typname, t.typname)::text AS format" + else "" + extraRowFields = if metaOn then ",\n identity_generation,\n is_generated,\n format" else "" + basePtJoin = if metaOn then "LEFT JOIN pg_type base_pt ON base_pt.oid = bt.base_type" else "" + tableKindCol = if metaOn then ",\n CASE c.relkind WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized_view' WHEN 'f' THEN 'foreign_table' ELSE 'table' END as table_kind" else "" -- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's allM2OandO2ORels :: SQL.Statement () [Relationship] diff --git a/src/PostgREST/SchemaCache/Routine.hs b/src/PostgREST/SchemaCache/Routine.hs index 674c21dee1..67e13f2710 100644 --- a/src/PostgREST/SchemaCache/Routine.hs +++ b/src/PostgREST/SchemaCache/Routine.hs @@ -52,29 +52,38 @@ data FuncVolatility type FuncSettings = [(Text,Text)] data Routine = Function - { pdSchema :: Schema - , pdName :: Text - , pdDescription :: Maybe Text - , pdParams :: [RoutineParam] - , pdReturnType :: RetType - , pdVolatility :: FuncVolatility - , pdHasVariadic :: Bool - , pdIsoLvl :: Maybe SQL.IsolationLevel - , pdFuncSettings :: FuncSettings + { pdSchema :: Schema + , pdName :: Text + , pdDescription :: Maybe Text + , pdParams :: [RoutineParam] + , pdReturnType :: RetType + , pdVolatility :: FuncVolatility + , pdHasVariadic :: Bool + , pdIsoLvl :: Maybe SQL.IsolationLevel + , pdFuncSettings :: FuncSettings + -- `prorows` estimate (set-returning functions); surfaced for the typegen + -- OpenAPI metadata extension so generated RPC return types know cardinality. + , pdRows :: Maybe Double + -- OUT/TABLE return columns (name, pg_catalog short type) for RETURNS TABLE / + -- OUT-param functions; surfaced for the typegen OpenAPI metadata extension so + -- the generated return type lists the columns instead of falling back to record. + , pdReturnColumns :: [(Text, Text)] } deriving (Eq, Show, Generic) -- need to define JSON manually bc SQL.IsolationLevel doesn't have a JSON instance(and we can't define one for that type without getting a compiler error) instance JSON.ToJSON Routine where - toJSON (Function sch nam desc params ret vol hasVar _ sets) = JSON.object + toJSON (Function sch nam desc params ret vol hasVar _ sets rows retCols) = JSON.object [ - "pdSchema" .= sch - , "pdName" .= nam - , "pdDescription" .= desc - , "pdParams" .= JSON.toJSON params - , "pdReturnType" .= JSON.toJSON ret - , "pdVolatility" .= JSON.toJSON vol - , "pdHasVariadic" .= JSON.toJSON hasVar - , "pdFuncSettings" .= JSON.toJSON sets + "pdSchema" .= sch + , "pdName" .= nam + , "pdDescription" .= desc + , "pdParams" .= JSON.toJSON params + , "pdReturnType" .= JSON.toJSON ret + , "pdVolatility" .= JSON.toJSON vol + , "pdHasVariadic" .= JSON.toJSON hasVar + , "pdFuncSettings" .= JSON.toJSON sets + , "pdRows" .= JSON.toJSON rows + , "pdReturnColumns" .= JSON.toJSON retCols ] data RoutineParam = RoutineParam @@ -88,10 +97,10 @@ data RoutineParam = RoutineParam -- Order by least number of params in the case of overloaded functions instance Ord Routine where - Function schema1 name1 des1 prms1 rt1 vol1 hasVar1 iso1 sets1 `compare` Function schema2 name2 des2 prms2 rt2 vol2 hasVar2 iso2 sets2 + Function schema1 name1 des1 prms1 rt1 vol1 hasVar1 iso1 sets1 rows1 retCols1 `compare` Function schema2 name2 des2 prms2 rt2 vol2 hasVar2 iso2 sets2 rows2 retCols2 | schema1 == schema2 && name1 == name2 && length prms1 < length prms2 = LT | schema1 == schema2 && name1 == name2 && length prms1 > length prms2 = GT - | otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1, iso1, sets1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2, iso2, sets2) + | otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1, iso1, sets1, rows1, retCols1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2, iso2, sets2, rows2, retCols2) -- | A map of all procs, all of which can be overloaded(one entry will have more than one Routine). -- | It uses a HashMap for a faster lookup. diff --git a/src/PostgREST/SchemaCache/Table.hs b/src/PostgREST/SchemaCache/Table.hs index e2a16ee7d8..c461c21048 100644 --- a/src/PostgREST/SchemaCache/Table.hs +++ b/src/PostgREST/SchemaCache/Table.hs @@ -33,6 +33,10 @@ data Table = Table , tableDeletable :: Bool , tablePKCols :: [FieldName] , tableColumns :: ColumnMap + -- Relation kind: "table" | "view" | "materialized_view" | "foreign_table". + -- Surfaced for the typegen OpenAPI metadata extension; `tableIsView` stays + -- for the existing view/table branching. + , tableKind :: Text } deriving (Show, Generic, JSON.ToJSON) @@ -43,14 +47,20 @@ instance Eq Table where Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2 data Column = Column - { colName :: FieldName - , colDescription :: Maybe Text - , colNullable :: Bool - , colType :: Text - , colNominalType :: Text - , colMaxLen :: Maybe Int32 - , colDefault :: Maybe Text - , colEnum :: [Text] + { colName :: FieldName + , colDescription :: Maybe Text + , colNullable :: Bool + , colType :: Text + , colNominalType :: Text + , colMaxLen :: Maybe Int32 + , colDefault :: Maybe Text + , colEnum :: [Text] + -- Identity / generated metadata for the typegen OpenAPI extension. + -- `colIdentityGeneration` is "ALWAYS" | "BY DEFAULT" | Nothing. + , colIdentityGeneration :: Maybe Text + , colIsGenerated :: Bool + -- pg_catalog short type name (int8, _text, user_status); typegen extension only. + , colFormat :: Text } deriving (Eq, Show, Ord, Generic, JSON.ToJSON) diff --git a/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRoutines].yaml b/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRoutines].yaml index 7063ea43bb..6a13448e1f 100644 --- a/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRoutines].yaml +++ b/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbRoutines].yaml @@ -7,6 +7,7 @@ pdHasVariadic: false pdName: rpc_with_two_hoisted pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -15,6 +16,7 @@ - false tag: Composite tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -30,6 +32,7 @@ ppType: text ppTypeMaxLength: text ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -37,6 +40,7 @@ qiSchema: pg_catalog tag: Scalar tag: SetOf + pdRows: 1000 pdSchema: public pdVolatility: Volatile @@ -47,6 +51,7 @@ pdHasVariadic: false pdName: default_isolation_level pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -54,6 +59,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -69,6 +75,7 @@ ppType: text ppTypeMaxLength: text ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -76,6 +83,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -96,6 +104,7 @@ ppType: integer ppTypeMaxLength: integer ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -103,6 +112,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -113,6 +123,7 @@ pdHasVariadic: false pdName: notify_pgrst pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -120,6 +131,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -130,6 +142,7 @@ pdHasVariadic: false pdName: migrate_function pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -137,6 +150,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -152,6 +166,7 @@ ppType: text ppTypeMaxLength: text ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -159,6 +174,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -169,6 +185,7 @@ pdHasVariadic: false pdName: reset_max_rows_config pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -176,6 +193,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -186,6 +204,7 @@ pdHasVariadic: false pdName: reset_invalid_role_claim_key pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -193,6 +212,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -203,6 +223,7 @@ pdHasVariadic: false pdName: custom_vary_hdr pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -210,6 +231,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -220,6 +242,7 @@ pdHasVariadic: false pdName: get_postgres_version pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -227,6 +250,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -237,6 +261,7 @@ pdHasVariadic: false pdName: do_nothing pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -244,6 +269,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -259,6 +285,7 @@ ppType: text ppTypeMaxLength: text ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -266,6 +293,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -278,6 +306,7 @@ pdHasVariadic: false pdName: one_sec_timeout pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -285,6 +314,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -295,6 +325,7 @@ pdHasVariadic: false pdName: reload_pgrst_config pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -302,6 +333,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -314,6 +346,7 @@ pdHasVariadic: false pdName: rpc_with_one_hoisted pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -322,6 +355,7 @@ - false tag: Composite tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -332,6 +366,7 @@ pdHasVariadic: false pdName: hello pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -339,6 +374,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -349,6 +385,7 @@ pdHasVariadic: false pdName: change_db_schemas_config pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -356,6 +393,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -371,6 +409,7 @@ ppType: double precision ppTypeMaxLength: double precision ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -378,6 +417,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -390,6 +430,7 @@ pdHasVariadic: false pdName: serializable_isolation_level pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -397,6 +438,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -407,6 +449,7 @@ pdHasVariadic: false pdName: drop_change_cats pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -414,6 +457,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -434,6 +478,7 @@ ppType: boolean ppTypeMaxLength: boolean ppVar: false + pdReturnColumns: [] pdReturnType: contents: contents: @@ -441,6 +486,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -451,6 +497,7 @@ pdHasVariadic: false pdName: invalid_role_claim_key_reload pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -458,6 +505,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -468,6 +516,7 @@ pdHasVariadic: false pdName: reset_db_schemas_config pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -475,6 +524,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -485,6 +535,7 @@ pdHasVariadic: false pdName: root pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -492,6 +543,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -502,6 +554,7 @@ pdHasVariadic: false pdName: 'true' pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -509,6 +562,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -519,6 +573,7 @@ pdHasVariadic: false pdName: create_function pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -526,6 +581,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -536,6 +592,7 @@ pdHasVariadic: false pdName: get_pgrst_version pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -543,6 +600,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -553,6 +611,7 @@ pdHasVariadic: false pdName: uses_prepared_statements pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -560,6 +619,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -572,6 +632,7 @@ pdHasVariadic: false pdName: repeatable_read_isolation_level pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -579,6 +640,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -589,6 +651,7 @@ pdHasVariadic: false pdName: notify_do_nothing pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -596,6 +659,7 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -606,6 +670,7 @@ pdHasVariadic: false pdName: rpc_work_mem pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -614,6 +679,7 @@ - false tag: Composite tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile @@ -626,6 +692,7 @@ pdHasVariadic: false pdName: four_sec_timeout pdParams: [] + pdReturnColumns: [] pdReturnType: contents: contents: @@ -633,5 +700,6 @@ qiSchema: pg_catalog tag: Scalar tag: Single + pdRows: 0 pdSchema: public pdVolatility: Volatile diff --git a/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTables].yaml b/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTables].yaml index 1f1ac761e6..80d6d23f42 100644 --- a/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTables].yaml +++ b/test/io/__snapshots__/test_cli/test_schema_cache_snapshot[dbTables].yaml @@ -5,6 +5,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: authors_only tablePKCols: [] tableSchema: public @@ -17,6 +18,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: uuid + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: id colNominalType: uuid @@ -26,6 +30,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: text + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: name colNominalType: text @@ -35,6 +42,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: cats tablePKCols: - id @@ -48,6 +56,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: int4 + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: id colNominalType: integer @@ -57,6 +68,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: text + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: isolation_level colNominalType: text @@ -66,6 +80,7 @@ tableDescription: null tableInsertable: true tableIsView: true + tableKind: view tableName: items_w_isolation_level tablePKCols: [] tableSchema: public @@ -78,6 +93,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: int4 + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: id colNominalType: integer @@ -87,6 +105,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: text + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: name colNominalType: text @@ -96,6 +117,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: directors tablePKCols: - id @@ -109,6 +131,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: projects tablePKCols: [] tableSchema: public @@ -121,6 +144,7 @@ tableDescription: null tableInsertable: false tableIsView: true + tableKind: view tableName: infinite_recursion tablePKCols: [] tableSchema: public @@ -133,6 +157,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: int4 + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: director_id colNominalType: integer @@ -142,6 +169,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: int4 + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: id colNominalType: integer @@ -151,6 +181,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: text + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: title colNominalType: text @@ -160,6 +193,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: films tablePKCols: - id @@ -173,6 +207,9 @@ colDefault: null colDescription: null colEnum: [] + colFormat: int4 + colIdentityGeneration: null + colIsGenerated: false colMaxLen: null colName: id colNominalType: integer @@ -182,6 +219,7 @@ tableDescription: null tableInsertable: true tableIsView: false + tableKind: table tableName: items tablePKCols: [] tableSchema: public diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index d280c6254c..32a9d297db 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index 7f1ac07a14..ff07d298bf 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index 7f1ac07a14..ff07d298bf 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index ddd9364c28..c59e73595c 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/jspath-str-op-dump1.config b/test/io/configs/expected/jspath-str-op-dump1.config index 25fd233463..3aa4f546e4 100644 --- a/test/io/configs/expected/jspath-str-op-dump1.config +++ b/test/io/configs/expected/jspath-str-op-dump1.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/jspath-str-op-dump2.config b/test/io/configs/expected/jspath-str-op-dump2.config index b53bf827ed..5d158409d5 100644 --- a/test/io/configs/expected/jspath-str-op-dump2.config +++ b/test/io/configs/expected/jspath-str-op-dump2.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/jspath-str-op-dump3.config b/test/io/configs/expected/jspath-str-op-dump3.config index bd1bed9936..3cc1e66d73 100644 --- a/test/io/configs/expected/jspath-str-op-dump3.config +++ b/test/io/configs/expected/jspath-str-op-dump3.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/jspath-str-op-dump4.config b/test/io/configs/expected/jspath-str-op-dump4.config index b169f03a22..349279648d 100644 --- a/test/io/configs/expected/jspath-str-op-dump4.config +++ b/test/io/configs/expected/jspath-str-op-dump4.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/jspath-str-op-dump5.config b/test/io/configs/expected/jspath-str-op-dump5.config index 13596cc813..a0b934cfe7 100644 --- a/test/io/configs/expected/jspath-str-op-dump5.config +++ b/test/io/configs/expected/jspath-str-op-dump5.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index c47bb402ed..d8e220400c 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -31,6 +31,7 @@ log-query = true openapi-mode = "disabled" openapi-security-active = false openapi-server-proxy-uri = "https://otherexample.org/api" +openapi-metadata = false server-cors-allowed-origins = "http://otherorigin.com" server-host = "0.0.0.0" server-port = 80 diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 4363f3262c..e3e4d6ed9e 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -31,6 +31,7 @@ log-query = true openapi-mode = "ignore-privileges" openapi-security-active = true openapi-server-proxy-uri = "https://example.org/api" +openapi-metadata = false server-cors-allowed-origins = "http://origin.com" server-host = "0.0.0.0" server-port = 80 diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index 3156287737..d5259f5458 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -31,6 +31,7 @@ log-query = true openapi-mode = "ignore-privileges" openapi-security-active = true openapi-server-proxy-uri = "https://postgrest.org" +openapi-metadata = false server-cors-allowed-origins = "http://example.com" server-host = "0.0.0.0" server-port = 80 diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index fac4d596df..2447864aa4 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/configs/expected/utf-8.config b/test/io/configs/expected/utf-8.config index 7f29a498c9..cde856c9ff 100644 --- a/test/io/configs/expected/utf-8.config +++ b/test/io/configs/expected/utf-8.config @@ -31,6 +31,7 @@ log-query = false openapi-mode = "follow-privileges" openapi-security-active = false openapi-server-proxy-uri = "" +openapi-metadata = false server-cors-allowed-origins = "" server-host = "!4" server-port = 3000 diff --git a/test/io/test_cli.py b/test/io/test_cli.py index 72351fc356..94a69643a3 100644 --- a/test/io/test_cli.py +++ b/test/io/test_cli.py @@ -256,7 +256,13 @@ def test_invalid_openapi_mode(invalidopenapimodes, defaultenv): def test_schema_cache_snapshot(baseenv, key, snapshot_yaml): "Dump of schema cache should match snapshot." - schema_cache = yaml.load(cli(["--dump-schema"], env=baseenv), Loader=yaml.Loader) + # Dump with the opt-in typegen OpenAPI metadata feature ON so the snapshot + # exercises the enriched schema cache (object kind, identity/generated + # columns, function rows/return columns). With the feature OFF those fields + # are intentionally discarded to keep the resident cache at the baseline + # memory footprint, so they would not appear in the dump. + env = {**baseenv, "PGRST_OPENAPI_METADATA": "true"} + schema_cache = yaml.load(cli(["--dump-schema"], env=env), Loader=yaml.Loader) formatted = yaml.dump( schema_cache[key], encoding="utf8", diff --git a/test/observability/ObsHelper.hs b/test/observability/ObsHelper.hs index 268bdbf67c..a34050c740 100644 --- a/test/observability/ObsHelper.hs +++ b/test/observability/ObsHelper.hs @@ -105,6 +105,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in , configOpenApiMode = OAFollowPriv , configOpenApiSecurityActive = False , configOpenApiServerProxyUri = Nothing + , configOpenApiMetadata = False , configServerCorsAllowedOrigins = Nothing , configServerHost = "localhost" , configServerPort = 3000 diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index 86e0ab547a..f0eae05665 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -146,6 +146,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in , configOpenApiMode = OAFollowPriv , configOpenApiSecurityActive = False , configOpenApiServerProxyUri = Nothing + , configOpenApiMetadata = False , configServerCorsAllowedOrigins = Nothing , configServerHost = "localhost" , configServerPort = 3000