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:
@@ -8,6 +8,7 @@
|
||||
tableName: authors_only
|
||||
tablePKCols: []
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: cats
|
||||
@@ -39,6 +40,7 @@
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: items_w_isolation_level
|
||||
@@ -69,6 +71,7 @@
|
||||
tableName: items_w_isolation_level
|
||||
tablePKCols: []
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: directors
|
||||
@@ -100,6 +103,7 @@
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: projects
|
||||
@@ -112,6 +116,7 @@
|
||||
tableName: projects
|
||||
tablePKCols: []
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: infinite_recursion
|
||||
@@ -124,6 +129,7 @@
|
||||
tableName: infinite_recursion
|
||||
tablePKCols: []
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: false
|
||||
|
||||
- - qiName: awards
|
||||
@@ -182,6 +188,7 @@
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: films
|
||||
@@ -222,6 +229,7 @@
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: items
|
||||
@@ -243,4 +251,5 @@
|
||||
tableName: items
|
||||
tablePKCols: []
|
||||
tableSchema: public
|
||||
tableUniqueCols: []
|
||||
tableUpdatable: true
|
||||
|
||||
@@ -239,7 +239,7 @@ def test_pool_acquisition_timeout(level, defaultenv, metapostgrest):
|
||||
assert data["message"] == "Timed out acquiring connection from connection pool."
|
||||
|
||||
# ensure the message appears on the logs as well
|
||||
output = sorted(postgrest.read_stdout(nlines=10))
|
||||
output = sorted(drain_stdout(postgrest))
|
||||
|
||||
if level == "crit":
|
||||
assert len(output) == 0
|
||||
|
||||
+3
-3
@@ -44,7 +44,7 @@ def test_log_level(level, defaultenv):
|
||||
response = postgrest.session.get("/")
|
||||
assert response.status_code == 200
|
||||
|
||||
output = postgrest.read_stdout(nlines=9)
|
||||
output = drain_stdout(postgrest)
|
||||
|
||||
if level == "crit":
|
||||
assert len(output) == 0
|
||||
@@ -82,7 +82,7 @@ def test_log_level(level, defaultenv):
|
||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||
],
|
||||
)
|
||||
assert len(output) == 9
|
||||
assert len(output) > 3
|
||||
assert any("Connection" and "is available" in line for line in output)
|
||||
assert any("Connection" and "is used" in line for line in output)
|
||||
|
||||
@@ -403,7 +403,7 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):
|
||||
assert response.status_code == 500
|
||||
|
||||
# ensure the message appears on the logs
|
||||
output = postgrest.read_stdout(nlines=8)
|
||||
output = drain_stdout(postgrest)
|
||||
|
||||
if level == "crit":
|
||||
assert len(output) == 0
|
||||
|
||||
@@ -285,10 +285,91 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>"
|
||||
"description": "Note:\nThis is a Unique column.<unique/>\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>"
|
||||
}
|
||||
|]
|
||||
|
||||
it "includes a unique description for a column with a unique constraint" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let uniqueKey = r ^? key "definitions" . key "single_unique" . key "properties" . key "unique_key"
|
||||
|
||||
liftIO $
|
||||
uniqueKey `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is a Unique column.<unique/>"
|
||||
}
|
||||
|]
|
||||
|
||||
it "includes the column list of a composite unique constraint" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let compoundKey1 = r ^? key "definitions" . key "compound_unique" . key "properties" . key "key1"
|
||||
compoundKey2 = r ^? key "definitions" . key "compound_unique" . key "properties" . key "key2"
|
||||
|
||||
liftIO $ do
|
||||
compoundKey1 `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
||||
}
|
||||
|]
|
||||
compoundKey2 `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
||||
}
|
||||
|]
|
||||
|
||||
it "includes the column list for mixed single and composite unique constraints" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let uniqueCol = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "id"
|
||||
compoundKey1 = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "key1"
|
||||
compoundKey2 = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "key2"
|
||||
|
||||
liftIO $ do
|
||||
uniqueCol `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is a Unique column.<unique/>"
|
||||
}
|
||||
|]
|
||||
compoundKey1 `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
||||
}
|
||||
|]
|
||||
compoundKey2 `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "int32",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
||||
}
|
||||
|]
|
||||
|
||||
it "includes m2m relationship markers in the table description" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let beingDescription = r ^? key "definitions" . key "being" . key "description"
|
||||
|
||||
liftIO $
|
||||
beingDescription `shouldBe` Just
|
||||
[aesonQQ|"<m2m table='part' junction='being_part' source='being' target='part'/>"|]
|
||||
|
||||
describe "Foreign table" $
|
||||
|
||||
it "includes foreign table properties" $ do
|
||||
|
||||
Vendored
+9
@@ -1451,6 +1451,15 @@ create table test.compound_unique(
|
||||
unique(key1, key2)
|
||||
);
|
||||
|
||||
create table test.mixed_unique(
|
||||
id integer not null,
|
||||
key1 integer not null,
|
||||
key2 integer not null,
|
||||
value text,
|
||||
unique(id),
|
||||
unique(key1, key2)
|
||||
);
|
||||
|
||||
create table test.family_tree (
|
||||
id text not null primary key,
|
||||
name text not null,
|
||||
|
||||
Reference in New Issue
Block a user