feat: expose unique columns and many-to-many markers in OpenAPI

Add unique constraint and many-to-many relationship metadata to the
generated OpenAPI spec so clients can render them.

- Store unique constraints on Table as tableUniqueCols (mirroring
  tablePKCols) instead of denormalizing them onto each Column.
- Compute unique constraints via a per-table tbl_unique_cols CTE in
  tablesSqlQuery.
- Annotate unique columns and composite unique constraints in property
  descriptions, and emit m2m markers in table descriptions.
This commit is contained in:
2026-08-20 18:03:43 +02:00
parent ce7ea53a57
commit 77ab8f83ac
8 changed files with 169 additions and 9 deletions
+33 -4
View File
@@ -30,8 +30,8 @@ import PostgREST.Network (escapeHostName)
import PostgREST.Query.OpenApi (TableAccess (..), TablesAccess)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Cardinality (..), Relationship (..),
RelationshipsMap)
import PostgREST.SchemaCache.Relationship (Cardinality (..), Junction (..),
Relationship (..), RelationshipsMap)
import PostgREST.SchemaCache.Routine (FuncVolatility (..), Routine (..),
RoutineParam (..))
import PostgREST.SchemaCache.Table (Column (..), Table (..), TablesMap,
@@ -110,13 +110,35 @@ parseDefault colType colDefault =
makeTableDef :: RelationshipsMap -> (Table, TableAccess) -> (Text, Schema)
makeTableDef rels (t, access) =
(tn, (mempty :: Schema)
& description .~ tableDescription t
& description .~ tblDescription
& type_ ?~ SwaggerObject
& properties .~ fromList (makeProperty t rels <$> cols)
& required .~ fmap colName (filter (not . colNullable) cols))
where
tn = tableName t
cols = accessibleCols t (taSelectCols access)
tblDescription = case m2mMarkers t rels of
[] -> tableDescription t
ms -> Just $ maybe "" (`T.append` "\n\n") (tableDescription t) <> T.intercalate "\n" ms
-- | Emits markers for the many-to-many relationships of a table, so that clients
-- can render these relations. The marker includes the target table(embedding key),
-- the junction table and the junction columns referencing source and target.
m2mMarkers :: Table -> RelationshipsMap -> [Text]
m2mMarkers tbl rels = mapMaybe m2mMarker searchedRels
where
searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
m2mMarker Relationship{relForeignTable, relCardinality=M2M junction} =
Just $ T.intercalate ""
[ "<m2m table='", qiName relForeignTable
, "' junction='", qiName (junTable junction)
, "' source='", junctionSourceCol junction
, "' target='", junctionTargetCol junction
, "'/>"
]
m2mMarker _ = Nothing
junctionSourceCol junction = maybe mempty snd (headMay $ junColsSource junction)
junctionTargetCol junction = maybe mempty snd (headMay $ junColsTarget junction)
accessibleCols :: Table -> [FieldName] -> [Column]
accessibleCols t cols = filter ((`elem` cols) . colName) (tableColumnsList t)
@@ -144,11 +166,18 @@ makeProperty tbl rels col = (colName col, Inline s)
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
pk :: Bool
pk = colName col `elem` tablePKCols tbl
uniqueNotes :: [Text]
uniqueNotes = mapMaybe uniqueNote (filter (colName col `elem`) (tableUniqueCols tbl))
where
uniqueNote cols
| length cols == 1 = Just "This is a Unique column.<unique/>"
| otherwise = Just $ "This is part of a composite unique constraint.<unique cols='" <> T.intercalate "," cols <> "'/>"
n = catMaybes
[ Just "Note:"
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
, fk
]
<> uniqueNotes
<> catMaybes [fk]
d =
if length n > 1 then
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)