MongoDB
Experimental
The MongoDB adapter is experimental. APIs may change at any moment.
@atscript/mongo provides a MongoDB adapter for the Atscript DB abstraction layer. It translates annotation-driven CRUD operations into native MongoDB queries, with support for auto-increment fields, ObjectId handling, nested document storage, Atlas Search indexes, and raw aggregation pipelines.
Installation
pnpm add @atscript/mongo @atscript/utils-db @atscript/typescript mongodbnpm install @atscript/mongo @atscript/utils-db @atscript/typescript mongodbyarn add @atscript/mongo @atscript/utils-db @atscript/typescript mongodbmongodb is a peer dependency (^6.17.0).
Plugin Configuration
Register the MongoPlugin in your atscript.config.mts to enable MongoDB-specific annotations and primitives (mongo.objectId, mongo.vector):
import { defineConfig } from '@atscript/core'
import ts from '@atscript/typescript'
import { MongoPlugin } from '@atscript/mongo/plugin'
export default defineConfig({
rootDir: 'src',
plugins: [ts(), MongoPlugin()],
})This adds the @db.mongo.* annotation namespace and the mongo.objectId / mongo.vector primitive types.
Define Your Schema
Create a .as file with @db.table and @db.mongo.collection annotations:
// todo.as
@db.table 'todos'
@db.mongo.collection
export interface Todo {
@meta.id
@db.default.fn 'increment'
id: number
title: string
description?: string
@db.default.value 'false'
completed: boolean
@db.default.fn 'now'
createdAt?: number.timestamp.created
}The @db.mongo.collection annotation automatically injects a non-optional _id: mongo.objectId field if one is not explicitly defined. This means every document gets a MongoDB _id even when your logical primary key is a different field (like id above).
TIP
If you want to control the _id type, declare it explicitly. It must be string, number, or mongo.objectId:
@db.table 'users'
@db.mongo.collection
export interface User {
_id: mongo.objectId
email: string.email
}Connect and Create Tables
The AsMongo class wraps a MongoDB connection and provides table/adapter access. It accepts either a connection string or an existing MongoClient instance:
import { AsMongo } from '@atscript/mongo'
import { Todo } from './schema/todo.as'
const mongo = new AsMongo('mongodb://localhost:27017/myapp')
const todos = mongo.getTable(Todo)
await todos.ensureTable()
await todos.syncIndexes()ensureTable() creates the collection if it does not exist. syncIndexes() synchronizes indexes defined in annotations with the database -- it creates missing indexes and drops stale ones (those prefixed with atscript__ that no longer match the schema).
You can also access the underlying MongoAdapter directly:
const adapter = mongo.getAdapter(Todo)Or use an existing MongoClient:
import { MongoClient } from 'mongodb'
const client = new MongoClient('mongodb://localhost:27017/myapp')
const mongo = new AsMongo(client)CRUD Operations
The table API follows the same patterns as the generic DB abstraction layer. All examples below assume the todos table from the setup above.
Insert
// Single insert
const result = await todos.insertOne({
title: 'Write documentation',
completed: false,
})
// -> { insertedId: ObjectId('...') }
// id auto-incremented, createdAt defaults to Date.now()
// _id auto-generated as ObjectId
// Bulk insert
const bulk = await todos.insertMany([
{ title: 'First task' },
{ title: 'Second task' },
])
// -> { insertedCount: 2, insertedIds: [ObjectId('...'), ObjectId('...')] }
// id values auto-incremented in order (1, 2, ...)Fields with @db.default.value or @db.default.fn are automatically applied before insertion. Fields marked @db.ignore are stripped. ObjectId _id fields are optional on insert (auto-generated by MongoDB).
Find
// Find one
const todo = await todos.findOne({
filter: { completed: false },
controls: {},
})
// Find many with sorting and pagination
const page = await todos.findMany({
filter: { completed: false },
controls: { $sort: { createdAt: -1 }, $limit: 10, $skip: 0 },
})
// Find by ID (tries _id first, then falls back to @meta.id fields)
const byId = await todos.findById(1)
// Count
const total = await todos.count({
filter: { completed: true },
controls: {},
})See Queries & Filters for the full filter syntax.
Update
// Partial update by primary key
await todos.updateOne({ _id: someObjectId, completed: true })
// Update many by filter
await todos.updateMany(
{ completed: false },
{ completed: true },
)Delete
// Delete by primary key
await todos.deleteOne({ _id: someObjectId })
// Delete many by filter
await todos.deleteMany({ completed: true })_id and Primary Keys
MongoDB always uses _id as the document primary key. The adapter enforces this regardless of your schema:
@db.mongo.collectionauto-injects_id: mongo.objectIdif you don't declare one. The_idfield is always non-optional.@meta.idon a non-_idfield does not make that field a MongoDB primary key. Instead, the adapter creates a unique index on it and registers it as a "unique property" for fallback lookups.findByIdfirst tries_id, then falls back to fields marked with@meta.id. This meansfindById(1)works even when1is the auto-incrementedidfield rather than an ObjectId.prepareId()automatically converts string IDs toObjectIdinstances (formongo.objectIdfields) or to numbers (for numeric_idfields), so you can pass string values from URL parameters directly.
// All of these work:
await todos.findById(new ObjectId('507f1f77bcf86cd799439011')) // by _id
await todos.findById('507f1f77bcf86cd799439011') // string -> ObjectId
await todos.findById(42) // by @meta.id fieldAuto-Increment
The @db.default.fn 'increment' annotation enables auto-increment behavior for numeric fields:
@meta.id
@db.default.fn 'increment'
id: numberHow it works:
- On
insertOne, the adapter reads the current maximum value for the field via a$groupaggregation ({ $max: "$id" }). - It assigns
max + 1to the field. - On
insertMany, values are batch-assigned in order -- the first item getsmax + 1, the second getsmax + 2, and so on. - If a document already has an explicit value for the field, that value is kept and the running maximum is updated accordingly.
WARNING
Auto-increment uses no transactions or retries. It is simple and predictable for typical workloads, but concurrent inserts could produce duplicate values under high contention. For guaranteed uniqueness, combine @db.default.fn 'increment' with @db.index.unique.
Nested Objects
Unlike relational databases where nested objects are flattened into __-separated columns, MongoDB stores nested objects natively. The adapter skips flattening entirely -- nested JavaScript objects are passed through to MongoDB as-is and read back without reconstruction.
@db.table 'users'
@db.mongo.collection
export interface User {
@meta.id
@db.default.fn 'increment'
id: number
name: string
contact: {
email: string
phone?: string
}
preferences: {
theme: string
lang: string
}
}// Insert with nested objects -- stored natively in MongoDB
await users.insertOne({
name: 'Alice',
contact: { email: 'alice@example.com', phone: '555-0100' },
preferences: { theme: 'dark', lang: 'en' },
})
// Query by nested path -- dot-notation works directly
const result = await users.findMany({
filter: { 'contact.email': 'alice@example.com' },
controls: { $sort: { 'preferences.theme': 1 } },
})TIP
Since MongoDB handles nested objects natively, the @db.json annotation has no effect -- there is no flattening to override. You can still use it for documentation purposes, but it does not change storage behavior.
Accessing the Adapter
For operations beyond the standard CRUD interface, use table.getAdapter() to access the MongoAdapter directly. This gives you the raw MongoDB Collection and all driver methods:
const adapter = todos.getAdapter()
// Run an aggregation pipeline
const cursor = adapter.collection.aggregate([
{ $match: { completed: true } },
{ $group: { _id: null, count: { $sum: 1 } } },
])
const results = await cursor.toArray()
// Use any MongoDB driver method
await adapter.collection.bulkWrite([...])
await adapter.collection.distinct('status')You can also get the adapter via AsMongo:
const adapter = mongo.getAdapter(Todo)
const collection = adapter.collectionSee Also
- Core Annotations --
@db.table,@db.index.*,@db.default.*,@db.json - MongoDB Patch Operations -- Array patch pipelines, merge/replace strategies
- MongoDB Annotations --
@db.mongo.collection,@db.mongo.search.* - Search & Vectors -- Atlas Search and vector search indexes
- DB Tables --
AtscriptDbTableAPI reference