Skip to content

Fields

Every field of an Item (or object) type requires at least:

  1. A name, which is used in generated code and can be referenced in key path templates.
  2. A type which determines what kind of values can go in the field, how the field is stored, and what type the generated code will use for it.
  3. A fieldNum (field number) which must be unique within fields and which must never be reused. Field numbers are used to store and transfer data more compactly.

Besides that, there are some optional properties:

  1. required: By default, all fields are required, meaning it must be set or the item is invalid. The definition of “set” is a value other than the zero value for that type. Set required: false to allow a field to be unset.
  2. valid: A validation expression for the field, expressed in the CEL language. This is in addition to any validation inherent in the data type itself.
  3. deprecated: This will cause a deprecation annotation to be added to the generated code.
  4. fromMetadata which populates the field from Item metadata.
  5. initialValue which automatically sets the field value when the Item is created.

For example, here’s a User item type with a few fields:

export const User = itemType("User", {
keyPath: "/user-:id",
fields: {
id: {
type: uuid,
initialValue: "uuid",
fieldNum: 1,
},
displayName: {
type: string,
fieldNum: 2,
required: false, // it's OK to not set a name
},
email: {
type: string,
fieldNum: 3,
valid: 'this.matches("[^@]+@[^@]+")',
},
lastLoginDate: {
type: timestampSeconds,
fieldNum: 4,
},
numLogins: {
type: uint,
fieldNum: 5,
},
},
});

Initial Value Fields

There are times when you want your database to choose a value for you. One common pattern is picking a unique identifier for an Item. For example, in the relational database world this is often an auto-incrementing integer (eg: AUTO_INCREMENT in MySQL or SERIAL/SEQUENCE in Postgres). When StatelyDB chooses an identifier via initialValue, it guarantees that no Item already exists with the same key path. You can specify the initialValue property when configuring a field, with the following variants:

uuid

initialValue: 'uuid' produces a globally unique UUIDv4. UUIDs are a good choice for when you need an ID to be globally unique no matter where the ID is in its key path. The type of the field must also be uuid.

rand53

initialValue: 'rand53' generates an unsigned random 53-bit integer. 53-bit integers are the maximum safe integer size in Javascript, so this is a good choice if you are primarily using StatelyDB in a Javascript environment and want maximum compatibility. It is also a 50% smaller alternative to UUIDs that can still be unique within your Store. The type of the field must be a uint. Note that unlike UUIDs, rand53 values may repeat in different key paths—for example you might have /user-1234 and /post-1234 and /post-6789/comment-1234. The only thing that matters here is that the entire key path is unique.

sequence

initialValue: 'sequence' generates an unsigned, monotonically increasing integer starting from 1, where the counter is unique per parent Key Path. This is ideal for when data should have a consistent order based on insertion order, such as a list of messages within a conversation. The type of the field must be an integer type, ideally uint. This can only be used for items within a Group—it cannot be used in a Group Key.

For example, if we have the key path template /customer-:customerID/order-:orderId for the Order Item type. orderId can be a sequence, and each order ID for a customer will count up 1, 2, 3, and so on. You can have another key path template for a LineItem that looks like /customer-:customerId/order-:orderId/li-:lineItemId and lineItemId can be a sequence, and each line item within an Order will count up 1, 2, 3, and so on. In this setup, the third line item for the fifth order a Customer makes would have the key path /customer-12957/order-5/li-3. However, customerId cannot be a sequence, because it is a Group Key. If we allowed Group Keys to be sequences, then Groups would no longer be well-partitioned, which could impact scaling.

Metadata Fields

Every Item in StatelyDB automatically tracks the following metadata:

  • createdAtTime - The timestamp in microseconds when the Item was created.
  • lastModifiedTime - The timestamp in microseconds when the Item was last modified.
  • createdAtVersion - The Group Version of the Item’s Group minted when the Item was created.
  • lastModifiedVersion - The Group Version of the Item’s Group minted when the Item was last modified.

By default these fields are not exposed. In order to use these fields you need to define a field in the Item type definition in your schema, and map it to one of these metadata with fromMetadata. Any field with fromMetadata is read-only and any values written to it will be ignored.

Note: Metadata fields still require the correct type so that we know how to present the underlying data. For instance, it is necessary to choose the granularity of timestamp desired to access any of the timestamp-metadata fields.

Here is an example of a model that uses all of these metadata fields:

import {
itemType,
timestampMicroseconds,
timestampSeconds,
uint,
uuid,
} from "@stately-cloud/schema";
export const MyItemType = itemType("MyItemType", {
keyPath: "/myitemtype-:id",
fields: {
id: {
type: uuid,
fieldNum: 1,
},
creationTime: {
type: timestampSeconds,
fieldNum: 2,
fromMetadata: "createdAtTime",
},
modifiedTime: {
type: timestampMicroseconds,
fieldNum: 3,
fromMetadata: "lastModifiedAtTime",
},
createdAtVersion: {
type: uint,
fieldNum: 4,
fromMetadata: "createdAtVersion",
},
modifiedAtVersion: {
type: uint,
fieldNum: 5,
fromMetadata: "lastModifiedAtVersion",
},
},
});