Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fajarnugraha37/drizzle-castor/llms.txt

Use this file to discover all available pages before exploring further.

Once you have a CastorInstance from .build(), you call repoFactory(tableName) to obtain a repository. The repository is a collection of strongly typed async methods that covers every database operation Castor supports — from single-record inserts to paginated reads with nested relations. TypeScript binds the repository to the named table at call-site, so filter paths, projection arrays, and insert shapes are all validated against your actual Drizzle column definitions.

Obtaining a repository

import { schemaMetadata } from "./castor";

const userRepo = schemaMetadata.repoFactory("users");
repoFactory is generic over TName, which must be a key of the metadata map you built via .table() calls. The returned Repository<TSchema, TName> exposes two categories of members: factory helpers for constructing typed query fragments, and core methods for executing operations.

Factory helpers

Factory helpers are no-op identity functions whose only job is to capture TypeScript generics and give you autocomplete when building reusable query fragments. They do not touch the database.
Returns a typed FilterQuery<TEntity> value. Use this to define a reusable filter outside the call-site.
const activeUsers = userRepo.defineFilter({
  deletedFlag: { $eq: 0 },
  age: { $gte: 18 },
});

const users = await userRepo.searchMany({ filter: activeUsers }, "admin");
Returns a typed array of dot-notation paths that can be passed as projection. TypeScript infers the element type from the declared paths.
const publicFields = userRepo.defineProjection([
  "name",
  "email",
  "profile.bio",
  "settings.theme",
]);

const user = await userRepo.searchOne({ projection: publicFields }, "public");
Returns a typed OrderQuery<TEntity> value for reusable order configurations.
const latestFirst = userRepo.defineQuery({
  createdAt: "desc",
  "posts.comments.createdAt": { direction: "desc", nulls: "last" },
});
Returns a typed UpdateSet<TInsert> value. Useful for shared update payloads.
const markActive = userRepo.defineUpdateSet({
  "settings.notifications": true,
});
Returns a typed insert payload. Helpful when constructing insert data outside the call-site with full type checking.
const newUser = userRepo.defineInsertValue({
  name: "Jane Doe",
  email: "jane@example.com",
});

Passing profiles

Every core method accepts an optional profile parameter as its last argument. This value is matched against the policies registered on the builder via .policies(). Pass a single string or an array of strings; if you pass an array, the RBAC engine unions the capabilities of every matched profile.
// Single profile
await userRepo.searchMany({}, "admin");

// Multiple profiles — capabilities are unioned
await userRepo.searchMany({}, ["public", "editor"]);

// No profile — falls back to "default"
await userRepo.searchMany({});
In strict mode, calling without a profile or with a profile that has no registered policy throws an AccessDeniedError before the query runs.

Create methods

1

createOne

Inserts a single record and returns the full hydrated entity.
const newUser = await userRepo.createOne(
  {
    name: "Jane Doe",
    email: "jane@example.com",
  },
  "admin",
);
// Returns: TEntity (full row)
2

createMany

Inserts multiple records in one operation and returns an array of the created entities.
const newUsers = await userRepo.createMany(
  [
    { name: "Alice", email: "alice@example.com" },
    { name: "Bob",   email: "bob@example.com"   },
  ],
  "admin",
);
// Returns: TEntity[]

Read methods

All read methods accept a query object whose shape is validated against the entity type. The projection array shapes the TypeScript return type at compile time via DeepPick.

searchOne

Returns a single record matching the filter, or null if none is found.
const user = await userRepo.searchOne(
  {
    projection: ["name", "email", "profile.bio"],
    filter: { email: { $eq: "jane@example.com" } },
    order:  { createdAt: "desc" },
  },
  "admin",
);
// Returns: { name: string; email: string; profile: { bio: string | null } } | null

searchMany

Returns all records matching the filter as an array. Does not support page or pageSize.
const users = await userRepo.searchMany(
  {
    filter: { age: { $gte: 18 } },
    order:  { createdAt: "desc" },
  },
  "admin",
);
// Returns: TEntity[]

searchPage

Returns a paginated result set with metadata. Use this for list views that need total counts.
const page = await userRepo.searchPage(
  {
    page:     1,
    pageSize: 10,
    filter:   { "posts.comments.content": { $notIsNull: true } },
    order:    { createdAt: "desc" },
  },
  "public",
);

// Returns:
// {
//   data: TEntity[];
//   meta: {
//     currentPage: number;
//     pageSize:    number;
//     totalPages:  number;
//     totalItems:  number;
//   };
// }
searchPage uses a CTE split-query strategy internally to avoid Cartesian fan-out when one-to-many joins are involved, keeping pagination counts accurate.

Projection and return type inference

When you pass a projection array, TypeScript picks only the requested paths from the entity type:
const result = await userRepo.searchOne(
  { projection: ["name", "settings.theme"] },
  "admin",
);

// result is typed as:
// { name: string; settings: { theme: string } } | null
// — not the full TEntity
This inference is powered by the DeepPick<TEntity, Q["projection"][number]> utility type defined in src/types/repository.ts.

Soft-delete read variants

When a table has softDelete configured, its active records exclude soft-deleted rows automatically. To query the deleted records explicitly, use the searchDeleted* variants:
const deleted = await userRepo.searchDeletedOne(
  { filter: { id: { $eq: 1 } } },
  "admin",
);
// Returns the soft-deleted record or null

Update methods

updateOne

Updates a record by its primary key. Returns the updated entity or null if no record was found.
const updated = await userRepo.updateOne(
  1,                          // primary key
  {
    name: "John Updated",
    "settings.theme": "light", // partial JSON column update
  },
  "admin",
);
// Returns: TEntity | null

updateMany

Updates all records matching a filter. Returns the array of updated entities.
const updated = await userRepo.updateMany(
  { age: { $lt: 18 } },              // filter
  { "settings.notifications": false }, // set
  "admin",
);
// Returns: TEntity[]
On PostgreSQL and SQLite, updateMany uses a RETURNING clause for atomic batch updates. On MySQL, Castor falls back to a temporary-table strategy to avoid race conditions. See Multi-dialect support for details.

Soft-delete and restore methods

Soft-delete methods write the deleteValue configured on the table (e.g., { deletedFlag: 1 }) rather than removing the row. Subsequent searchOne, searchMany, and searchPage calls automatically exclude soft-deleted rows.
// Delete a single record by primary key
const success = await userRepo.softDeleteOne(1, "admin");
// Returns: boolean

// Delete all records matching a filter
const count = await userRepo.softDeleteMany(
  { "settings.theme": { $eq: "light" } },
  "admin",
);
// Returns: number (rows affected)

Hard-delete methods

Hard delete permanently removes rows. There is no recovery path.
// Delete a single record permanently by primary key
const removed = await userRepo.hardDeleteOne(1, "admin");
// Returns: boolean

// Delete all matching records permanently
const count = await userRepo.hardDeleteMany(
  { age: { $lt: 13 } },
  "admin",
);
// Returns: number (rows affected)
hardDeleteOne and hardDeleteMany bypass any soft-delete configuration and issue a physical DELETE statement. Rows are unrecoverable after this call.

Full method reference

JSON-based querying

Learn the full filter operator vocabulary and how to write complex nested queries.

Soft deletes

Configure and customise soft-delete behaviour per table.

Pagination

Understand the CTE split-query strategy and how to work with searchPage.

Type system

Explore how DeepPick and FlattenPaths provide end-to-end type safety.

Build docs developers (and LLMs) love