Skip to content

Key Paths

Each item type must have at least one key path that determines where it is stored and how you can access it. The first Key Path is the primary Key Path; additional Key Paths are aliases.

  • A key path has one or more “segments”, which are in the form /namespace-:field. Note that each segment starts with /. For example, /course-:courseId/year-:academicYear/quarter-:academicQuarter has three segments.
  • Each segment consists of a namespace and an ID. In a key path template, the ID is a field reference.
    • The namespace can be are any combination of letters or underscores (no hyphens, numbers, or other special characters). For example course, year, and quarter are all namespaces.
    • When defining key paths for an item type, the ID is a field reference which will be replaced with the value of a field. The field references are the name of a field in your item type preceded by a colon (:). For example :courseId, :academicYear, and :academicQuarter.
    • The ID is optional on the last segment of a key path (but must be included in the first segment). For example, a key path of /course-:courseId/syllabus could be used for an item that you only have one of per course.
  • The first segment in a key path is also called the group key and it determines how the item is partitioned in the store. Every item’s key path must contain at least one segment so it can be located on a partition.

Let’s see how this looks with example Item types Student and Course:

import { itemType, uint, string, arrayOf } from "@stately-cloud/schema";
import { CourseID, Quarter } from "./my-types.js";
itemType("Course", {
keyPath: [
"/course-:courseId/year-:academicYear/quarter-:academicQuarter",
],
fields: {
courseId: {type: CourseID},
academicYear: {type: uint},
academicQuarter: {type: Quarter},
courseName: {type: string},
description: {type: string},
instructorIds: {type: arrayOf(uint)},
// ...any other information related to a course.
},
});
itemType("Student", {
keyPath: [
"/student-:studentId",
],
fields: {
studentId: {type: uint},
// ...more fields such as phone number, emergency contact
},
});

Using the schema above, we can retrieve the Student Item with studentId of 1234 by getting /student-1234. We can also fetch information about a particular course in a particular quarter of a particular year using a complete keypath such as /course-MATH321/year-2023/quarter-1, which will fetch the Item for Math-321 in the Autumn quarter of 2023. But we can also fetch all occurrences of a given course across all years and quarters via a List operation with prefix /course-MATH321 or all offerings of a course in a given year via a List with prefix /course-MATH321/year-2023.

Let’s continue this example to answer the question below:

Why would I want more than one Key Path (an alias Key Path)?

Defining more than one Key Path for an Item is a way to make it possible to access those Items using different fields. In this way, it’s like an index in other databases, though unlike a traditional database, you can also access multiple items at once by Listing with a key path prefix.

StatelyDB guarantees that all Key Paths for a single Item are put, updated or deleted atomically with any change to the Item. Consider the relationship between a student and a class: a class may have many students and a student may be a member of many classes. Our application requires that we are able to answer the questions “Which classes is a student taking?” and “Which students are taking a given class?” Let’s build on the previous example and introduce a third Item type that will act as the glue between Students and Courses — the EnrolledStudent:

import { itemType, uint } from "@stately-cloud/schema";
import { CourseID, Quarter } from "./my-types.js";
itemType("EnrolledStudent", {
keyPath: [
"/course-:courseId/year-:year/quarter-:quarter/student-:studentId",
"/student-:studentId/year-:year/quarter-:quarter/course-:courseId",
],
fields: {
courseId: {type: CourseID},
year: {type: uint},
quarter: {type: Quarter},
studentId: {type: uint},
// information pertinent to the student's enrollment in the course,
// such as payment status, attendance, exam scores, etc.
},
});

Putting an EnrolledStudent Item will result in two records. One under the Student’s [Group] and one under the Course’s [Group]. This allows us to answer the questions above:

  • “Which classes is a student taking?” - A List with prefix /student-123 will give us all the courses a student has ever participated in.
  • “Which students are taking a given class?” - A List with prefix /course-PHYS341/year-2019 will return all the instances PHYS341 was offered in 2019 as well as all students who enrolled in those courses.

Beyond those questions, we can exploit the structure of these key paths to answer more specific questions. Listing with a prefix of /student-123/year-2019/quarter-3 will return only the courses student 123 participated in the spring quarter of 2019.

Again, you can think of these as a sort of index:

  • /course-:courseId/year-:year/quarter-:quarter/student-:studentId is similar to index EnrolledStudent on (courseId, year, quarter, studentId)
  • /student-:studentId/year-:year/quarter-:quarter/course-:courseId is similar to `index EnrolledStudent on (studentId, year, quarter, courseId)

The wild part is that all Items share the same Key Path space. Since we also have key paths of /course-:courseId/year-:year/quarter-:quarter (for Course) and /student-:studentId (for Student), a single List operation can pick up Students, EnrolledStudents, and Courses all at once.

In certain scenarios, it is advantageous to reference key IDs within nested Object Type fields. This is particularly useful when Object Type fields contain uniquely identifying data that can serve as part of the Key Path. Stately supports this using dot notation: /itemType-:objectField.nestedField.

Consider the following example, where a ContactInfo Object Type is shared between a BuyerAccount and SellerAccount Item Type. By referencing ContactInfo fields in the Key Path, it can be ensured that no two buyers or sellers can create an account with a previously used phone number or email:

import { itemType, objectType, string, uint } from "@stately-cloud/schema";
const ContactInfo = objectType("ContactInfo", {
fields: {
firstName: { type: string },
lastName: { type: string },
email: { type: string },
phoneNumber: { type: string },
}
});
itemType("BuyerAccount", {
keyPath: [
"/buyerAccount-:buyerId",
"/email-:contactInfo.email",
"/phone-:contactInfo.phoneNumber"
],
fields: {
buyerId: { type: uint },
contactInfo: { type: ContactInfo },
// other buyer-specific info such as payment info, etc.
},
});
itemType("SellerAccount", {
keyPath: [
"/sellerAccount-:sellerId",
"/email-:contactInfo.email",
"/phone-:contactInfo.phoneNumber"
],
fields: {
sellerId: { type: uint },
contactInfo: { type: ContactInfo },
// other seller-specific info such as receiver bank account, etc.
},
});

In this example, both the BuyerAccount and SellerAccount Item Types have key paths that reference the email and phoneNumber fields within the ContactInfo Object Type. This ensures that contact information is unique across each Item Type, providing, consistent, efficient and reusable way to access and manage data.

Being able to fetch multiple items using List is the power of an Item type with more than one key path, though it is worth answering a few related questions:

If an Item has more than one Key Path, what happens when I attempt to delete an Item by an ‘alias’ (eg: instead of the primary Key Path)?

Since an alias is a way to address an Item, if you attempt to delete any Item by an alias it will delete the Item. StatelyDB maintains consistency of an Item across all of its Key Paths. In our example above a delete issued to /student-123/year-2019/quarter-3/course-MATH321 will also delete /course-MATH321/year-2019/quarter-3/student-123 and vice versa.

What happens when a field used to calculate an ‘alias’ changes during an Item update?

To better frame this question, let’s continue to examine the example above and imagine there was a typo in the year when writing a EnrolledStudent Item. We meant to use the year 2023 but had accidentally typed 223. Thus we one Item with two Key Paths in our database:

  1. /student-123/year-223/quarter-3/course-MATH321
  2. /course-MATH321/year-223/quarter-3/student-123

…but want the Key Paths:

  1. /student-123/year-2023/quarter-3/course-MATH321
  2. /course-MATH321/year-2023/quarter-3/student-123

How can we correct this problem? If we attempt to write a new EnrolledStudent with an updated year, the primary Key Path of this Item is different than the original Item’s Key Path and thus the write will create a new item. The way to correct the problem is to write a new Item and then delete the incorrect Item.

Let’s modify our schema to make this more interesting by adding a new /classof-:graduatingYear/student-:studentId alias to the Student Item type:

itemType("Student", {
keyPath: [
"/student-:studentId",
"/classof-:graduatingYear/student-:studentId",
],
fields: {
studentId: {type: uint},
graduatingYear: {type: uint}
// ... more fields
},
});

Now we have the Student Item stored at the Key Paths:

  1. /student-123
  2. /classof-223/student-123

…but we want the Item stored at the Key Paths:

  1. /student-123
  2. /classof-2023/student-123

How can we correct the problem? Easy! Correct the graduatingYear field in the offending Student Item and write it back to the store. Since graduatingYear is not the unique component of the primary Key Path, StatelyDB understands student 123 should only have one /classof-*/student-123 alias, and will atomically “move” the previous Item /classof-223/student-123 to /classof-2023/student-123.

Is metadata the same between aliased items?

Timestamp metadata is guaranteed to be consistent across all aliases of an Item. However, version metadata may differ depending on the originating Key Path as version is a property of the Group an Item is in.

You can set options on a key path. For example, you can set syncable to opt out of syncability for a key path (or an entire item type or schema).

itemType("Student", {
keyPath: [
{ path: "/student-:studentId", syncable: false },
"/classof-:graduatingYear/student-:studentId",
],
...
});
Key Path TemplateValid?
/course-:courseId/year-:academicYear/quarter-:academicQuarter✅ Yes!
/classof-:graduatingYear/student-:studentId✅ Yes!
/student-:studentId✅ Yes!
/courses/course-:courseId/syllabus✅ Yes!
/courses🚫 No - this doesn’t have a full first segment (group key)
/courses/course-:courseId🚫 No - this also doesn’t have a full first segment (group key)
/course-:courseId/years/year-:academicYear🚫 No - the segment in the middle (years) doesn’t have an ID
/course-:courseId/lecture-notes-:id🚫 No - the namespace lecture-notes has a hyphen in it. Only letters and underscores are allowed.
/student-studentId🚫 No - the id studentId is missing a colon so it isn’t a valid field reference.