`:read`ing random entry

meow! i'd like to ask a possibly dumb question. i am currently making an inspirational quotes API. i have a /api/quote endpoint with :random action which pretty much speaks for itself - returns a random quote and its author how should i correctly define my :random action? i know how to fetch a random entry using Ecto from q in "table", order_by: fragment("RANDOM()"), limit: 1, select: q. how would i do this in my action? i am using sqlite btw and here's my resource definition
use Ash.Resource,
domain: App.Domain,
extensions: [AshJsonApi.Resource],
data_layer: AshSqlite.DataLayer

json_api do
type "quote"
end

sqlite do
table "quotes_en"

repo App.Repo
end

actions do
read :random do
get? true

prepare build(limit: 1) # made this to check if this even works
end
end

attributes do
attribute :text, :string do
primary_key? true
allow_nil? false
end

attribute :author, :string do
allow_nil? true
end
end
use Ash.Resource,
domain: App.Domain,
extensions: [AshJsonApi.Resource],
data_layer: AshSqlite.DataLayer

json_api do
type "quote"
end

sqlite do
table "quotes_en"

repo App.Repo
end

actions do
read :random do
get? true

prepare build(limit: 1) # made this to check if this even works
end
end

attributes do
attribute :text, :string do
primary_key? true
allow_nil? false
end

attribute :author, :string do
allow_nil? true
end
end
i use quote's text as primary key in my db to eliminate duplicates and to add new quotes from CSV-s and other stuff w/ ease. and i guess i won't need ids for them in the near future as you can see, i have language code in my table's name, that's because i have multiple tables with quotes in different languages. so i need to choose which table to query too...
Solution:
prepare build(limit: 1, sort: Ash.Sort.expr_sort(fragment("RANDOM()")))
prepare build(limit: 1, sort: Ash.Sort.expr_sort(fragment("RANDOM()")))
...
Jump to solution
13 Replies
Solution
ZachDaniel
ZachDaniel•21h ago
prepare build(limit: 1, sort: Ash.Sort.expr_sort(fragment("RANDOM()")))
prepare build(limit: 1, sort: Ash.Sort.expr_sort(fragment("RANDOM()")))
ZachDaniel
ZachDaniel•21h ago
Try that you may need to require Ash.Sort
kira🌺
kira🌺OP•21h ago
thanks! it works. now i need to somehow deal with the multiple tables situation. also API responses are a bit off - i need to return :text field inside "attributes", and not as id, but i don't have an 'id' column in my tables
{
"data": [
{
"attributes": {
// no text
"author": "Mother Teresa"
},
// here it is
"id": "Peace begins with a smile.",
"links": {},
"meta": {},
"type": "quote",
"relationships": {}
}
],
"links": {
"self": "http://localhost:4000/api/quote"
},
"meta": {},
"jsonapi": {
"version": "1.0"
}
}
{
"data": [
{
"attributes": {
// no text
"author": "Mother Teresa"
},
// here it is
"id": "Peace begins with a smile.",
"links": {},
"meta": {},
"type": "quote",
"relationships": {}
}
],
"links": {
"self": "http://localhost:4000/api/quote"
},
"meta": {},
"jsonapi": {
"version": "1.0"
}
}
ZachDaniel
ZachDaniel•21h ago
The id attribute is actually non-optional in the JSON:API specification so we take the primary key and we write it to the id attribute and remove it from attributes I forget if there is an option to tell it not to do that, lemme double check Looks like there is not currently, but one could be added without too much hassle. In the meantime you could use a generic action to make the response look like whatever you wanted
# in a different file
defmodule MyApp.MyDomain.MyResource.Types.QuoteResult do
use Ash.Type.NewType, subtype_of: :map, constraints: [
fields: [text: [type: :string, allow_nil?: false], author: [type: :string, allow_nil?: false]]
]
end

# in the actions block
action :random_quote, MyApp.MyDomain.MyResource.Types.QuoteResult do
run fn input, _ ->
{:ok, # get a random quote here and return `%{text: "the text", author: "the author"}`
end
end
# in a different file
defmodule MyApp.MyDomain.MyResource.Types.QuoteResult do
use Ash.Type.NewType, subtype_of: :map, constraints: [
fields: [text: [type: :string, allow_nil?: false], author: [type: :string, allow_nil?: false]]
]
end

# in the actions block
action :random_quote, MyApp.MyDomain.MyResource.Types.QuoteResult do
run fn input, _ ->
{:ok, # get a random quote here and return `%{text: "the text", author: "the author"}`
end
end
Then you can use route :get, "/random_quote", :random_quote that endpoint won't be JSON:API spec compliant but you probably don't care about that for this?
kira🌺
kira🌺OP•21h ago
so there's no option for uuid_primary_key/integer_primary_key [if i add one of them to my attributes list] to be like an abstract field? so Ash won't query it from my data layer?
ZachDaniel
ZachDaniel•20h ago
If you don't mind id and text being duplicated in the structs & response then you could do:
attribute :id, :string do
source :text
primary_key? true
allow_nil? false
end

attribute :text, :string do
allow_nil? false
end
attribute :id, :string do
source :text
primary_key? true
allow_nil? false
end

attribute :text, :string do
allow_nil? false
end
Then your response would have id and text populated the primary key can't be a calculation though, no You could also do
attributes do
uuid_primary_key :id

attribute :text, :string, allow_nil?: false
end

identities do
identity :unique_text, [:text]
end
attributes do
uuid_primary_key :id

attribute :text, :string, allow_nil?: false
end

identities do
identity :unique_text, [:text]
end
and then when you create something it will generate the UUID id and text will always be unique (or otherwise an error, unless you do an upsert)
kira🌺
kira🌺OP•20h ago
it'll do the job until i add ids to my tables, thanks! considering the table situation, i see that i can add multiple DSL table attributes
sqlite do
table "quotes_en"
table "quotes_zh"
table "quotes_fr"
# ...

repo App.Repo
end
sqlite do
table "quotes_en"
table "quotes_zh"
table "quotes_fr"
# ...

repo App.Repo
end
and Ash queries from the last defined table. if i define an argument in my :random action like :lang, is it possible to choose table from which to query?
ZachDaniel
ZachDaniel•20h ago
not using multiple table declarations no you'd do something like this:
argument :lang, :string, allow_nil?: false

prepare fn query, _ ->
Ash.Query.set_context(query, %{data_layer: %{table: "quotes_#{query.arguments.lang}"}})
end
argument :lang, :string, allow_nil?: false

prepare fn query, _ ->
Ash.Query.set_context(query, %{data_layer: %{table: "quotes_#{query.arguments.lang}"}})
end
If you do that, you must validate the language list 🙂
kira🌺
kira🌺OP•20h ago
where should i place validations block?
ZachDaniel
ZachDaniel•20h ago
there is one at the top level for validations across all create/update actions (and optionally destroy) and you can place validations in create/update/destroy actions directly
Elixord
Elixord•20h ago
Hexdocs Search Results
Searched ash-3.5.8 for validations
ash-3.5.8 | extras
ZachDaniel
ZachDaniel•20h ago
See the validations guide for more You can't use them on read actions, you have to roll your own validation code currently in preparations
kira🌺
kira🌺OP•20h ago
much obliged for the help!

Did you find this page helpful?