CRUD Operations
Atscript's DB layer provides a type-safe API for creating, reading, updating, and deleting records. All operations go through AtscriptDbTable, which handles validation, default values, nested object flattening, and adapter translation automatically.
Getting a Table Instance
import { DbSpace } from '@atscript/db'
import { User } from './schema/user.as'
const users = db.getTable(User) // AtscriptDbTable<typeof User>getTable() returns a cached instance — calling it again with the same type returns the same table. See Setup for how to create a DbSpace.
Inserting Records
Insert One
Insert a single record and get back the generated primary key:
const result = await users.insertOne({
email: 'alice@example.com',
name: 'Alice',
status: 'active',
})
// result: { insertedId: 1 }Fields with @db.default.* annotations (@db.default.increment, @db.default.uuid, @db.default.now, or @db.default 'value') are applied automatically — you can omit them from the input.
Insert Many
Insert multiple records in a single transaction:
const result = await users.insertMany([
{ email: 'alice@example.com', name: 'Alice' },
{ email: 'bob@example.com', name: 'Bob' },
{ email: 'charlie@example.com', name: 'Charlie' },
])
// result: { insertedCount: 3, insertedIds: [1, 2, 3] }Nested Creation
Both insertOne and insertMany support nested relation data — inserting related records across foreign keys in a single call. This is covered in Relations — Deep Operations.
Reading Records
Find by ID
Look up a single record by primary key:
const user = await users.findById(1)
// Returns the record or nullfindById is flexible — it accepts:
- A single primary key value
- An object with composite key fields
- A value matching a single-field unique index
- An object matching a compound unique index
Find One
Return the first record matching a filter:
const user = await users.findOne({
filter: { email: 'alice@example.com' },
})
// Returns the first match or nullFind Many
Return all records matching a filter, with optional sorting and pagination:
const active = await users.findMany({
filter: { status: 'active' },
controls: {
$sort: { name: 1 },
$limit: 10,
$skip: 0,
},
})For a full reference on filter operators and controls, see Queries & Filters.
Count
Count matching records without fetching data:
const total = await users.count({
filter: { status: 'active' },
})Pass no arguments to count all records:
const allUsers = await users.count()Find Many with Count
Get both data and total count in one call — useful for paginated UIs:
const { data, count } = await users.findManyWithCount({
filter: { status: 'active' },
controls: { $limit: 10, $skip: 20 },
})
// data: first 10 records after skipping 20
// count: total matching records (ignoring $limit/$skip)Updating Records
Update One
Partially update a record. The primary key field(s) must be included to identify the record — only the other provided fields are changed:
const result = await users.updateOne({
id: 1,
name: 'Alice Smith',
})
// result: { matchedCount: 1, modifiedCount: 1 }Patch Operators
For updating embedded arrays and nested objects with fine-grained control, see Update & Patch.
Update Many
Update all records matching a filter:
const result = await users.updateMany(
{ status: 'inactive' }, // filter
{ status: 'archived' }, // data to set
)
// result: { matchedCount: 5, modifiedCount: 5 }updateMany does not support nested relation operations — only own fields.
Replacing Records
Replace One
Replace an entire record by primary key. Unlike updateOne, all fields must be provided — missing fields are not preserved:
const result = await users.replaceOne({
id: 1,
email: 'alice.new@example.com',
name: 'Alice Smith',
status: 'active',
})Replace vs. Update
updateOne— sends only the fields you want to change (partial)replaceOne— replaces the entire record with new data (full)
Deleting Records
Delete One
Delete a single record by ID:
const result = await users.deleteOne(1)
// result: { deletedCount: 1 }deleteOne accepts the same flexible ID format as findById — primary key, composite key object, or unique index value.
Delete Many
Delete all records matching a filter:
const result = await users.deleteMany({
status: 'archived',
})
// result: { deletedCount: 12 }Cascade & Set-Null
When a deleted record is referenced by other tables via foreign keys, cascade and set-null behaviors are handled automatically based on @db.rel.onDelete annotations. See Relations for details.
Validation
Tables automatically validate data on every write operation using constraints from your .as definitions (@expect.* annotations). Validation is purpose-aware:
| Purpose | Used by | Behavior |
|---|---|---|
'insert' | insertOne, insertMany | PK, defaulted, and FK fields become optional |
'bulkUpdate' | updateOne, bulkUpdate | Top level is partial; merge-strategy objects partial, replace-strategy objects require all fields |
'bulkReplace' | replaceOne, bulkReplace | All non-optional fields required |
'patch' | updateMany | Fully partial |
You can access validators directly for manual checks:
const validator = users.getValidator('insert')
if (!validator.validate(data, true)) {
// safe = true → returns false instead of throwing
console.log(validator.errors)
// [{ path: 'email', message: 'Required field' }, ...]
}Error Handling
Database operations throw DbError with a code property indicating the error type:
| Code | Meaning |
|---|---|
CONFLICT | Unique constraint violation |
FK_VIOLATION | Foreign key constraint violated |
NOT_FOUND | Record not found |
CASCADE_CYCLE | Circular cascade detected |
INVALID_QUERY | Malformed query or filter |
Handle errors by checking the code:
import { DbError } from '@atscript/db'
try {
await users.insertOne({ email: 'alice@example.com', name: 'Alice' })
} catch (err) {
if (err instanceof DbError) {
switch (err.code) {
case 'CONFLICT':
console.log('Email already exists:', err.errors)
break
case 'FK_VIOLATION':
console.log('Referenced record missing:', err.errors)
break
}
}
}Each error includes an errors array with { path, message } entries for detailed diagnostics.
Error Paths in Nested Data
When validation fails inside nested or array payloads, error paths use dot notation to pinpoint the exact location:
| Context | Example path | Meaning |
|---|---|---|
| Top-level field | "title" | The title field failed validation |
| TO navigation | "project.title" | The title field inside inline project data |
| FROM array element | "comments.0.body" | The body field of the first comment in the array |
| Deep nesting | "tasks.2.project.title" | The title of the project in the third task |
This makes it straightforward to map errors back to specific fields in complex nested payloads — useful for building form validation UIs.
Next Steps
- Queries & Filters — Advanced filtering, sorting, and projection
- Update & Patch — Embedded array and object patch operators
- Transactions — Atomic multi-table operations
- Relations — Deep Operations — Nested creation and replacement across relations