github clockworklabs/SpacetimeDB v1.8.0
Release v1.8.0 - Module Defined Views

16 hours ago

Module Defined Views

The shipping continues! This time we have Module Defined Views or just "views". Views are a simple, but incredibly expressive way to define custom and intricate read permissioning for your tables.

Views, which are inspired by a similar concept from SQL, are virtual tables that are defined by new "view functions" in your module and derived from other tables or parameters.

Views are defined by read-only procedural, functions in the language of your module. This function returns data derived from your database tables. You can then query and subscribe to this view as you would any normal database table and it will be updated automatically in realtime.

Here's a look at the syntax for defining a view in the various module languages:

Rust

Declare with #[view]. First argument is a view context (&ViewContext or &AnonymousViewContext). Return Option<T> (0–1 row) or Vec<T> (many rows).

#[view(name = my_player, public)]
fn my_player(ctx: &ViewContext) -> Option<Player> {
    ctx.db.player().identity().find(ctx.sender)
}

#[view(name = players_for_level, public)]
fn players_for_level(ctx: &AnonymousViewContext) -> Vec<Player> {
    ctx.db
        .player_level()
        .level()
        .filter(2u64) // players for level 2
        .map(|player| {
            ctx.db
                .player()
                .id()
                .find(player.player_id)
        })
        .collect()
}

Notes

  • A name is required.
  • Only the context parameter is allowed; no extra args (yet).
  • The context provides a read-only view of the database
  • Mutations are not allowed
  • Full table scans are not allowed

C#

Use [SpacetimeDB.View] with ViewContext or AnonymousViewContext. Return a single row as T? or many rows as List<T> / T[].

[SpacetimeDB.View(Name = "my_player", Public = true)]
public static Player? MyPlayer(ViewContext ctx) =>
    ctx.Db.Player.Identity.Find(ctx.Sender) as Player;

[SpacetimeDB.View(Name = "players_for_level", Public = true)]
public static List<Player> PlayerLocations(AnonymousViewContext ctx) {
    var rows = new List<Player>();
    foreach (var player in ctx.Db.PlayerLevel.Level.Filter(2))
    {
        if (ctx.Db.Player.Id.Find(player.PlayerId) is Player p)
        {
            rows.Add(p);
        }
    }
    return rows;
}

TypeScript

Register with schema.view(...) or schema.anonymousView(...). Use t.option(row) for 0–1 row or t.array(row) for many rows.

spacetimedb.view(
  { name: 'my_player', public: true },
  t.option(players.row()),
  (ctx) => {
  return ctx.db.players.identity.find(ctx.sender) ?? null;
  }
);

spacetimedb.anonymousView(
  { name: 'players_for_level', public: true },
  t.option(players.row()),
  (ctx) => {
    const out = [];
    for (const pl of ctx.db.playerLevels.level.find(2)) {
      const p = ctx.db.players.id.find(pl.player_id);
      if (p) out.push(p);
    }
    return out;
  }
);

Row-level security rules

Currently procedurally defined view functions are limited to index probing tables so that we can efficiently compute the real-time delta for procedural functions. However, we also plan to shortly add the ability to return typed queries from view functions which will allow you to define performant, incrementally evaluated queries which execute full tables scans.

This functionality will make views strictly more expressive and powerful than the existing unstable RLS (row-level security) rules API that we introduced earlier this year. As such we will be deprecating the RLS API in favor of the view API. Here is an idea (not final API) of what that might look like in TypeScript

spacetimedb.view(
  { name: 'high_level_players', public: true },
  t.query(players.row()),
  (ctx) => {
  return ctx.from(ctx.db.player).where(player => gt(player.level, 50))
  }
);

What's Changed

Full Changelog: v1.7.0...v1.8.0

Don't miss a new SpacetimeDB release

NewReleases is sending notifications on new releases.