Help us grow — star us on GitHubGitHub stars
LinkedRecords

Query Patterns

Advanced querying with $hasDataType, $latest, and $not

Overview

LinkedRecords provides powerful query capabilities using special predicates and operators. This guide covers advanced querying techniques for filtering and finding records.

Basic Query Structure

Queries are expressed as objects where each key defines a named result set:

const result = await lr.Record.findAll({
  tasks: [
    ['$it', 'isA', 'Task'],
  ],
  projects: [
    ['$it', 'isA', 'Project'],
  ],
});
 
// result.tasks is an array of Task records
// result.projects is an array of Project records

Query Predicates

$hasDataType

Filter by record type (KeyValueAttribute, LongTextAttribute, or BlobAttribute):

const { documents } = await lr.Record.findAll({
  documents: [
    ['$it', '$hasDataType', 'KeyValueAttribute'],
    ['$it', 'isA', 'Document'],
  ],
});
 
const { notes } = await lr.Record.findAll({
  notes: [
    ['$it', '$hasDataType', 'LongTextAttribute'],
    ['$it', 'belongsTo', projectId],
  ],
});
 
const { files } = await lr.Record.findAll({
  files: [
    ['$it', '$hasDataType', 'BlobAttribute'],
    ['$it', 'isA', 'Attachment'],
  ],
});

$latest(predicate)

Get the most recent value for a predicate. This is essential for state tracking:

const { activeTasks } = await lr.Record.findAll({
  activeTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', activeStateId],
  ],
});

$latest() is used when you track state changes over time by creating new facts rather than deleting old ones. It returns records where the most recent fact with that predicate matches the specified value.

$not(value)

Negation operator - find records where a predicate does NOT have a specific value:

const { incompleteTasks } = await lr.Record.findAll({
  incompleteTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', `$not(${completedStateId})`],
  ],
});

Combining $latest and $not

The most common pattern is combining these for filtering by state:

const { activeItems } = await lr.Record.findAll({
  activeItems: [
    ['$it', 'isA', 'TodoList'],
    ['$it', '$latest(stateIs)', `$not(${archivedStateId})`],
  ],
});

Query by Record ID

Query specific records by their ID:

// Single record
const { doc } = await lr.Record.findAll({
  doc: documentId,  // Just pass the ID as a string
});
 
// Multiple specific records
const { doc1, doc2 } = await lr.Record.findAll({
  doc1: 'kv-abc123',
  doc2: 'kv-def456',
});

Compound Queries

Combine multiple criteria to narrow results:

const { results } = await lr.Record.findAll({
  results: [
    ['$it', '$hasDataType', 'KeyValueAttribute'],
    ['$it', 'isA', 'TodoList'],
    ['$it', '$isMemberOf', collectionId],
    ['$it', '$latest(stateIs)', `$not(${archivedStateId})`],
    [orgId, '$isAccountableFor', '$it'],
  ],
});

This query finds:

  • KeyValue records
  • Classified as TodoList
  • That are members of a specific collection
  • Not archived (based on latest state)
  • Where a specific organization is accountable

Relationship Queries

Find by Membership

const { listItems } = await lr.Record.findAll({
  listItems: [
    ['$it', '$isMemberOf', listId],
  ],
});

Find by Custom Relationships

const { authoredDocuments } = await lr.Record.findAll({
  authoredDocuments: [
    ['$it', 'isA', 'Document'],
    ['$it', 'authoredBy', authorId],
  ],
});
 
const { projectTasks } = await lr.Record.findAll({
  projectTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', 'belongsTo', projectId],
  ],
});

Find by Accountability

const { orgResources } = await lr.Record.findAll({
  orgResources: [
    [organizationId, '$isAccountableFor', '$it'],
  ],
});

State Management Pattern

A common pattern is using separate state records to track item status.

Why Records Instead of Terms?

You might wonder: why create records for states instead of using terms directly (e.g., [taskId, 'stateIs', 'Completed'])? The answer is permission control.

When states are records, you can use $canReferTo to control which users or teams can set which states:

// Only managers can archive items
await lr.Fact.createAll([
  [managersTeamId, '$canReferTo', archivedState.id],
]);
 
// All team members can mark items as completed
await lr.Fact.createAll([
  [allMembersTeamId, '$canReferTo', completedState.id],
]);

This enables granular workflows where different roles have different state transition permissions.

Implementation

// 1. Define state terms
await lr.Fact.createAll([
  ['ActiveState', '$isATermFor', 'Indicates item is active'],
  ['ArchivedState', '$isATermFor', 'Indicates item is archived'],
  ['CompletedState', '$isATermFor', 'Indicates item is completed'],
]);
 
// 2. Create state records
const { activeState, archivedState, completedState } = await lr.Record.createAll({
  activeState: {
    type: 'KeyValueAttribute',
    value: {},
    facts: [['$it', 'isA', 'ActiveState']],
  },
  archivedState: {
    type: 'KeyValueAttribute',
    value: {},
    facts: [['$it', 'isA', 'ArchivedState']],
  },
  completedState: {
    type: 'KeyValueAttribute',
    value: {},
    facts: [['$it', 'isA', 'CompletedState']],
  },
});
 
// 3. Set state on an item
await lr.Fact.createAll([
  [taskId, 'stateIs', activeState.id],
]);
 
// 4. Insert a newer state fact
await lr.Fact.createAll([
  [taskId, 'stateIs', completedState.id],
]);
 
// 5. Query by state
const { completedTasks } = await lr.Record.findAll({
  completedTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', completedState.id],
  ],
});
 
const { pendingTasks } = await lr.Record.findAll({
  pendingTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', `$not(${completedState.id})`],
  ],
});

Alternative: String-Based States

For simpler state tracking, you can use term values directly:

// Set state with string
await lr.Fact.createAll([
  [taskId, 'stateIs', 'Completed'],
]);
 
// Query (note: $latest still works with term values)
const { active } = await lr.Record.findAll({
  active: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', '$not(Completed)'],
  ],
});

Note: "Completed" must be declared as a term

Multiple Result Sets

Fetch different categories in a single query:

const {
  activeTasks,
  completedTasks,
  archivedTasks,
  projects,
} = await lr.Record.findAll({
  activeTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', `$not(${completedId})`],
    ['$it', '$latest(stateIs)', `$not(${archivedId})`],
  ],
  completedTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', completedId],
  ],
  archivedTasks: [
    ['$it', 'isA', 'Task'],
    ['$it', '$latest(stateIs)', archivedId],
  ],
  projects: [
    ['$it', 'isA', 'Project'],
  ],
});

Access Control in Queries

Queries automatically respect authorization - you only see records you have access to:

// User A creates a private document
await userA.Record.createKeyValue(
  { title: 'Secret' },
  [['$it', 'isA', 'Document']]
);
 
// User B queries for documents
const { docs } = await userB.Record.findAll({
  docs: [['$it', 'isA', 'Document']],
});
// docs is empty - User B can't see User A's private document

Loading Record Values

After querying, you can get record values:

const { tasks } = await lr.Record.findAll({
  tasks: [['$it', 'isA', 'Task']],
});
 
for (const task of tasks) {
  const value = await task.getValue();
  console.log(value.title);
}

Or use findAndLoadAll() for batch loading records:

const { tasks } = await lr.Record.findAndLoadAll({
  tasks: [['$it', 'isA', 'Task']],
});
 
// Values are pre-loaded
for (const task of tasks) {
  const value = await task.getValue(); // No additional network request
  console.log(value.title);
}

Query Performance Tips

  1. Be specific - Include type filters (isA) to narrow results quickly

  2. Use accountability filters - Filter by $isAccountableFor to scope to specific organizations

  3. Combine criteria - Multiple criteria help the query engine optimize

  4. Avoid fetching everything - Query for what you need rather than loading all records and filtering client-side

Best Practices

  1. Use $latest for state tracking instead of deleting old state facts

  2. Create dedicated state records for reusable state markers

  3. Combine $latest and $not for filtering out unwanted states

  4. Use meaningful predicate names that describe the relationship

  5. Structure queries logically with the most selective criteria first