GraphQL API layer for Brando CMS blueprints via Absinthe.
Auto-generates Absinthe types, queries, and resolvers from your existing Brando blueprints. No hand-written schemas — add a graphql DSL block and you get a fully functional GraphQL endpoint.
Add brando_graphql to your dependencies:
# mix.exs
defp deps do
[
{:brando_graphql, github: "brandocms/brando_graphql"}
]
enddefmodule MyApp.Projects.Project do
use Brando.Blueprint,
application: "MyApp",
domain: "Projects",
schema: "Project",
singular: "project",
plural: "projects",
extensions: [BrandoGraphql.Resource]
# ... attributes, assets, relations, forms, listings ...
graphql do
type_name :project
queries do
list :list_projects
get :get_project
end
hide_fields [:creator_id, :deleted_at, :marked_as_deleted]
end
endA single module aggregates all your blueprint types into one Absinthe schema:
defmodule MyAppWeb.API.Schema do
use BrandoGraphql.Schema,
schemas: [
MyApp.Projects.Project,
MyApp.Projects.Client
]
endThis auto-generates:
- An Absinthe object type for each blueprint (from attributes, assets, and relations)
- List and get query fields with resolvers that delegate to your existing context functions
- Custom types for Brando images, videos, files, and galleries
# router.ex
scope "/api" do
pipe_through :api
forward "/graphql", Absinthe.Plug,
schema: MyAppWeb.API.Schema,
json_codec: Jason
endThat's it. Start your server and query away.
{
projects(limit: 10, offset: 0, status: "published") {
entries {
id
title
slug
insertedAt
}
paginationMeta {
totalEntries
totalPages
currentPage
pageSize
}
}
}List queries return a wrapper with entries and paginationMeta.
{
project(id: "1") {
id
title
slug
status
listingImage {
url
width
height
alt
dominantColor
sizes
}
projectGallery {
galleryObjects {
sequence
image { url width height }
video { url width height }
}
}
}
}Pass an order string argument. Prefix with - for descending:
{
projects(order: "-inserted_at,title") {
entries { id title }
}
}Pass a JSON-encoded string to the filter argument:
# Inline
{
projects(filter: "{\"language\": \"en\"}") {
entries { id title }
paginationMeta { totalEntries }
}
}With variables (recommended — cleaner for frontends):
query($filter: String) {
projects(filter: $filter) {
entries { id title }
}
}{ "filter": "{\"language\": \"en\"}" }Standard GraphQL introspection works out of the box:
{
__type(name: "Project") {
fields {
name
type { name kind }
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
type_name |
:atom |
derived from blueprint singular | Absinthe object type name |
hide_fields |
[:atom] |
[] |
Fields to exclude from the GraphQL type |
| Entity | Argument | Description |
|---|---|---|
list |
:action |
Context function for listing (e.g. :list_projects) |
get |
:action |
Context function for a single entry (e.g. :get_project) |
The context function names must match functions defined in your blueprint's context module (the same ones used by your admin interface).
List queries accept:
| Argument | Type | Default | Description |
|---|---|---|---|
limit |
Int |
25 |
Max entries to return |
offset |
Int |
0 |
Number of entries to skip |
order |
String |
— | Comma-separated fields, - prefix for desc |
status |
String |
"published" |
Filter by status |
filter |
String |
— | JSON-encoded filter object, e.g. "{\"language\": \"en\"}" |
Get queries accept:
| Argument | Type | Required | Description |
|---|---|---|---|
id |
ID! |
yes | Entry ID |
| Brando type | GraphQL type |
|---|---|
:string, :text, :slug, :villain, :status, :enum, :language |
String |
:integer |
Int |
:float |
Float |
:boolean |
Boolean |
:datetime, :naive_datetime |
NaiveDateTime |
:date |
Date |
:json, :map |
JSON (custom scalar) |
| Brando asset | GraphQL type | Key fields |
|---|---|---|
:image |
BrandoImage |
url, path, width, height, alt, title, credits, dominantColor, sizes, focal, formats |
:video |
BrandoVideo |
url, width, height, source, remoteId, thumbnailUrl |
:file |
BrandoFile |
url, filename, filesize, contentType |
:gallery |
BrandoGallery |
galleryObjects → [BrandoGalleryObject] with sequence, image, video |
The url field on images, videos, and files is auto-resolved to a full URL using Brando's media URL configuration.
The sizes field on images returns each size with a resolved URL and width descriptor, making it easy to build responsive <img srcset="..."> on the frontend:
{
"sizes": {
"small": { "url": "/media/images/photo-small.jpg", "width": 700 },
"medium": { "url": "/media/images/photo-medium.jpg", "width": 1100 },
"large": { "url": "/media/images/photo-large.jpg", "width": 1700 },
"xlarge": { "url": "/media/images/photo-xlarge.jpg", "width": 2100 }
}
}Width values are read from Brando's default_srcset config at runtime. Frontend usage:
function ResponsiveImage({ image }) {
const srcSet = Object.values(image.sizes)
.filter(s => s.width)
.map(s => `${s.url} ${s.width}w`)
.join(', ')
return <img src={image.url} srcSet={srcSet} sizes="100vw" alt={image.alt} />
}Relations (:belongs_to, :has_many, :has_one) are automatically exposed if the related module is included in the schemas list of your schema module. Otherwise they are skipped.
# Both Project and Client types will include the relationship field
use BrandoGraphql.Schema,
schemas: [
MyApp.Projects.Project, # has belongs_to :client
MyApp.Projects.Client
]Hidden fields
These fields are always hidden by default:
creator_id,deleted_at,marked_as_deleted,password
Add more via hide_fields in the DSL:
graphql do
hide_fields [:internal_notes, :admin_data]
endAsset associations (image, video, file, gallery) are automatically preloaded when queried. You don't need to configure preloads manually.
Gallery assets additionally preload nested gallery_objects with their image and video associations.
For public content (status: published), no authentication is needed — the same data is already public on the rendered site.
For elevated access, use the included bearer token plug:
# config/runtime.exs
config :brando_graphql,
bearer_tokens: [System.get_env("API_BEARER_TOKEN")]# router.ex
pipeline :api_auth do
plug BrandoGraphql.Plug.Auth
end
scope "/api" do
pipe_through [:api, :api_auth]
forward "/graphql", Absinthe.Plug, schema: MyAppWeb.API.Schema
endImage and file URLs are resolved automatically. The resolution strategy (in order):
- Explicit config:
config :brando_graphql, media_url_prefix: "https://cdn.example.com/media/" - Brando's
Brando.Utils.media_url()(auto-detected at runtime) - Fallback:
"/media/"
For production with a CDN:
# config/runtime.exs
config :brando_graphql,
media_url_prefix: System.get_env("CDN_URL", "https://cdn.example.com/media/")Blueprint (graphql do ... end)
↓ Spark DSL Extension
Transformer (persists config at compile time)
↓
Schema macro (reads DSL, generates Absinthe AST)
↓
TypeBuilder (attributes + assets + relations → object types)
↓
Resolver (GraphQL args → Brando.Query args → context function)
↓
Preloads (auto-preload visible assets)
The package follows the same architecture as brando_json_api — a Spark DSL extension that reads blueprint metadata at compile time and generates the API layer.
No changes needed to Brando core. The extensions: mechanism in Brando.Blueprint forwards extensions to Spark, which handles the DSL registration.
MIT