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;