add: config to emit warning for legacy target names
Adds the `url_use_legacy_target_names` config. Enabled (default): * It allows using the resource name in filters, orders or limits when it has an alias, e.g. `table?select=alias:target(*)&target.id=eq.1` * Logs a WARNING with a hint to use the alias * Returns a Warning header in the response Disabled: * It returns an error, only the alias is allowed * No warnings returned This feature is deprecated
This commit is contained in:
committed by
Laurence Isla
parent
2fa8de4e52
commit
490d1dc5d3
@@ -1,7 +1,59 @@
|
||||
- - - qiName: awards
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - director_id
|
||||
- id
|
||||
relCons: awards_director_id_fkey
|
||||
tag: M2O
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: directors
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - film_id
|
||||
- id
|
||||
relCons: awards_film_id_fkey
|
||||
tag: M2O
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: films
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
|
||||
- - - qiName: directors
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- director_id
|
||||
relCons: awards_director_id_fkey
|
||||
tag: O2M
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: directors
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- director_id
|
||||
@@ -22,6 +74,22 @@
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- film_id
|
||||
relCons: awards_film_id_fkey
|
||||
tag: O2M
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: films
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - director_id
|
||||
- id
|
||||
|
||||
@@ -126,6 +126,64 @@
|
||||
tableSchema: public
|
||||
tableUpdatable: false
|
||||
|
||||
- - qiName: awards
|
||||
qiSchema: public
|
||||
- tableColumns:
|
||||
director_id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: director_id
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
film_id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: film_id
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: id
|
||||
colNominalType: integer
|
||||
colNullable: false
|
||||
colType: integer
|
||||
name:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: name
|
||||
colNominalType: text
|
||||
colNullable: true
|
||||
colType: text
|
||||
year:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: year
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
tableDeletable: true
|
||||
tableDescription: null
|
||||
tableInsertable: true
|
||||
tableIsView: false
|
||||
tableName: awards
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: films
|
||||
qiSchema: public
|
||||
- tableColumns:
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = true
|
||||
server-trace-header = "traceparent"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = false
|
||||
server-trace-header = "CF-Ray"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = true
|
||||
server-trace-header = "X-Request-Id"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = false
|
||||
|
||||
@@ -43,3 +43,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -41,6 +41,7 @@ PGRST_SERVER_TRACE_HEADER: X-Request-Id
|
||||
PGRST_SERVER_TIMING_ENABLED: true
|
||||
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
||||
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
||||
PGRST_URL_USE_LEGACY_TARGET_NAMES: false
|
||||
PGRST_ADMIN_SERVER_HOST: 127.0.0.1
|
||||
PGRST_ADMIN_SERVER_PORT: 3001
|
||||
PGRST_ADMIN_SERVER_UNIX_SOCKET: /tmp/admin_io_test.sock
|
||||
|
||||
@@ -38,6 +38,7 @@ server-trace-header = "X-Request-Id"
|
||||
server-timing-enabled = true
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = false
|
||||
admin-server-port = 3001
|
||||
admin-server-host = "127.0.0.1"
|
||||
admin-server-unix-socket = "/tmp/admin_io_test.sock"
|
||||
|
||||
@@ -25,6 +25,7 @@ ALTER ROLE db_config_authenticator SET pgrst.openapi_server_proxy_uri = 'https:/
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://origin.com';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_timing_enabled = 'false';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.url_use_legacy_target_names = 'true';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.db_hoisted_tx_settings = 'autovacuum_work_mem';
|
||||
|
||||
-- override with database specific setting
|
||||
@@ -81,6 +82,7 @@ ALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://oth
|
||||
ALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://otherorigin.com';
|
||||
ALTER ROLE other_authenticator SET pgrst.server_timing_enabled = 'true';
|
||||
ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';
|
||||
ALTER ROLE other_authenticator SET pgrst.url_use_legacy_target_names = 'true';
|
||||
ALTER ROLE other_authenticator SET pgrst.db_hoisted_tx_settings = 'maintenance_work_mem';
|
||||
|
||||
create schema postgrest;
|
||||
|
||||
@@ -3,7 +3,7 @@ GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
|
||||
|
||||
GRANT SELECT ON authors_only TO postgrest_test_author;
|
||||
GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
GRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
GRANT SELECT ON directors, films, awards TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
|
||||
GRANT ALL ON cats TO postgrest_test_anonymous;
|
||||
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
|
||||
|
||||
@@ -25,6 +25,14 @@ CREATE TABLE films (
|
||||
on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE awards (
|
||||
id int primary key,
|
||||
name text,
|
||||
year int,
|
||||
film_id int references films(id),
|
||||
director_id int references directors(id)
|
||||
);
|
||||
|
||||
-- data to test resource embedding
|
||||
TRUNCATE TABLE directors CASCADE;
|
||||
INSERT INTO directors
|
||||
|
||||
@@ -2089,3 +2089,33 @@ def test_work_mem_in_role_settings(defaultenv):
|
||||
response = postgrest.session.post("/rpc/get_work_mem", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == '"3MB"'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", ["true", "false"])
|
||||
def test_use_legacy_target_names(enabled, defaultenv):
|
||||
"Show a warning when a target name is used instead of an alias, only when config is enabled"
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_URL_USE_LEGACY_TARGET_NAMES": enabled,
|
||||
}
|
||||
|
||||
with run(env=env) as postgrest:
|
||||
response = postgrest.session.get(
|
||||
"/directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026"
|
||||
)
|
||||
|
||||
output = postgrest.read_stdout(nlines=10)
|
||||
|
||||
log_err_warning = "WARNING: Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
|
||||
log_err_hint = "Update filters, orders or limits that use `films` to `all_films`, `awards` to `awards_2026` in `GET /directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026`"
|
||||
|
||||
has_warning_log = any(log_err_warning in line for line in output)
|
||||
has_hint_log = any(log_err_hint in line for line in output)
|
||||
|
||||
if enabled == "true":
|
||||
assert response.status_code == 200
|
||||
assert has_warning_log and has_hint_log
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
assert not has_warning_log and not has_hint_log
|
||||
|
||||
@@ -33,7 +33,6 @@ CREATE TABLE test.roles (
|
||||
character TEXT
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE test.authors_only ();
|
||||
|
||||
CREATE FUNCTION test.call_me (name TEXT) RETURNS TEXT
|
||||
|
||||
@@ -122,6 +122,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configInternalSCQuerySleepFst = Nothing
|
||||
, configInternalSCQuerySleepSnd = Nothing
|
||||
, configServerTimingEnabled = True
|
||||
, configUrlUseLegacyTargetNames = True
|
||||
}
|
||||
|
||||
testCfg :: AppConfig
|
||||
|
||||
@@ -7,6 +7,7 @@ import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion190)
|
||||
|
||||
import Protolude hiding (get)
|
||||
@@ -1071,16 +1072,6 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
{ "id":4,"children":[]}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when embedding the same table more than once" $
|
||||
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name":"Lake",
|
||||
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
|
||||
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
|
||||
}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "ordering response" $ do
|
||||
it "by a column asc" $
|
||||
get "/items?id=lte.2&order=id.asc"
|
||||
@@ -1186,6 +1177,12 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
]
|
||||
}
|
||||
|
||||
it "filters, orders and limits aliased embeded entities using the target name" $
|
||||
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = ["Warning" <:> "299 PostgRESTv15(pre-release) \"Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release. Update `tasks` to `the_tasks` in query string filters, orders or limits.\""] }
|
||||
|
||||
|
||||
describe "Accept headers" $ do
|
||||
it "should respond an unknown accept type with 406" $
|
||||
request methodGet "/simple_pk"
|
||||
@@ -1690,3 +1687,30 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = ["Content-Length" <:> "96"]}
|
||||
|
||||
specLegacyTargetNames :: SpecWithConfig
|
||||
specLegacyTargetNames withConfig = withConfig (baseCfg { configUrlUseLegacyTargetNames = False }) $
|
||||
context "disable legacy target names" $ do
|
||||
describe "Shaping response with select parameter" $ do
|
||||
it "works when embedding the same table more than once" $
|
||||
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name":"Lake",
|
||||
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
|
||||
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
|
||||
}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
describe "ordering response" $
|
||||
it "filters, orders or limits do not work with aliased embeded entities using the target name" $
|
||||
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"code":"PGRST108",
|
||||
"details":"Target names are not allowed in filters if they have an alias",
|
||||
"hint":"Change 'tasks' to 'the_tasks' in filters, orders or limits.",
|
||||
"message":"'tasks' is not an embedded resource in this request"
|
||||
}
|
||||
|]
|
||||
{ matchStatus = 400,
|
||||
matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -152,6 +152,7 @@ main = do
|
||||
, ("Feature.Query.PreparedStatementsSpec.spec" , Feature.Query.PreparedStatementsSpec.spec)
|
||||
, ("Feature.Query.QueryLimitedSpec" , Feature.Query.QueryLimitedSpec.spec)
|
||||
, ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion)
|
||||
, ("Feature.Query.QuerySpec.specLegacyTargetNames" , Feature.Query.QuerySpec.specLegacyTargetNames)
|
||||
, ("Feature.Query.RangeSpec" , Feature.Query.RangeSpec.spec)
|
||||
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
|
||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
||||
|
||||
@@ -175,6 +175,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configServerTraceHeader = Nothing
|
||||
, configServerUnixSocket = Nothing
|
||||
, configServerUnixSocketMode = 432
|
||||
, configUrlUseLegacyTargetNames = True
|
||||
, configDbTxAllowOverride = True
|
||||
, configDbTxRollbackAll = True
|
||||
, configAdminServerHost = "localhost"
|
||||
|
||||
Reference in New Issue
Block a user