Skip to content

Cross-Entity and Transactions

Most real applications do more than basic CRUD. They need:

  • cross-entity lookups
  • aggregates and reporting
  • multi-step flows
  • transaction-style writes

Slingshot supports those cases, but not all of them belong in the same layer.

For read-heavy projections and reporting, use:

  • operation configs like lookup, aggregate, and computed variants
  • package domain routes when the response spans multiple entities
  • capabilities when one package depends on another package’s service contract

Inside package domain routes, the canonical entity access pattern is typed entity modules:

const resumes = ctx.entities.get(ResumeEntity);
const workExperiences = ctx.entities.get(WorkExperienceEntity);

If the entity lives in another package, make that explicit:

const notes = ctx.entities.get(entityRef(NoteEntity, { plugin: 'notes' }));

Avoid this as the default authoring style:

  • ctx.entities.get({ entity: 'Resume' })
  • ctx.entities.get<BareEntityAdapter>(...)
  • as any

If the endpoint is “analytics across notes and comments”, that is usually a package domain route, not an entity-local CRUD handler.

For multi-step entity behavior, use the higher-order operation DSL first:

  • transaction
  • pipe
  • derive
  • batch
  • upsert

Those operation kinds keep the workflow declarative and compatible with generated adapters and routes.

See Operations Reference for the exact shape of each operation.

Move to a package domain route or raw plugin when:

  • the workflow spans multiple entities with custom branching
  • you need non-standard external I/O in the middle of the flow
  • the response shape is custom and not a natural entity result
  • the operation is better described as package orchestration than entity behavior

Use capabilities for cross-package contracts:

const commentsApi = defineCapability<{
countForNote(noteId: string): Promise<number>;
}>('comments.api');

Then consume them from package domain routes or package-local services instead of reaching into random plugin state.

Yes, but not inside the entity DSL itself.

The entity DSL is for declarative persistence and generated routing. If you need raw SQL or database-specific queries:

  • use factories wiring to plug in a custom repo-backed adapter
  • use manual wiring to build the adapter yourself
  • use a package domain route or raw plugin when the flow is not an entity adapter concern

That keeps the public authoring model clear:

  • entity DSL for declarative domain behavior
  • custom adapter/repo code for storage-specific logic
  • package domain routes for orchestration and custom endpoints

Use this decision rule:

  • one entity, standard behavior: entity + operations
  • one entity, custom route semantics that still belong to the entity shell: entity + extraRoutes or overrides
  • multiple entities or reporting: package domain(...)
  • storage-specific query logic: custom factories/manual adapter wiring
  • framework-level escape hatch: raw plugin