Chromia
Start Building

12 / 27Module 1: A Rell backend with FT accountsBeginner

Basic queries

Queries are how you read data from a Chromia blockchain. You will build and test each one in turn.

Your dapp needs queries that:

  1. Show a user's username.
  2. Return the number of followers a user has.
  3. Return the number of users a user follows.
  4. Tell you whether one user follows another.
  5. Fetch a list of posts.
  6. List every user.

The steps below show how to write and use these queries in Chromia:

User and follower queries

You declare a query like a function, using the query keyword. Start with a query that returns a username that uniquely identifies the user:

rell/src/news_feed/queries.rell
query get_user_name(user_id: byte_array): text {  return user @ { user_id } ("%s#%s".format(.name, .id.to_hex().sub(0, 5)));}

The query looks up the user and builds a text value by joining the name with the first five characters of the ID's hex form. Next, write two queries: one that counts a user's followers and one that counts the users they follow.

rell/src/news_feed/queries.rell
query get_followers_count(user_id: byte_array): integer {  return follower @ { .user == user @ { user_id } } (@sum 1);}query get_following_count(user_id: byte_array): integer {  return follower @ { .follower == user @ { user_id } } (@sum 1);}

The two queries differ only in how they filter the follower database query. Rell can infer the integer return type from the return statement, so you can drop it and shorten the queries:

rell
query get_followers_count(user_id: byte_array) =  follower @ { .user == user @ { user_id } } (@sum 1);

To find out whether a follower entity exists, write this query:

rell/src/news_feed/queries.rell
query is_following(my_id: byte_array, your_id: byte_array) =  exists(follower @? { .user.id == your_id, .follower.id == my_id });

Query posts with pagination

Now you need a query that returns posts from the users a given user follows. The result set can grow large, so you add pagination to keep it manageable. Order the posts from newest to oldest.

To carry post data, define a struct named post_dto:

src/news_feed/dto.rell
struct post_dto {  timestamp;  user: struct<user>;  content: text;}

It mirrors the post entity, except that the user field uses a slightly different format. A struct holds an entity's data in memory, so every field is loaded and ready to use.

Retrieve posts

The query joins tables, filters the data, and applies pagination to get the posts you want:

rell/src/news_feed/queries.rell
query get_posts(  user_id: byte_array,  pointer: integer,  n_posts: integer): (pointer: integer, posts: list<post_dto>) {  val posts = (user, follower, post) @* {    user.id == user_id,    follower.follower == user,    post.user == follower.user  } (    @sort_desc @omit post.rowid,    post_dto(        post.timestamp,        post.content,        user = post.user.to_struct()    )  ) offset pointer limit n_posts;  return (    pointer = pointer + posts.size(),    posts = posts  );}

How the query works:

  • user_id identifies the user whose feed you want: posts by the users they follow.
  • It joins the user, follower, and post tables to collect the required data.
  • It sorts posts by creation timestamp, descending, so the newest come first.
  • It builds a post_dto for each post, including a structured representation of the user.
  • It takes an offset to skip posts and a limit to cap how many are returned.

The descending sort happens in the database query so the latest posts are fetched first, but the timestamp itself is not included in the returned structure. You could paginate by timestamp instead; using an offset keeps things simpler.

Return results

The query returns a named tuple with two parts:

  • pointer: the index where the next query should begin.
  • posts: a list of post_dto objects holding the fetched posts.

This gives you paginated access to posts by the users a user follows, which makes the data easier to handle and display in your dapp.

Query user list

Listing every user takes a query that reuses the name formatting from get_user_name and the pagination from get_posts:

rell/src/news_feed/queries.rell
query get_users(pointer: integer, n_users: integer) {  val users = user @* {} (name = "%s#%s".format(.name, .id.to_hex().sub(0, 5)), id = .id) offset pointer limit n_users;  return (    pointer = pointer + users.size(),    users = users  );}

Test the queries

Next, confirm the queries behave correctly. Begin with a basic test for get_user_name:

src/test/news_feed_test.rell
function test_user_name() {    rell.test.tx()        .op(create_user("Alice", alice))        .run();    assert_equals(get_user_name(alice), "Alice#02466");    val users_result = get_users(0, 20);    assert_equals(users_result.pointer, 1);    assert_equals(users_result.users.size(), 1);    assert_true(users_result.users @* {} (.name).contains("Alice#02466"));}

Note that the @-operator works on lists here.

Then check the follower counts with a second test:

src/test/news_feed_test.rell
val charlie = rell.test.pubkeys.charlie;function test_follower_calculation() {    rell.test.tx()        .op(create_user("Alice", alice))        .op(create_user("Bob", bob))        .op(create_user("Charlie", charlie))        .run();    rell.test.tx()        .op(follow_user(alice, bob))        .op(follow_user(alice, charlie))        .run();    assert_true(is_following(alice, bob));    assert_true(is_following(alice, charlie));    assert_equals(get_following_count(alice), 2);    assert_equals(get_following_count(bob), 0);    assert_equals(get_followers_count(alice), 0);    assert_equals(get_followers_count(bob), 1);}

What this test does:

  • Creates three users: Alice, Bob, and Charlie.
  • Makes Alice follow both Bob and Charlie.
  • Uses assert_equals to confirm the queries report the right follower and following counts.

Test pagination for posts

Finally, test post pagination. The test creates users, sets up follow relationships, and then adds posts.

src/test/news_feed_test.rell
function test_pagination_of_posts() {    rell.test.tx()        .op(create_user("Alice", alice))        .op(create_user("Bob", bob)).run();    rell.test.tx()        .op(ft_auth_operation_for(alice))        .op(follow_user(bob))        .sign(alice_kp)        .run();    for (i in range(5)) {        rell.test.tx()            .op(ft_auth_operation_for(bob))            .op(make_post("Content %d".format(i)))            .sign(bob_kp)            .run();    }    val initial_posts = get_posts(alice, 0, 4);    assert_equals(initial_posts.pointer, 4);    assert_equals(initial_posts.posts.size(), 4);    val last_posts = get_posts(alice, initial_posts.pointer, 4);    assert_equals(last_posts.pointer, 5);    assert_equals(last_posts.posts.size(), 1);}

What this test does:

  • Creates the users Alice and Bob.
  • Alice follows Bob, and Bob publishes five posts.
  • Fetches Alice's feed in pages of 4: the first page returns 4 posts and pointer 4, the second returns the last post and pointer 5.
Written for this site from Chromia’s public course. Code samples are the course’s own.