Skip to content

Temporal

TypeGraph tracks temporal validity for all nodes and edges. Use temporal queries to view the graph at a point in time, audit changes, or access historical data.

The temporal() method controls which versions of data are returned:

ModeDescription
"current"Only currently valid data (default behavior)
"asOf"Data as it existed at a specific timestamp
"includeEnded"All versions, including historical
"includeTombstones"All versions, including soft-deleted

By default, queries return only currently valid, non-deleted data:

// Returns only current, non-deleted nodes
const currentPeople = await store
.query()
.from("Person", "p")
.select((ctx) => ctx.p)
.execute();

This is equivalent to:

.temporal("current")

Query the graph as it existed at a specific moment:

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const pastState = await store
.query()
.from("Article", "a")
.temporal("asOf", yesterday)
.whereNode("a", (a) => a.id.eq(articleId))
.select((ctx) => ctx.a)
.execute();

This returns nodes and edges that were valid at the specified timestamp, even if they’ve since been updated or deleted.

  • Auditing: See what data looked like at a specific time
  • Debugging: Reproduce issues by querying historical state
  • Compliance: Generate point-in-time reports
  • Recovery: Find old values before an erroneous update
// What did the user's profile look like last week?
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const historicalProfile = await store
.query()
.from("User", "u")
.temporal("asOf", lastWeek)
.whereNode("u", (u) => u.id.eq(userId))
.select((ctx) => ctx.u)
.first();

View all versions, including superseded records:

const history = await store
.query()
.from("Article", "a")
.temporal("includeEnded")
.whereNode("a", (a) => a.id.eq(articleId))
.orderBy((ctx) => ctx.a.validFrom, "desc")
.select((ctx) => ({
title: ctx.a.title,
validFrom: ctx.a.validFrom,
validTo: ctx.a.validTo,
version: ctx.a.version,
}))
.execute();
// Result shows all versions:
// [
// { title: "Final Title", validFrom: "2024-03-01", validTo: undefined, version: 3 },
// { title: "Draft v2", validFrom: "2024-02-15", validTo: "2024-03-01", version: 2 },
// { title: "Initial Draft", validFrom: "2024-02-01", validTo: "2024-02-15", version: 1 },
// ]

Build a complete change history:

async function getAuditTrail(nodeId: string) {
return store
.query()
.from("Document", "d")
.temporal("includeEnded")
.whereNode("d", (d) => d.id.eq(nodeId))
.select((ctx) => ({
version: ctx.d.version,
title: ctx.d.title,
status: ctx.d.status,
validFrom: ctx.d.validFrom,
validTo: ctx.d.validTo,
updatedAt: ctx.d.updatedAt,
}))
.orderBy("d", "version", "asc")
.execute();
}

Including Soft-Deleted Data (includeTombstones)

Section titled “Including Soft-Deleted Data (includeTombstones)”

Include records that have been soft-deleted:

const allIncludingDeleted = await store
.query()
.from("User", "u")
.temporal("includeTombstones")
.select((ctx) => ({
id: ctx.u.id,
name: ctx.u.name,
deletedAt: ctx.u.deletedAt, // Will have a value for deleted records
}))
.execute();
// Find only deleted records
const deletedUsers = await store
.query()
.from("User", "u")
.temporal("includeTombstones")
.whereNode("u", (u) => u.deletedAt.isNotNull())
.select((ctx) => ({
id: ctx.u.id,
name: ctx.u.name,
deletedAt: ctx.u.deletedAt,
}))
.execute();

When querying with temporal context, these fields are available:

FieldTypeDescription
validFromstring | undefinedWhen this version became valid
validTostring | undefinedWhen this version was superseded (undefined if current)
createdAtstringWhen the node was first created
updatedAtstringWhen this version was written
deletedAtstring | undefinedSoft-delete timestamp (undefined if not deleted)
versionnumberOptimistic concurrency version number
.select((ctx) => ({
...ctx.a, // All node properties
validFrom: ctx.a.validFrom,
validTo: ctx.a.validTo,
createdAt: ctx.a.createdAt,
updatedAt: ctx.a.updatedAt,
deletedAt: ctx.a.deletedAt,
version: ctx.a.version,
}))

Temporal modes apply to traversals as well:

// See who worked at a company last year
const lastYear = new Date("2023-01-01").toISOString();
const pastEmployees = await store
.query()
.from("Company", "c")
.temporal("asOf", lastYear)
.whereNode("c", (c) => c.name.eq("Acme Corp"))
.traverse("worksAt", "e", { direction: "in" })
.to("Person", "p")
.select((ctx) => ({
name: ctx.p.name,
role: ctx.e.role,
}))
.execute();

Compare two versions of a document:

async function compareVersions(docId: string, v1: number, v2: number) {
const versions = await store
.query()
.from("Document", "d")
.temporal("includeEnded")
.whereNode("d", (d) => d.id.eq(docId))
.select((ctx) => ctx.d)
.execute();
const version1 = versions.find((v) => v.version === v1);
const version2 = versions.find((v) => v.version === v2);
return { version1, version2 };
}

Generate a report as of a specific date:

async function generateQuarterlyReport(quarterEnd: string) {
const activeContracts = await store
.query()
.from("Contract", "c")
.temporal("asOf", quarterEnd)
.whereNode("c", (c) => c.status.eq("active"))
.traverse("belongsTo", "e")
.to("Customer", "cust")
.select((ctx) => ({
contractId: ctx.c.id,
value: ctx.c.value,
customer: ctx.cust.name,
}))
.execute();
return {
asOf: quarterEnd,
totalContracts: activeContracts.length,
totalValue: activeContracts.reduce((sum, c) => sum + c.value, 0),
contracts: activeContracts,
};
}

Find the previous value before an update:

async function getPreviousVersion(nodeId: string) {
const versions = await store
.query()
.from("Document", "d")
.temporal("includeEnded")
.whereNode("d", (d) => d.id.eq(nodeId))
.select((ctx) => ctx.d)
.orderBy("d", "version", "desc")
.limit(2)
.execute();
return {
current: versions[0],
previous: versions[1],
};
}