ash json patch with composite primary key

I have a resource with 2 primary keys
json_api do
type "client_data"

primary_key do
keys [:client_id, :data_id]
delimiter "~"
end
end
json_api do
type "client_data"

primary_key do
keys [:client_id, :data_id]
delimiter "~"
end
end
And this update action
update :update do
primary? true
accept :exposed
end
update :update do
primary? true
accept :exposed
end
And finally this json API route patch :update But how do I use it now? If I make this request
curl -X 'PATCH' \
'http://localhost:4000/api/v1/client_data/00000000-0000-0000-0000-000000000000~3e8be57c-26ed-4eef-abe5-52526d52b982' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json' \
-d '{
"data": {
"attributes": {
"exposed": true
},
"id": "00000000-0000-0000-0000-000000000000~3e8be57c-26ed-4eef-abe5-52526d52b982"
}
}'
curl -X 'PATCH' \
'http://localhost:4000/api/v1/client_data/00000000-0000-0000-0000-000000000000~3e8be57c-26ed-4eef-abe5-52526d52b982' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json' \
-d '{
"data": {
"attributes": {
"exposed": true
},
"id": "00000000-0000-0000-0000-000000000000~3e8be57c-26ed-4eef-abe5-52526d52b982"
}
}'
Then the wrong data is set as exposed. This is the query SQL query that is generated
UPDATE "client_data" AS p0 SET "exposed" = $1 FROM (SELECT sp0."exposed" AS "exposed", sp0."client_id" AS "client_id", sp0."data_id" AS "data_id" FROM "client_data" AS sp0 WHERE (sp0."client_id"::uuid = $2::uuid) AND ((CASE WHEN sp0."client_id"::uuid = $3::uuid THEN $4 ELSE ash_raise_error($5::jsonb) END)) LIMIT $6) AS s1 WHERE ((p0."data_id" = s1."data_id") AND (p0."client_id" = s1."client_id")) RETURNING p0."client_id", p0."data_id", p0."exposed" [true, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000", true, "{\"input\":{\"authorizer\":\"Ash.Policy.Authorizer\"},\"exception\":\"Ash.Error.Forbidden.Placeholder\"}", 1]
UPDATE "client_data" AS p0 SET "exposed" = $1 FROM (SELECT sp0."exposed" AS "exposed", sp0."client_id" AS "client_id", sp0."data_id" AS "data_id" FROM "client_data" AS sp0 WHERE (sp0."client_id"::uuid = $2::uuid) AND ((CASE WHEN sp0."client_id"::uuid = $3::uuid THEN $4 ELSE ash_raise_error($5::jsonb) END)) LIMIT $6) AS s1 WHERE ((p0."data_id" = s1."data_id") AND (p0."client_id" = s1."client_id")) RETURNING p0."client_id", p0."data_id", p0."exposed" [true, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000", true, "{\"input\":{\"authorizer\":\"Ash.Policy.Authorizer\"},\"exception\":\"Ash.Error.Forbidden.Placeholder\"}", 1]
In other words, the ID for the data is ignored and idk how to set it properly. I also have this policy for what it's worth
policies do
# Filter which rows the tenant sees instead of rejecting their whole request (this is default).
default_access_type :filter

# Reject whole request if actor is absent.
policy actor_present() do
authorize_if expr(client_id == ^actor(:client_id))
end
end
policies do
# Filter which rows the tenant sees instead of rejecting their whole request (this is default).
default_access_type :filter

# Reject whole request if actor is absent.
policy actor_present() do
authorize_if expr(client_id == ^actor(:client_id))
end
end
Solution:
GitHub
ash_json_api/documentation/topics/composite-primary-keys.md at main...
The JSON:API extension for the Ash Framework. Contribute to ash-project/ash_json_api development by creating an account on GitHub.
Jump to solution
19 Replies
ZachDaniel
ZachDanielβ€’3mo ago
πŸ€” Sorry, not quite following. When you say the id is ignored what do you mean? Which part of that SQL is the problematic part? Not always easy to connect the dots on a fresh read, sorry πŸ˜…
Sienhopist
SienhopistOPβ€’3mo ago
There's a row in my db with UUID columns client_id 00000000-0000-0000-0000-000000000000 and data_id 3e8be57c-26ed-4eef-abe5-52526d52b982 and a boolean exposed column. The data_id is not used in the query. The patch request is supposed to set the exposed to true in a specific row, but the generated query just selects a random row where the client_id is 00000000-0000-0000-0000-000000000000 and sets that to true. The data_id is hot present in the query I think maybe I'm just misunderstanding how patch requests work in general with Ash json API πŸ€”
ZachDaniel
ZachDanielβ€’3mo ago
πŸ€” that is weird Something seems wrong there Maybe a bug in our parsing the composite primary key? That definitely looks like a problem to me Please reproduce and I will investigate.
Sienhopist
SienhopistOPβ€’3mo ago
I think I did it on top of the Ash Admin repo. But now should I make a PR to the admin UI repo or upload the repo on my account and just paste a link? It's not exactly the same issue, maybe, but I think it's coming from the same place. Either way something is wrong with patch requests I suppose I could also send patch files right here
Sienhopist
SienhopistOPβ€’3mo ago
@Zach Daniel So apply these patches on top of 91331df9e5ed of ash_admin. Run postgres through docker compose - docker compose up -d. Run the migrations to get the ExampleDomain and Example.ExampleResource - mix migrate. Run the server with iex -S mix dev. Go to /api/json/swaggerui. You should see the get and patch endpoints. Create some starting data:
data = [ %{ client_id: "67a7bd7b-c506-40e9-b486-23a14f849087", data_id: "355656b6-cc35-4221-beec-4eeaa2952db3" }, %{ client_id: "25859a99-16ec-45da-900b-877e4a39585d", data_id: "e6da9ed7-2100-4d50-a591-b2f52264d3db" }, %{ client_id: "e8d3592c-9845-4d83-b41c-861f987dbbcf", data_id: "016b5fe4-df9a-431e-a986-f5c3a1df9246" }, %{ client_id: "14112007-c281-4d43-b61c-925fa64e9220", data_id: "92dca674-28c9-4547-863a-c6135f53375e" }, %{ client_id: "db8c8093-e9d1-45cd-af51-c868992236c1", data_id: "226ae4e2-9f33-447a-93bd-32c2cb45b09a" } ]

Ash.bulk_create!(data, Example.ExampleResource, :create)
data = [ %{ client_id: "67a7bd7b-c506-40e9-b486-23a14f849087", data_id: "355656b6-cc35-4221-beec-4eeaa2952db3" }, %{ client_id: "25859a99-16ec-45da-900b-877e4a39585d", data_id: "e6da9ed7-2100-4d50-a591-b2f52264d3db" }, %{ client_id: "e8d3592c-9845-4d83-b41c-861f987dbbcf", data_id: "016b5fe4-df9a-431e-a986-f5c3a1df9246" }, %{ client_id: "14112007-c281-4d43-b61c-925fa64e9220", data_id: "92dca674-28c9-4547-863a-c6135f53375e" }, %{ client_id: "db8c8093-e9d1-45cd-af51-c868992236c1", data_id: "226ae4e2-9f33-447a-93bd-32c2cb45b09a" } ]

Ash.bulk_create!(data, Example.ExampleResource, :create)
Now try to edit one resource's exposed to true. For example, the first result returned in Swagger UI is this (see screenshot). Let's try to patch it. This is the curl call generated by Swagger UI
curl -X 'PATCH' \
'http://localhost:4000/api/json/example/14112007-c281-4d43-b61c-925fa64e9220~92dca674-28c9-4547-863a-c6135f53375e?fields%5Bclient_data%5D=client_id%2Cdata_id%2Cexposed' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json' \
-H 'x-csrf-token: OxcbA3cYJ2x2EQMuYGcqTAMlOC5mFScAOGHK5_A6CRKl8Va60obk-wAy' \
-d '{
"data": {
"attributes": {
"exposed": true
},
"id": "14112007-c281-4d43-b61c-925fa64e9220~92dca674-28c9-4547-863a-c6135f53375e"
}
}'
curl -X 'PATCH' \
'http://localhost:4000/api/json/example/14112007-c281-4d43-b61c-925fa64e9220~92dca674-28c9-4547-863a-c6135f53375e?fields%5Bclient_data%5D=client_id%2Cdata_id%2Cexposed' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json' \
-H 'x-csrf-token: OxcbA3cYJ2x2EQMuYGcqTAMlOC5mFScAOGHK5_A6CRKl8Va60obk-wAy' \
-d '{
"data": {
"attributes": {
"exposed": true
},
"id": "14112007-c281-4d43-b61c-925fa64e9220~92dca674-28c9-4547-863a-c6135f53375e"
}
}'
The result is success 200. But now when we look at our data in the Admin UI I expected the highlighted row to be true. Instead the third one is (see 2nd screenshot).
Sienhopist
SienhopistOPβ€’3mo ago
This is the SQL query that was run for the request
23:54:03.265 [debug] QUERY OK source="example_resource" db=1.6ms queue=0.6ms idle=1184.4ms
UPDATE "example_resource" AS e0 SET "exposed" = $1 FROM (SELECT se0."client_id" AS "client_id", se0."data_id" AS "data_id", se0."exposed" AS "exposed" FROM "example_resource" AS se0 LIMIT $2) AS s1 WHERE ((e0."data_id" = s1."data_id") AND (e0."client_id" = s1."client_id")) RETURNING e0."client_id", e0."data_id", e0."exposed" [true, 1]
23:54:03.265 [debug] QUERY OK source="example_resource" db=1.6ms queue=0.6ms idle=1184.4ms
UPDATE "example_resource" AS e0 SET "exposed" = $1 FROM (SELECT se0."client_id" AS "client_id", se0."data_id" AS "data_id", se0."exposed" AS "exposed" FROM "example_resource" AS se0 LIMIT $2) AS s1 WHERE ((e0."data_id" = s1."data_id") AND (e0."client_id" = s1."client_id")) RETURNING e0."client_id", e0."data_id", e0."exposed" [true, 1]
It's not exactly what I talked about earlier, but it may stem from the same issue. The IDs provided in the patch request are not handled properly. Either that, or I'm gravely misunderstanding how I'm supposed to use this πŸ˜…
ZachDaniel
ZachDanielβ€’3mo ago
Is it possible for you to provide a full project that I can run? Reproducing piecemeal like this has cost me a lot of time in the past You can just zip up your project where you are seeing this
Sienhopist
SienhopistOPβ€’3mo ago
Alright sure that works too Although it would only cover the first step. You'd still need to do the steps from postgres down
ZachDaniel
ZachDanielβ€’3mo ago
πŸ‘
Sienhopist
SienhopistOPβ€’3mo ago
Left the git history to make it clearer
Sienhopist
SienhopistOPβ€’3mo ago
It's only the top 4 commits
ZachDaniel
ZachDanielβ€’3mo ago
wtf I guess no one has used composite primary keys with AshJsonApi I don't understand how this hasn't been spotted before 😑 Working on it I have a fix, just working on some tests Okay, I get it now people would have been doing route: "/:data_id/:client_id" etc. I will have to make this opt-in to make it non-breaking We will now properly show an error on an unhandled input, and you can opt-in to behavior that sets a given path param to the composite primary key @Sienhopist the resolution for this is a new option which is available in main, please try it out
Solution
ZachDaniel
ZachDanielβ€’3mo ago
GitHub
ash_json_api/documentation/topics/composite-primary-keys.md at main...
The JSON:API extension for the Ash Framework. Contribute to ash-project/ash_json_api development by creating an account on GitHub.
Sienhopist
SienhopistOPβ€’3mo ago
It looks like it does work based on a few tests. Wow you're fast. In that case I only have one last question. This update wants us to provide the complete composite key in the url. However, can this be simplified for the caller of the API if the ^actor/1 already contains the client_id and the caller only wants to specify the data_id? Or should I make a generic action for this?
ZachDaniel
ZachDanielβ€’3mo ago
Yes, you'd update the route And add arguments to the action argument :data_id, ... And on the json api route get ..., route: "/:data_id" iirc Double check the get route docs
Sienhopist
SienhopistOPβ€’3mo ago
But how do you get the client_id from the actor into the composite key?
ZachDaniel
ZachDanielβ€’3mo ago
change filter(expr(client_id == ^actor(:id))
Sienhopist
SienhopistOPβ€’3mo ago
Sorry for asking again, but I'm not sure how to get this to work and I also don't understand your example πŸ˜… This is my update action
update :update do
primary? true
accept :exposed

argument :data_id, :uuid, allow_nil?: false, public?: true

change filter expr(client_id == ^actor(:client_id) and data_id == ^arg(:data_id))
end
update :update do
primary? true
accept :exposed

argument :data_id, :uuid, allow_nil?: false, public?: true

change filter expr(client_id == ^actor(:client_id) and data_id == ^arg(:data_id))
end
And this is my json API
base_route "/client_data", Client2Data do
index :read
patch :update, route: "/:data_id"
end
base_route "/client_data", Client2Data do
index :read
patch :update, route: "/:data_id"
end
But when I make a request like the following, I get an error that the data_id is missing.
curl -X 'PATCH' \
'http://localhost:4000/api/v1/client_data/23f7f57a-0d85-4188-a07a-f0aa69e7dd70' \
-H 'accept: application/vnd.api+json' \
-H 'Authorization: Bearer my_bearer_token' \
-H 'Content-Type: application/vnd.api+json' \
-d '{
"data": {
"attributes": {
"exposed": true
}
}
}'
curl -X 'PATCH' \
'http://localhost:4000/api/v1/client_data/23f7f57a-0d85-4188-a07a-f0aa69e7dd70' \
-H 'accept: application/vnd.api+json' \
-H 'Authorization: Bearer my_bearer_token' \
-H 'Content-Type: application/vnd.api+json' \
-d '{
"data": {
"attributes": {
"exposed": true
}
}
}'
I think I'm not doing something right Actually never mind. While I would love to know how to actually do this, I realized if I went ahead with it then I wouldn't be able to reuse this route to allow admin users to make changes to other clients
ZachDaniel
ZachDanielβ€’3mo ago
πŸ‘Œmakes sense.

Did you find this page helpful?