mongokit

@classytic/mongokit — Production-grade MongoDB repository pattern for Node.js/TypeScript. Use when building MongoDB CRUD, REST APIs with Mongoose 9, repository pattern, pagination, caching, soft delete, audit trail, multi-tenant, custom ID generation, or query parsing. Triggers: mongoose model, repository pattern, mongokit, mongo crud, pagination, soft delete, audit trail, multi-tenant, custom id, query parser, cache plugin, BaseController.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "mongokit" with this command: npx skills add classytic/mongokit/classytic-mongokit-mongokit

@classytic/mongokit

Production-grade MongoDB repository pattern with zero external dependencies. 17 built-in plugins, smart pagination, event-driven hooks, and full TypeScript support. 687 tests.

Requires: Mongoose ^9.0.0 | Node.js >=18

Installation

npm install @classytic/mongokit mongoose

Core Pattern

Every interaction starts with a Repository wrapping a Mongoose model:

import { Repository } from "@classytic/mongokit";

const repo = new Repository(UserModel);

const user = await repo.create({ name: "John", email: "john@example.com" });
const users = await repo.getAll({ page: 1, limit: 20 });
const found = await repo.getById(id);
const updated = await repo.update(id, { name: "Jane" });
await repo.delete(id);
const exists = await repo.exists({ email: "john@example.com" });
const count = await repo.count({ status: "active" });
const userOrNew = await repo.getOrCreate({ email: "x@y.com" }, { name: "X" });

Full API

MethodDescription
create(data, opts)Create single document
createMany(data[], opts)Create multiple documents
getById(id, opts)Find by ID
getByQuery(query, opts)Find one by query
getAll(params, opts)Paginated list (auto-detects offset vs keyset)
getOrCreate(query, data, opts)Find or create
update(id, data, opts)Update document
delete(id, opts)Delete document
count(query, opts)Count documents
exists(query, opts)Check existence
aggregate(pipeline, opts)Run aggregation
aggregatePaginate(opts)Paginated aggregation
distinct(field, query)Distinct values
withTransaction(fn)Atomic transaction

Pagination (Auto-Detected)

// Offset (dashboards) — pass `page`
const result = await repo.getAll({
  page: 1,
  limit: 20,
  filters: { status: "active" },
  sort: { createdAt: -1 },
});
// → { method: 'offset', docs, total, pages, hasNext, hasPrev }

// Keyset (infinite scroll) — pass `sort` without `page`, or `after`
const stream = await repo.getAll({ sort: { createdAt: -1 }, limit: 20 });
// → { method: 'keyset', docs, hasMore, next: 'eyJ2IjoxLC...' }
const next = await repo.getAll({
  after: stream.next,
  sort: { createdAt: -1 },
  limit: 20,
});

Detection: page → offset | after/cursor → keyset | sort only → keyset | default → offset

Required indexes for keyset:

Schema.index({ createdAt: -1, _id: -1 });
Schema.index({ organizationId: 1, createdAt: -1, _id: -1 }); // multi-tenant

Plugin System

Plugins compose via array — order matters:

import {
  Repository,
  timestampPlugin,
  softDeletePlugin,
  cachePlugin,
  createMemoryCache,
  customIdPlugin,
  sequentialId,
} from "@classytic/mongokit";

const repo = new Repository(UserModel, [
  timestampPlugin(),
  softDeletePlugin(),
  cachePlugin({ adapter: createMemoryCache(), ttl: 60 }),
]);

All 17 Plugins

PluginDescriptionNeeds methodRegistry?
timestampPlugin()Auto createdAt/updatedAtNo
softDeletePlugin(opts)Mark deleted instead of removingNo
auditLogPlugin(logger)Log all CUD operations (external logger)No
auditTrailPlugin(opts)DB-persisted audit trail + change diffsNo (Yes for queries)
cachePlugin(opts)Redis/memory caching + auto-invalidationNo
validationChainPlugin(validators)Custom validation rulesNo
fieldFilterPlugin(preset)Role-based field visibility (RBAC)No
cascadePlugin(opts)Auto-delete related documentsNo
multiTenantPlugin(opts)Auto-inject tenant isolationNo
customIdPlugin(opts)Sequential/random ID generationNo
observabilityPlugin(opts)Timing, metrics, slow queriesNo
methodRegistryPlugin()Dynamic method registrationNo (base for below)
mongoOperationsPlugin()increment, pushToArray, upsertYes
batchOperationsPlugin()updateMany, deleteManyYes
aggregateHelpersPlugin()groupBy, sum, averageYes
subdocumentPlugin()Manage subdocument arraysYes
elasticSearchPlugin(opts)Delegate search to ES/OpenSearchYes

Soft Delete

const repo = new Repository(UserModel, [
  softDeletePlugin({ deletedField: "deletedAt" }),
]);
await repo.delete(id); // Sets deletedAt
await repo.getAll(); // Auto-excludes deleted
await repo.getAll({ includeDeleted: true }); // Include deleted

Unique index gotcha: Use partial filter expressions:

Schema.index(
  { email: 1 },
  { unique: true, partialFilterExpression: { deletedAt: null } },
);

Caching

const repo = new Repository(UserModel, [
  cachePlugin({
    adapter: createMemoryCache(),
    ttl: 60,
    byIdTtl: 300,
    queryTtl: 30,
  }),
]);
const user = await repo.getById(id); // Cached
const fresh = await repo.getById(id, { skipCache: true }); // Skip cache
await repo.update(id, { name: "New" }); // Auto-invalidates

Redis adapter:

const redisAdapter = {
  async get(key) {
    return JSON.parse((await redis.get(key)) || "null");
  },
  async set(key, value, ttl) {
    await redis.setex(key, ttl, JSON.stringify(value));
  },
  async del(key) {
    await redis.del(key);
  },
  async clear(pattern) {
    /* bulk invalidation */
  },
};

Multi-Tenant

const repo = new Repository(UserModel, [
  multiTenantPlugin({
    tenantField: "organizationId",
    contextKey: "organizationId",
    required: true,
  }),
]);
// All ops auto-scoped: repo.getAll({ organizationId: 'org_123' })
// Cross-tenant → returns "not found"

Custom ID Generation

Atomic MongoDB counters — concurrency-safe, zero duplicates:

import {
  customIdPlugin,
  sequentialId,
  dateSequentialId,
  prefixedId,
  getNextSequence,
} from "@classytic/mongokit";

// Sequential: INV-0001, INV-0002, ...
customIdPlugin({
  field: "invoiceNumber",
  generator: sequentialId({ prefix: "INV", model: InvoiceModel }),
});

// Date-partitioned: BILL-2026-02-0001 (resets monthly/yearly/daily)
customIdPlugin({
  field: "billNumber",
  generator: dateSequentialId({
    prefix: "BILL",
    model: BillModel,
    partition: "monthly",
  }),
});

// Random: ORD_a7b3xk9m2p (no DB round-trip)
customIdPlugin({
  field: "orderRef",
  generator: prefixedId({ prefix: "ORD", length: 10 }),
});

// Custom generator with getNextSequence()
customIdPlugin({
  field: "ref",
  generator: async (ctx) =>
    `ORD-${ctx.data?.region || "US"}-${String(await getNextSequence("orders")).padStart(4, "0")}`,
});

Options: sequentialId({ prefix, model, padding?: 4, separator?: '-', counterKey? }) Partitions: 'yearly' | 'monthly' | 'daily' Behavior: Counters never decrement on delete (standard for invoices/bills).

Validation Chain

import {
  validationChainPlugin,
  requireField,
  uniqueField,
  immutableField,
  blockIf,
  autoInject,
} from "@classytic/mongokit";
validationChainPlugin([
  requireField("email", ["create"]),
  uniqueField("email", "Email already exists"),
  immutableField("userId"),
  blockIf(
    "noAdminDelete",
    ["delete"],
    (ctx) => ctx.data?.role === "admin",
    "Cannot delete admins",
  ),
  autoInject("slug", (ctx) => slugify(ctx.data?.name), ["create"]),
]);

Cascade Delete

cascadePlugin({
  relations: [
    { model: "StockEntry", foreignKey: "product" },
    { model: "Review", foreignKey: "product", softDelete: false },
  ],
  parallel: true,
});

MongoDB Operations (Atomic)

import type { MongoOperationsMethods } from "@classytic/mongokit";
type Repo = Repository<IUser> & MongoOperationsMethods<IUser>;
const repo = new Repository(UserModel, [
  methodRegistryPlugin(),
  mongoOperationsPlugin(),
]) as Repo;

await repo.increment(id, "views", 1);
await repo.pushToArray(id, "tags", "featured");
await repo.upsert({ sku: "ABC" }, { name: "Product", price: 99 });
await repo.addToSet(id, "roles", "admin");

Audit Trail (DB-Persisted)

import { auditTrailPlugin, AuditTrailQuery } from "@classytic/mongokit";
import type { AuditTrailMethods } from "@classytic/mongokit";

// Per-repo: track operations with change diffs
const repo = new Repository(JobModel, [
  methodRegistryPlugin(),
  auditTrailPlugin({
    operations: ["create", "update", "delete"],
    trackChanges: true, // before/after diff
    ttlDays: 90, // auto-purge
    excludeFields: ["password"],
    metadata: (ctx) => ({ ip: ctx.req?.ip }),
  }),
]);
const trail = await repo.getAuditTrail(docId, { operation: "update" });

// Standalone: query across all models (admin dashboards)
const auditQuery = new AuditTrailQuery();
await auditQuery.getOrgTrail(orgId);
await auditQuery.getUserTrail(userId);
await auditQuery.getDocumentTrail("Job", jobId);
await auditQuery.query({ orgId, operation: "delete", from: startDate, to: endDate });

Options: operations, trackChanges (default: true), trackDocument (default: false), ttlDays, collectionName (default: 'audit_trails'), excludeFields, metadata

Observability

observabilityPlugin({
  onMetric: (m) => statsd.histogram(`mongokit.${m.operation}`, m.duration),
  slowThresholdMs: 200,
});

Event System

repo.on("before:create", async (ctx) => {
  ctx.data.processedAt = new Date();
});
repo.on("after:create", ({ context, result }) => {
  console.log("Created:", result._id);
});
repo.on("error:create", ({ context, error }) => {
  reportError(error);
});

Events: before:*, after:*, error:* for create, createMany, update, delete, getById, getByQuery, getAll, aggregatePaginate

QueryParser (HTTP to MongoDB)

import { QueryParser } from "@classytic/mongokit";
const parser = new QueryParser({
  maxLimit: 100,
  maxFilterDepth: 5,
  maxRegexLength: 100,
});
const { filters, limit, page, sort, search, populateOptions } = parser.parse(
  req.query,
);

URL patterns:

?status=active&role=admin             # exact match
?age[gte]=18&age[lte]=65             # range
?role[in]=admin,user                  # in-set
?name[regex]=^John                    # regex
?sort=-createdAt,name                 # multi-sort
?page=2&limit=50                      # offset pagination
?after=eyJfaWQiOi...&limit=20        # cursor pagination
?search=john                          # full-text
?populate[author][select]=name,email  # advanced populate

Security: Blocks $where/$function/$accumulator/$expr | ReDoS protection | $options restricted to [imsx] | Populate path sanitization

BaseController (Auto-CRUD)

The package includes a BaseController reference implementation (see examples/api/BaseController.ts) that provides instant auto-generated CRUD with security:

import { BaseController } from "./BaseController";

class UserController extends BaseController<IUser> {
  constructor(repository: Repository<IUser>) {
    super(repository, {
      fieldRules: {
        role: { systemManaged: true }, // Users cannot set role
        credits: { systemManaged: true }, // Users cannot set credits
      },
      query: {
        allowedLookups: ["departments", "teams"], // Only these collections can be joined
        allowedLookupFields: {
          departments: { localFields: ["deptId"], foreignFields: ["_id"] },
        },
      },
    });
  }

  // Override specific methods — the rest are auto-generated
  async create(ctx: IRequestContext) {
    await sendVerificationEmail(ctx.body.email);
    return super.create(ctx);
  }
}

Features:

  • Auto list/get/create/update/delete from IController interface
  • System-managed field sanitization (strips protected fields from user input)
  • 3-level lookup security: collection allowlist → per-collection field allowlist → pipeline/let blocking
  • Override any method, keep the rest auto-generated

JSON Schema Generation

import { buildCrudSchemasFromModel } from "@classytic/mongokit";
const { crudSchemas } = buildCrudSchemasFromModel(UserModel, {
  fieldRules: {
    organizationId: { immutable: true },
    role: { systemManaged: true },
  },
  strictAdditionalProperties: true,
});
// crudSchemas.createBody, updateBody, params, listQuery — use with Fastify schema validation or OpenAPI

Configuration

new Repository(UserModel, plugins, {
  defaultLimit: 20,
  maxLimit: 100,
  maxPage: 10000,
  deepPageThreshold: 100,
  useEstimatedCount: false,
  cursorVersion: 1,
});

Extending Repository

class UserRepository extends Repository<IUser> {
  constructor() {
    super(UserModel, [timestampPlugin(), softDeletePlugin()], {
      defaultLimit: 20,
    });
  }
  async findByEmail(email: string) {
    return this.getByQuery({ email });
  }
  async findActive() {
    return this.getAll({
      filters: { status: "active" },
      sort: { createdAt: -1 },
    });
  }
}

TypeScript Type Safety

import type { WithPlugins } from "@classytic/mongokit";
const repo = new Repository(UserModel, [
  methodRegistryPlugin(),
  mongoOperationsPlugin(),
  softDeletePlugin(),
  cachePlugin({ adapter: createMemoryCache() }),
]) as WithPlugins<IUser, Repository<IUser>>;
// Full autocomplete: repo.increment, repo.restore, repo.invalidateCache

Types: MongoOperationsMethods<T>, BatchOperationsMethods, AggregateHelpersMethods, SubdocumentMethods<T>, SoftDeleteMethods<T>, CacheMethods, AuditTrailMethods

Architecture Decisions

  • Zero external deps — only Mongoose as peer dep
  • Own event system — not Mongoose middleware, fully async with emitAsync
  • Own FilterQuery type — immune to Mongoose 9's rename to RootFilterQuery
  • Update pipelines gated — must pass { updatePipeline: true } explicitly
  • Atomic countersfindOneAndUpdate + $inc, not countDocuments (race-safe)
  • Cache versioningDate.now() timestamps, not incrementing integers (survives Redis eviction)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

CAN: Clock Address Naming

Agent & MCP integration. CAN stamps what flows through any pipe. Verify, name, log locally.

Registry SourceRecently Updated
01.4K
Profile unavailable
Security

FeedOracle Compliance Intelligence

MiCA compliance evidence and stablecoin risk scoring for regulated tokenized markets. 27 MCP tools with ES256K-signed responses. Use when the user explicitly...

Registry SourceRecently Updated
0120
Profile unavailable
Security

Best Practices

Apply modern web development best practices for security, compatibility, code quality, and coding standards. Covers KISS/DRY/YAGNI principles, TypeScript/Jav...

Registry SourceRecently Updated
0105
Profile unavailable