computed fields are read-only field types that allow you to transform and aggregate data from multiple records. Gadget
optimizes the underlying query performance for these fields, making them ideal for aggregate queries such as counts and sums.
For example, if I wanted to sum the total price of all orders for a customer, I could create the following computed field on a customer model:
api/models/customer/totalSpend.gelly
gelly
field on customer {
sum(orders.totalPrice)
}
Then read the value in the totalSpend field on a customer record:
JavaScript
const customer = await api.customer.findOne("123", {
select: { id: true, totalSpend: true },
});
// Will log the computed value
console.log(customer.totalSpend);
const customer = await api.customer.findOne("123", {
select: { id: true, totalSpend: true },
});
// Will log the computed value
console.log(customer.totalSpend);
Computed field values are dynamically calculated so you don't need to worry about keeping them in sync with the underlying data. They remove the need to pre-aggregate data, and unchanged values are cached for speedy reads.
Computed fields are written in Gelly, Gadget's data access language.
Defining computed fields
To create and use a computed field:
Create the field: Add a computed field to your model in the Gadget editor.
Write the query: A .gelly file will automatically be created for your computed field. Write your Gelly expression in this file.
Query the field: The computed field is automatically added to your API. Read the computed field's value for any record in your model.
Computed field syntax
Computed fields have a standard format:
api/models/<model>/<fieldName>.gelly
gelly
field on <model> {
<expression>
}
The expression must evaluate to a single value. This value can be calculated by transforming data on a single record or by aggregating values across multiple related records.
Examples: Transform data on a single record
Simple arithmetic
If you have a business model with revenue and costs fields, you can calculate the profit with a computed field:
gelly
// api/models/business/profit.gelly
field on business {
revenue - costs
}
Each business record will have a profit field that computes the difference between revenue and costs.
String transformations
We can compute a customer record's fullName by concatenating the existing firstName and lastName fields:
api/models/customer/fullName.gelly
gelly
field on customer {
concat([firstName, " ", lastName])
}
Examples: Aggregate values across multiple related records
If you want to find the average size of all orders for a store, where the relationship is defined as storehas manyorders:
api/models/store/averageOrderSize.gelly
gelly
field on store {
avg(orders.totalPrice)
}
This calculates a value for each store by aggregating across related order records.
Reading computed field values
You can read computed fields using your app's API client or GraphQL API, or using the @gadgetinc/react hooks in your frontend.
Computed fields are included by default in responses to queries made using the public API:
Read a computed field from the frontend
const record = await api.someModel.findOne(123);
// the computed field is included by default
console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123");
// the computed field is included by default
console.log(data.computedField);
query {
someModel(id: 123) {
id
computedField
}
}
const record = await api.someModel.findOne(123);
// the computed field is included by default
console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123");
// the computed field is included by default
console.log(data.computedField);
You can exclude computed fields from public API responses by passing a [select] param to your API client:
Exclude a computed field from a read request
const record = await api.someModel.findOne(123, {
select: { id: true, computedField: false },
});
// the field is not included, will return null
console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: false } });
// the field is not included, will return null
console.log(data.computedField);
query {
someModel(id: 123) {
id # computedField is not included
}
}
const record = await api.someModel.findOne(123, {
select: { id: true, computedField: false },
});
// the field is not included, will return null
console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: false } });
// the field is not included, will return null
console.log(data.computedField);
You can also select computed fields from related records by nesting your select param:
Read a computed field from a related model
const record = await api.someModel.findOne(123, {
select: { id: true, relatedRecord: { id: true, computedField: true } },
});
// prints the computed field value
console.log(record.relatedRecord.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, relatedRecord: { id: true, computedField: true } } });
// prints the computed field value
console.log(data.relatedRecord.computedField);
query {
someModel(id: 123) {
id
name
relatedRecord {
id
computedField
}
}
}
const record = await api.someModel.findOne(123, {
select: { id: true, relatedRecord: { id: true, computedField: true } },
});
// prints the computed field value
console.log(record.relatedRecord.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, relatedRecord: { id: true, computedField: true } } });
// prints the computed field value
console.log(data.relatedRecord.computedField);
From the internal API
Computed fields are excluded by default when reading data using the internal API. A select parameter must be used to include them:
Read a computed field using the internal API
const record = await api.internal.someModel.findOne(123, {
select: { id: true, computedField: false },
});
// the field is not included, will return null
console.log(record.computedField);
const record = await api.internal.someModel.findOne(123, {
select: { id: true, computedField: false },
});
// the field is not included, will return null
console.log(record.computedField);
There is no way to add computed fields to this record. To access a computed field on this record in your action, you must load it manually using the API:
field on shopifyShop {
sum(cast(orders.totalPrice, type: "Number"), where: !isNull(orders.cancelledAt))
}
Current month's revenue within a Shopify Shop:
api/models/shopifyShop/totalRevenue.gelly
gelly
field on shopifyShop {
sum(cast(orders.totalPrice, type: "Number"), where: (orders.shopifyCreatedAt > dateTrunc(part: "month", date: now())) && isNull(orders.cancelledAt))
}
Compute the third side of a right triangle (Pythagorean theorem):
api/models/triangle/c.gelly
gelly
field on triangle {
sqrt(power(a, exponent: 2) + power(b, exponent: 2))
}
Performance
Gadget executes computed fields under the hood using SQL statements doing read-time aggregation. This means that if your computed field is aggregating over a significant number of records, it can take some time for your hosted database to execute the query. Gadget automatically optimizes the layout and indexes in your database to ensure your computed fields are computed quickly, but they can certainly add time to the duration of your API calls.
If you do not need the value of a computed field for a query, you can omit it from your query with select: { someComputedField: false } and it will not be computed. If performance becomes critical, you can also choose to pre-compute values and store them in normal fields at write time instead of computing them on read.