diff --git a/src/library/PostgREST/Response/OpenAPI.hs b/src/library/PostgREST/Response/OpenAPI.hs index 13065e075..b2548295d 100644 --- a/src/library/PostgREST/Response/OpenAPI.hs +++ b/src/library/PostgREST/Response/OpenAPI.hs @@ -132,13 +132,13 @@ m2mMarkers tbl rels = mapMaybe m2mMarker searchedRels Just $ T.intercalate "" [ "" ] m2mMarker _ = Nothing - junctionSourceCol junction = maybe mempty snd (headMay $ junColsSource junction) - junctionTargetCol junction = maybe mempty snd (headMay $ junColsTarget junction) + junctionSourceCols junction = T.intercalate "," (snd <$> junColsSource junction) + junctionTargetCols junction = T.intercalate "," (snd <$> junColsTarget junction) accessibleCols :: Table -> [FieldName] -> [Column] accessibleCols t cols = filter ((`elem` cols) . colName) (tableColumnsList t) @@ -147,23 +147,32 @@ makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema) makeProperty tbl rels col = (colName col, Inline s) where e = if null $ colEnum col then Nothing else JSON.decode $ JSON.encode $ colEnum col - fk :: Maybe Text - fk = + fks :: [Text] + fks = let searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels -- Sorts the relationship list to get tables first relsSortedByIsView = sortOn relFTableIsView [ r | r@Relationship{} <- searchedRels] - -- Finds the relationship that has a single column foreign key - rel = find (\case - Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns) - Relationship{relCardinality=(O2O _ relColumns False)} -> [colName col] == (fst <$> relColumns) + -- Finds the relationships that have this column among their foreign key columns + relsMatching = filter (\case + Relationship{relCardinality=(M2O _ relColumns)} -> colName col `elem` (fst <$> relColumns) + Relationship{relCardinality=(O2O _ relColumns False)} -> colName col `elem` (fst <$> relColumns) _ -> False ) relsSortedByIsView - fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel) - fTbl = qiName . relForeignTable <$> rel - fTblCol = (,) <$> fTbl <*> fCol + -- Prefer real table relationships over the ones derived from views + tableRels = filter (not . relFTableIsView) relsMatching + relsSelected = if null tableRels then relsMatching else tableRels in - (\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`."]) <$> fTblCol + fkNote <$> [ (qiName (relForeignTable r), relColumns (relCardinality r)) | r@Relationship{} <- relsSelected ] + fkNote :: (Text, [(FieldName, FieldName)]) -> Text + fkNote (tblName, cols) = + T.intercalate "" + [ "This is a Foreign Key to `", tblName, ".", refCols, "`." ] + where + refCols = case snd <$> cols of + [refCol] -> refCol + cols' -> "(" <> T.intercalate ", " cols' <> ")" + colPairs = T.intercalate "," [ localCol <> ":" <> foreignCol | (localCol, foreignCol) <- cols ] pk :: Bool pk = colName col `elem` tablePKCols tbl uniqueNotes :: [Text] @@ -177,7 +186,7 @@ makeProperty tbl rels col = (colName col, Inline s) , if pk then Just "This is a Primary Key." else Nothing ] <> uniqueNotes - <> catMaybes [fk] + <> fks d = if length n > 1 then Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n) diff --git a/test/spec/Feature/OpenApi/OpenApiSpec.hs b/test/spec/Feature/OpenApi/OpenApiSpec.hs index 243849995..d675045f1 100644 --- a/test/spec/Feature/OpenApi/OpenApiSpec.hs +++ b/test/spec/Feature/OpenApi/OpenApiSpec.hs @@ -165,7 +165,7 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do "type": "string" }, "parent_id": { - "description": "Note:\nThis is a Foreign Key to `entities.id`.", + "description": "Note:\nThis is a Foreign Key to `entities.id`.", "format": "int32", "type": "integer" } @@ -200,7 +200,7 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do "type": "string" }, "parent_id": { - "description": "Note:\nThis is a Foreign Key to `entities.id`.", + "description": "Note:\nThis is a Foreign Key to `entities.id`.", "format": "int32", "type": "integer" } @@ -285,7 +285,7 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do { "format": "int32", "type": "integer", - "description": "Note:\nThis is a Unique column.\nThis is a Foreign Key to `second.id`." + "description": "Note:\nThis is a Unique column.\nThis is a Foreign Key to `second.id`." } |] @@ -370,6 +370,45 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do beingDescription `shouldBe` Just [aesonQQ|""|] + it "includes a composite foreign key marker with the full column mapping" $ do + r <- simpleBody <$> get "/" + + let parentFk = r ^? key "definitions" . key "comp_component_instance" . key "properties" . key "product_instance_id" + + liftIO $ + parentFk `shouldBe` Just + [aesonQQ| + { + "format": "int32", + "type": "integer", + "description": "Note:\nThis is a Primary Key.\nThis is a Foreign Key to `comp_product_instance.(id, product_id)`." + } + |] + + it "includes a single column foreign key marker with the unified format" $ do + r <- simpleBody <$> get "/" + + let productFk = r ^? key "definitions" . key "comp_product_instance" . key "properties" . key "product_id" + + liftIO $ + productFk `shouldBe` Just + [aesonQQ| + { + "format": "int32", + "type": "integer", + "description": "Note:\nThis is part of a composite unique constraint.\nThis is a Foreign Key to `comp_product.id`." + } + |] + + it "includes composite m2m relationship markers in the table description" $ do + r <- simpleBody <$> get "/" + + let beingDescription = r ^? key "definitions" . key "comp_being" . key "description" + + liftIO $ + beingDescription `shouldBe` Just + [aesonQQ|""|] + describe "Foreign table" $ it "includes foreign table properties" $ do @@ -471,7 +510,7 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do { "format": "int32", "type": "integer", - "description": "Note:\nThis is a Foreign Key to `pages.link`." + "description": "Note:\nThis is a Foreign Key to `pages.link`." } |] @@ -488,7 +527,7 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do { "format": "int32", "type": "integer", - "description": "Note:\nThis is a Foreign Key to `clients.id`." + "description": "Note:\nThis is a Foreign Key to `clients.id`." } |] diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index 8c7b6b7c9..2968b07c6 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -1246,6 +1246,58 @@ create table test.being_part ( primary key(being, part) ); +-- Tables for testing composite foreign keys in the OpenAPI output +create table test.comp_product ( + id int primary key, + name text +); + +create table test.comp_component ( + product_id int not null references test.comp_product(id), + component_id int not null references test.comp_product(id), + primary key(product_id, component_id) +); + +create table test.comp_product_instance ( + id int primary key, + product_id int not null references test.comp_product(id), + unique(id, product_id) +); + +create table test.comp_component_instance ( + product_instance_id int not null, + product_id int not null, + component_instance_id int not null, + component_id int not null, + primary key(product_instance_id, component_id), + constraint comp_component_instance_parent_fkey foreign key (product_instance_id, product_id) references test.comp_product_instance(id, product_id), + constraint comp_component_instance_child_fkey foreign key (component_instance_id, component_id) references test.comp_product_instance(id, product_id), + constraint comp_component_instance_component_fkey foreign key (product_id, component_id) references test.comp_component(product_id, component_id) +); + +-- Tables for testing composite m2m relationships in the OpenAPI output +create table test.comp_being ( + a int not null, + b int not null, + primary key(a, b) +); + +create table test.comp_thing ( + x int not null, + y int not null, + primary key(x, y) +); + +create table test.comp_being_thing ( + a int not null, + b int not null, + x int not null, + y int not null, + primary key(a, b, x, y), + constraint comp_being_thing_being_fkey foreign key (a, b) references test.comp_being(a, b), + constraint comp_being_thing_thing_fkey foreign key (x, y) references test.comp_thing(x, y) +); + create function test.single_out_param(num int, OUT num_plus_one int) AS $$ select num + 1; $$ language sql;