Check out v4 of the Fauna CLI
v4 of the Fauna CLI is now GA. The new version introduces enhancements to the developer experience, including an improved authentication workflow. To get started, check out the CLI v4 quick start. Migrating from v3 of the CLI? See the CLI migration guide. |
Query composition
The Fauna client drivers compose queries using FQL template strings. You can interpolate variables, including other FQL template strings, into the template strings to compose dynamic queries.
For example, using the JavaScript driver:
import { Client, fql, FaunaError } from "fauna";
const client = new Client({
secret: 'FAUNA_SECRET'
});
// Create a native JS var.
const name = "avocados";
// Pass the var to an FQL query.
let query = fql`Product.where(.name == ${name})`;
let response = await client.query( query );
console.log(response.data);
client.close();
To prevent injection attacks, the drivers use the wire protocol encode interpolated variables to an appropriate FQL type before passing the query to the Core HTTP API’s Query endpoint.
Example: Product catalog search
An e-commerce app has a product catalog that users search by various criteria, such as product category, price range, or other properties. A user may search by product type, minimum price, maximum price, or any combination of these criteria.
The following dynamic query handles various combinations of these search parameters:
import { Client, fql, FaunaError } from "fauna";
const client = new Client({
secret: 'FAUNA_SECRET'
});
// Set up parameterized inclusive, exclusive,
// or exact matches.
const operators = {
eq: (field, value) => fql`x => x[${field}] == ${value}`,
gt: (field, value) => fql`x => x[${field}] > ${value}`,
gte: (field, value) => fql`x => x[${field}] >= ${value}`,
lt: (field, value) => fql`x => x[${field}] < ${value}`,
lte: (field, value) => fql`x => x[${field}] <= ${value}`,
}
// Define the product search parameters set in the UI.
const predicates = [
{ field: "type", operator: "eq", value: "food" },
{ field: "price", operator: "lte", value: 10000 },
{ field: "price", operator: "gte", value: 10 },
];
const base_query = fql`Product`
// Chain successive `where()` methods to the query,
// effectively ANDing the predicates.
const dynamicQuery = predicates.reduce(
(acc, val) =>
fql`${acc}.where( ${operators[val.operator] ( val.field, val.value)})`,
base_query
)
let response = await client.query(dynamicQuery)
console.log(response.data)
client.close();
For simplicity, the example uses
collection.where()
, which
requires a read of each document and isn’t performant on large datasets. For
better performance, use indexes instead.
Dynamic filtering using advanced query composition
Complex applications may need to handle arbitrary combinations of search criteria. In these cases, you can use query composition to dynamically apply indexes and filters to queries.
The following template uses query composition to:
-
Automatically select the most selective index
-
Apply remaining criteria as filters in priority order
-
Support both index-based and filter-based search patterns
The template uses TypeScript and the JavaScript driver. A similar approach can be used with any Fauna client driver.
/**
* A Javascript object with a sorted list of indexes or filters.
*
* Javascript maintains key order for objects.
* Sort items in the map from most to least selective.
*/
type QueryMap = Record<string, (...args: any[]) => Query>
/** Object to represent a search argument.
*
* Contains the name of the index to use and the arguments
* to pass to it.
*
* Example:
* { name: "by_name", args: ["limes"] }
* { name: "range_price", args: [{ from: 100, to: 500 }] }
*/
type SearchTerm = {
name: string
args: any[]
}
/**
* Composes a query by prioritizing the most selective index and then
* applying filters.
*
* @param default_query - The initial query to which indexes and filters are applied.
* @param index_map - A map of index names to functions that generate query components.
* @param filter_map - A map of filter names to functions that generate query components.
* @param search_terms - An array of search terms that specify the type and arguments
* for composing the query.
* @returns The composed query after applying all relevant indices and filters.
*/
const build_search = (
default_query: Query,
index_map: QueryMap,
filter_map: QueryMap,
search_terms: SearchTerm[]
): Query => {
const _search_terms = [...search_terms]
// Initialize a default query. Used if no other indexes are applicable.
let query: Query = default_query
// Iterate through the index map, from most to least selective.
build_index_query: for (const index_name of Object.keys(
index_map
)) {
// Iterate through each search term to check if it matches the highest priority index.
for (const search_term of _search_terms) {
// If a match is found, update the query. Then remove the search term from the
// list and break out of the loop.
if (index_name === search_term.name) {
query = index_map[search_term.name](...search_term.args)
_search_terms.splice(_search_terms.indexOf(search_term), 1)
break build_index_query
}
}
}
// Iterate through the filter map, from most to least selective.
for (const filter_name of Object.keys(filter_map)) {
// Iterate through each search term to check if it matches the highest priority filter.
for (const search_term of _search_terms) {
// If a match is found, update the query. Then remove the search term from the list.
if (filter_name === search_term.name) {
const filter = filter_map[search_term.name](...search_term.args)
query = fql`${query}${filter}`
_search_terms.splice(_search_terms.indexOf(search_term), 1)
}
}
}
// If there are remaining search terms, you can't build the full query.
if (_search_terms.length > 0) {
throw new Error("Unable to build query")
}
return query
}
The following example implements the template using the Fauna Dashboard's demo data:
// Implementation of `index_map` from the template.
// Sort items in the map from most to least selective.
const product_index_priority_map: QueryMap = {
by_order: (id: string) =>
fql`Order.byId(${id})!.items.map(.product!)`,
by_name: (name: string) => fql`Product.byName(${name})`,
by_category: (category: string) =>
fql`Product.byCategory(Category.byName(${category}).first()!)`,
range_price: (range: { from?: number; to?: number }) =>
fql`Product.sortedByPriceLowToHigh(${range})`,
}
// Implementation of `filter_map` from the template.
// Sort items in the map from most to least selective.
const product_filter_map: QueryMap = {
by_name: (name: string) => fql`.where(.name == ${name})`,
by_category: (category: string) =>
fql`.where(.category == Category.byName(${category}).first()!)`,
range_price: ({ from, to }: { from?: number; to?: number }) => {
// Dynamically filter products by price range.
if (from && to) {
return fql`.where(.price >= ${from} && .price <= ${to})`
} else if (from) {
return fql`.where(.price >= ${from})`
} else if (to) {
return fql`.where(.price <= ${to})`
}
return fql``
},
}
// Hybrid implementation of `index_map` and `filter_map` from the template.
// Combines filters and indexes to compose FQL query fragments.
// Sort items in the map from most to least selective.
const product_filter_with_indexes_map: QueryMap = {
by_name: (name: string) =>
fql`.where(doc => Product.byName(${name}).includes(doc))`,
by_category: (category: string) =>
fql`.where(doc => Product.byCategory(Category.byName(${category}).first()!).includes(doc))`,
range_price: (range: { from?: number; to?: number }) =>
fql`.where(doc => Product.sortedByPriceLowToHigh(${range}).includes(doc))`,
}
const order_id = (await client.query(fql`Order.all().first()!`))
.data.id
const query = build_search(
fql`Product.all()`,
product_index_priority_map,
product_filter_with_indexes_map,
[
// { type: "by", name: "name", args: ["limes"] },
// { type: "by", name: "category", args: ["produce"] },
{ type: "range", name: "price", args: [{ to: 1000 }] },
{ type: "by", name: "order", args: [order_id] },
]
)
const res = await client.query(query)
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!