API access to data
Gadget automatically generates and documents API endpoints that allow you to interact with your data. The API is how you fetch information for arbitrary queries (e.g. a query that is powered by the user's input on the frontend) or simple, predefined queries. For handling data in aggregates, or complex predefined queries, we recommend using computed fields.
Each time you add a new model to your project, Gadget instantly generates a public and private API to interact with the records stored in that table:
The public API is how your production application runs its business logic. The public API is described in actions that run both the database work we want our backend to do, along with any additional logic you specify. For example, a blog might have a createBlog action that persists blog data to the Gadget database before running a function to send a success email to the author.
The internal API is how you quickly interact with data (create, read, update, delete) at the database level, without running any additional business logic.
Your production application's frontend will typically make API calls to your Gadget backend via your public API, whereas your project's internal API is primarily used to fetch information to run the server side functions that power your actions.
Learn more about when to use the public API vs the internal API.
Data aggregation using the API
While, computed fields are typically the recommended approach to handling predefined aggregate queries (e.g. counts and sums) in Gadget, they cannot take in dynamic filters. In the event that your app has a query that requires dynamic filtering, you will need to aggregate the information you need at read-time by paginating, and produce your own filtered sums and counts.
Pagination
To produce read-time aggregates with dynamic filtering, you can fetch the desired records and paginate through them to get the desired aggregate data. This allows you to apply any filters you want to the GraphQL query and ensures you get an accurate count.
The page size is controlled using the first
or last
GraphQL field arguments.
Limitations
The downside to paginating through all records is performance. If the total count is 100000 records, walking through them in increments will take a long time. Especially considering that the maximum page size is 250 records and the default page size of 50 records.
When to paginate
- You are confident that you won't have a large number of total records in your collection
- You have dynamic filtering options that don't allow for counts to be pre-calculated
When to avoid pagination
- You may need to count a large number of records
- You don't need dynamic options for filtering your counts
For instance, if you want to get the count of all products in a Shopify store:
1export const run: ActionRun = async ({ api, record, params, logger }) => {2 const shopId = "my shop id";3 const productCount = 0;45 // api is an instance of my Gadget API client6 let products = await api.shopifyProduct.findMany({7 // use the maximum page size8 first: 250,9 // select the minimum amount of data from the model as needed10 select: {11 id: true,12 },13 // filter by the shop id14 // adding more filter conditions is usually a good idea, let the database handle filtering for you!15 filter: {16 shopId: {17 equals: shopId,18 },19 },20 });2122 productCount += products.length;2324 // paginate through all our products, 250 at a time25 while (products.hasNextPage) {26 products = await products.nextPage();27 productCount += products.length;28 }2930 logger.info(31 { productCount },32 "contains the total number of products for a given store"33 );34};
1export const run: ActionRun = async ({ api, record, params, logger }) => {2 const shopId = "my shop id";3 const productCount = 0;45 // api is an instance of my Gadget API client6 let products = await api.shopifyProduct.findMany({7 // use the maximum page size8 first: 250,9 // select the minimum amount of data from the model as needed10 select: {11 id: true,12 },13 // filter by the shop id14 // adding more filter conditions is usually a good idea, let the database handle filtering for you!15 filter: {16 shopId: {17 equals: shopId,18 },19 },20 });2122 productCount += products.length;2324 // paginate through all our products, 250 at a time25 while (products.hasNextPage) {26 products = await products.nextPage();27 productCount += products.length;28 }2930 logger.info(31 { productCount },32 "contains the total number of products for a given store"33 );34};
Paginating nested relationships
When you need to paginate through nested data relationships, efficiently utilize cursors to navigate from one model of related records to the next, ensuring seamless and performance-optimized data retrieval.
For instance, let's take a look at the example below: when fetching a class
record with paginated student data, then sequentially retrieving further student
records, enabling efficient navigation through large sets of nested data.
1const classRecord = await api.class.findFirst({2 select: {3 id: true,4 students: {5 pageInfo: {6 hasNextPage: true,7 endCursor: true,8 },9 edges: {10 node: {11 id: true,12 },13 },14 },15 },16});1718// Using the cursor to define the end of retrieval of records within the class model so we can go and fetch from the student model19const nextPageOfStudents = await api.student.findMany({20 first: 50,21 after: classRecord.students.pageInfo.endCursor,22 select: { id: true },23});2425// We now define that after the end of the retrieval we go right back to the next 50 results within the class model26const nextNextPageOfStudents = await api.student.findMany({27 first: 50,28 after: nextPageOfStudents.endCursor,29 select: { id: true },30});
1const classRecord = await api.class.findFirst({2 select: {3 id: true,4 students: {5 pageInfo: {6 hasNextPage: true,7 endCursor: true,8 },9 edges: {10 node: {11 id: true,12 },13 },14 },15 },16});1718// Using the cursor to define the end of retrieval of records within the class model so we can go and fetch from the student model19const nextPageOfStudents = await api.student.findMany({20 first: 50,21 after: classRecord.students.pageInfo.endCursor,22 select: { id: true },23});2425// We now define that after the end of the retrieval we go right back to the next 50 results within the class model26const nextNextPageOfStudents = await api.student.findMany({27 first: 50,28 after: nextPageOfStudents.endCursor,29 select: { id: true },30});
Pre-aggregating data
Within Gadget it is highly recommended to use computed fields in most instances, however, there are a few cases where you can consider pre-aggregating data at write time.
Large datasets: Pre-aggregation is particularly beneficial when dealing with large datasets where performing aggregations or complex calculations on the fly can be computationally expensive.
Grouping data: As Gadget doesn't currently support computed views (yet), there are cases where you might need to group data. In these instances, using pre-aggregation strategies with a combination of computed fields will be necessary.