This page explains how Custom Entity queries were built using the storage module from the @forge/api package.
This module is no longer supported or available as of June 22, 2026. The information on this page is only being provided as a reference for developers migrating to the @forge/kvs package.
If your app already uses the supported @forge/kvs package, see Querying the Custom Entity Store instead.
If you are still using the legacy storage module and need to migrate, see Migrating to @forge/kvs from unsupported storage module.
The legacy storage module supported building complex queries against
data stored in the Custom Entity Store
using a wide variety of filters and conditions.
The Forge API package was imported as follows:
1 2import { storage } from '@forge/api';
Each installation of an app is subject to the API's quotas and limits. See Storage quotas and Storage limits for more details.
Using Forge’s persistent hosted storage through the @forge/api package required the storage:app scope in the manifest file:
1 2 3 4permissions: scopes: - storage:app
For more information about scopes, see Permissions.
Before storing data in the Custom Entity Store, apps declare custom entities and indexes in the manifest file. Custom entities are user-defined data structures for storing app data. Forge's storage API lets you query data stored in these structures using a wide array of query conditions. These query conditions make it possible to build advanced, complex queries to suit your app's operations.
For information about storing data to the Custom Entity Store, see Storing data in custom entities .
For a detailed tutorial on storing and querying structured data through custom entities, see Use custom entities to store structured data.
All complex queries operate on a custom entity's index. Complex queries followed the same basic signature:
1 2 3 4 5await storage .entity("<custom-entity>") .query() .index()
This structure contained all the required methods for a complex query. The entity method set which custom entity to query, and index set which of that entity's indexes to query. Each query could only target one index from one custom entity.
When using indexes that featured a partition, a value had to be specified to match the parameter's attribute:
1 2 3 4 5 6 7await storage .entity("<custom-entity>") .query() .index("<index-name>", { partition: ["<value>"] })
If an index's partition had multiple attributes, a value had to be set for each attribute, in the order they were declared in the index. For example, consider the following index:
1 2 3 4 5 6 7 8indexes: - name: by-gender-and-age range: - employmentyear partition: - gender - age
An appropriate query for this index would have been:
1 2 3 4 5 6 7 8 9await storage .entity("employee") .query() .index("by-gender-and-age", { partition: ["male", 20] }) .where(WhereConditions.isGreaterThan(2003)) .getMany()
This query fetched employees who were:
employmentyear is higher than 2003).Every complex query returned up to 10 values by default. This could be increased to a maximum of 100 using query.limit.
While index filtered matches to an index's partition, where filtered against an index's range. The where filter was imported as follows:
1 2import { WhereConditions } from '@forge/api';
1 2.where(WhereConditions.<condition>("<value>"))
The where filtering method supported the following conditions:
beginsWithbetweenequalsToisGreaterThan, isLessThanisGreaterThanOrEqualTo, isLessThanOrEqualToThe index and where methods could only be used once per query. The andFilter and
orFilter methods allowed additional conditions to be added to a query.
Either filtering method was imported as follows:
1 2import { FilterConditions } from '@forge/api';
Each filtering method used the following signatures:
andFilter: all conditions had to be matched.
1 2.andFilter("<attribute>", FilterConditions.<condition>("<value>"))
orFilter: only one condition had to be matched.
1 2.orFilter("<attribute>", FilterConditions.<condition>("<value>"))
Within the same query, multiple andFilter or orFilter methods could be used. However, both methods could not be used within the same query.
In addition, the andFilter and orFilter methods were in-memory filters. Using them could sometimes produce pages with no results, with the cursor pointing to the next page where actual results existed.
Both filtering methods supported the following conditions:
beginsWithbetweenequalsToisGreaterThan, isLessThanisGreaterThanOrEqualTo, isLessThanOrEqualToexists, doesNotExistcontains, doesNotContainnotEqualsToThe sort method displayed results in either ascending (ASC) or descending (DESC) order:
1 2.sort(SortOrder.<"ASC|DESC">)
By default, results were displayed in ascending order.
Returns a new Query with a limit on how many matching values get returned. The query
API returns up to 10 values by default, this can be increased to a maximum of 100.
1 2query.limit(limit: number): Query
Returns a new Query that will start after the provided
cursor. Cursors enable your
app to fetch subsequent pages of results after completing an initial query.
Cursors are returned from the getMany query API.
1 2query.cursor(after: string): Query;
Execute the query and return a list of results up to the provided limit in length. This method returns both the array of results and a cursor that's used to fetch subsequent pages of results.
1 2 3 4 5 6 7 8 9 10 11 12query.getMany(): Promise<ListResult>; interface ListResult { results: Result[]; nextCursor?: string; } export interface Result { key: string; value: object; }
Execute the query and get the first matching result, if any matches exist. If there
is no match, the result resolves to undefined.
1 2 3 4 5 6 7query.getOne(): Promise<Result | undefined>; export interface Result { key: string; value: object; }
The following manifest.yml excerpt shows a custom entity named employee with several attributes and indexes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29app: id: "ari:cloud:ecosystem::app/406d303d-0393-4ec4-ad7c-1435be94583a" storage: entities: - name: employee attributes: surname: type: string age: type: integer employmentyear: type: integer gender: type: string nationality: type: string indexes: - surname - employmentyear - name: by-age range: - age - name: by-age-per-gender partition: - gender range: - age
This entity also creates four indexes based on the following employee attributes:
surnameemploymentyearage (further optimized for filtering according to different age ranges)age per gender (further optimized for filtering according to age ranges for each gender)Using the previous section's example entity and its indexes, the following queries demonstrated the use of each method:
Targeted the surname index of the employee entity.
1 2 3 4 5 6await storage .entity("employee") .query() .index("surname") .getMany()
Targeted the by-age index, which used age as its range. The where method limited matches to employees above the age of 30. Results were displayed in descending order.
1 2 3 4 5 6 7await storage .entity("employee") .query().index("by-age") .where(WhereConditions.isGreaterThan(30)) .sort(SortOrder.DESC) .getMany()
Targeted the by-age-per-gender index, and limited matches to female employees.
1 2 3 4 5 6 7 8await storage .entity("employee") .query() .index("by-age-per-gender", { partition: ["female"] }) .getMany()
Using the by-age-per-gender index, this query limited matches to female Australian employees above the age of 30 who were hired after 2020.
1 2 3 4 5 6 7 8 9 10 11await storage .entity("employee") .query() .index("by-age-per-gender", { partition: ["female"] }) .where(WhereConditions.isGreaterThan(30)) .andFilter("employmentyear", FilterConditions.isGreaterThan(2020)) .andFilter("nationality", FilterConditions.equalsTo("Australian")) .getMany()
Rate this page: