Computed fields 

computed fields are read-only field types that can transform and aggregate data from many records. Computed fields shift the burden of query performance from you onto Gadget, so for aggregate queries like counts and sums, Gadget recommends leveraging computed.

For example, if you wanted to display the total spend of a shopper on their profile, you could add a computed field called totalSpend to a customer model, and teach that field to store the results of a query that sums the total spend of the customer across all orders.

api/models/customer/totalSpend.gelly
gelly
field on customer { sum(orders.totalPrice) }

Then, you can fetch the totalSpend field from the customer record:

JavaScript
const customer = await api.customer.findOne(123, { select: { id: true, totalSpend: true }, }); console.log(customer.totalSpend); // will log the computed value for this expression, computed performantly
const customer = await api.customer.findOne(123, { select: { id: true, totalSpend: true }, }); console.log(customer.totalSpend); // will log the computed value for this expression, computed performantly

By using computed fields, you no longer have to:

  • aggregate data ahead of time when you write it
  • remember to re-run the query when any of its inputs change (e.g. if a new order was placed by the customer)
  • keep the query performant or manage database indexes

Queries to the Gadget database via computed fields are written in Gelly, Gadget's expression language. Gelly is a simple and easy-to-learn language that closely resembles SQL and GraphQL. Learn more about Gelly in the Gelly guide.

Computed views vs computed fields 

Computed fields compute one value for each record of a model. Computed views beta return one overall result, which can be whatever shape is most useful to you. Use computed fields when your computation is naturally anchored to one model in particular and should be easily available for each record, and use computed views otherwise.

Computed fields can also easily be selected at the same time as other fields of your models, whereas computed views must be fetched explicitly on their own.

Snippet syntax 

Computed field Gelly snippets define a new field with the field keyword, and then have one expression within them. This expression can use other fields from the model to do rich computation.

For example, we can compute a customer record's fullName with the following Gelly snippet that uses the firstName and lastName string fields:

api/models/customer/fullName.gelly
gelly
field on customer { concat([firstName, " ", lastName]) }

Computed fields can do arithmetic as well, like computing a post's score from its upvotes and downvotes:

api/models/post/score.gelly
gelly
field on post { (upvotes - downvotes) * 100 }

Computed fields can use data from related models, like computing the total spend of a customer:

api/models/customer/totalSpend.gelly
gelly
field on customer { sum(orders.totalPrice) }

Using a computed field 

You can create and use a computed field in three steps:

  1. Add the field to the main model whose data you want to compute with and select the computed field type
  2. Define the code for the field in the .gelly snippet file created for your field
  3. Query the computed field in your API along with any other fields you like.

For example, say your app has a model named business with number fields revenue and costs. We can add a new computed field to the business model called profit with this Gelly snippet:

api/models/business/profit.gelly
gelly
field on business { revenue - costs }

Then, we can query this field in our API like so:

JavaScript
await api.business.findMany({ select: { name: true, profit: true, }, });
await api.business.findMany({ select: { name: true, profit: true, }, });

Your Gelly snippet can support a wide variety of operators and functions, for more information refer to the Gelly reference.

Accessing computed fields on the frontend through your API 

computed fields can be accessed the same way as all your other fields from the frontend: using your API client or your auto-generated GraphQL API.

const record = await api.someModel.findOne(123, { select: { id: true, computedField: true }, }); // => value from the backend console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: true } }); // => value from the backend console.log(data.computedField);
query { someModel(id: 123) { id computedField } }
const record = await api.someModel.findOne(123, { select: { id: true, computedField: true }, }); // => value from the backend console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: true } }); // => value from the backend console.log(data.computedField);

You can choose to exclude computed fields by passing a [select] param to your API client:

const record = await api.someModel.findOne(123, { select: { id: true, computedField: false }, }); // => null console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: false } }); // => 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 }, }); // => null console.log(record.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, computedField: false } }); // => null console.log(data.computedField);

And you can select computed fields from related records by nesting your select param:

const record = await api.someModel.findOne(123, { select: { id: true, name: true, relatedRecord: { id: true, computedField: true } }, }); // => value from the backend console.log(record.relatedRecord.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, name: true, relatedRecord: { id: true, computedField: true } } }); // => null console.log(data.relatedRecord.computedField);
query { someModel(id: 123) { id name relatedRecord { id computedField } } }
const record = await api.someModel.findOne(123, { select: { id: true, name: true, relatedRecord: { id: true, computedField: true } }, }); // => value from the backend console.log(record.relatedRecord.computedField);
const [{data, fetching, error}] = useFindOne(api.someModel, "123", { select: { id: true, name: true, relatedRecord: { id: true, computedField: true } } }); // => null console.log(data.relatedRecord.computedField);

On the frontend, computed fields are included by default if you don't pass a select param.

Accessing computed fields on the backend 

On the backend, computed fields are excluded by default on the record object.

In order to keep your actions fast by default, Gadget does not include computed fields in the record object passed to your action code. Instead, you must load any computed field values manually if required for your action. See the performance section for more details.

For example, if working on the update action of a customer model, you wanted to access the value of the customer object's fullName computed field, you can load it with an API call:

JavaScript
import { applyParams, save } from "gadget-server"; import type { ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ api, record, params }) => { const reloadedRecord = await api.customer.findOne(record.id, { select: { id: true, fullName: true }, }); // do something with reloadedRecord.fullName // ... applyParams({ record, params }); await save({ record }); }; export const options: ActionOptions = { actionType: "update", };
import { applyParams, save } from "gadget-server"; import type { ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ api, record, params }) => { const reloadedRecord = await api.customer.findOne(record.id, { select: { id: true, fullName: true }, }); // do something with reloadedRecord.fullName // ... applyParams({ record, params }); await save({ record }); }; export const options: ActionOptions = { actionType: "update", };

Example computed fields 

Check spamminess of a post in a blog, where the post model has a markedAsSpam boolean column, and then a shadowBanned boolean column on a related author model:

api/models/post/isSpam.gelly
gelly
field on post { author.shadowBanned || markedAsSpam }

Count the number of comments on a post:

api/models/post/commentCount.gelly
gelly
field on post { count(comments) }

In an AI app with a chatMessage model that has a tokenCount number field describing how many tokens were used to generate the message, count the total number of tokens a user has used in the past 30 days:

api/models/user/chatTokenCount.gelly
gelly
field on user { sum(chatMessages.tokenCount, where: chatMessages.createdAt > now() - interval("30 days")) }

Count the total number of published products for a Shopify Shop when using the Shopify Connection:

api/models/shopifyShop/publishedProductCount.gelly
gelly
field on shopifyShop { count(products, where: !isNull(products.publishedAt)) }

Total the revenue of a Shopify Shop:

api/models/shopifyShop/totalRevenue.gelly
gelly
field on shopifyShop { sum(cast(orders.totalPrice, type: "Number")) }

Total the lost revenue of a Shopify Shop for orders that have been canceled:

api/models/shopifyShop/totalCancelledRevenue.gelly
gelly
field on shopifyShop { sum(cast(orders.totalPrice, type: "Number"), where: !isNull(orders.cancelledAt)) }

Total the current month's revenue within 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 length of the third side of a triangle with number fields for sides a and b:

api/models/triangle/c.gelly
gelly
field on triangle { sqrt(power(a, exponent: 2) + power(b, exponent: 2)) }

Pricing 

Including or excluding a computed field from your API calls in Gadget can change the price of the API call by making it take more or less time to execute. Computed fields are most often fast, but complicated expressions can take a long time to compute and increase the duration of your API calls.

For more information on Gadget's billing, see the Usage and billing guide.

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 don't 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 a major issue, you can also switch to pre-computing your values and storing them in normal fields at write time, instead of reading them each time.

Was this page helpful?