Database layer for Atscript
Your schema is your entire backend
Tables, relations, views, sync, and REST — from a single .as file. No ORM configuration. No migration files. No boilerplate.
Stop scattering your data definitions across ORM configs, migration scripts, and validation layers. One .as file holds your table name, columns, types, indexes, defaults, and constraints. Nothing else to maintain.
@db.table 'products'
export interface Product {
@meta.id
@db.default.increment
id: number
@db.index.fulltext 'search_idx', 3
name: string
description?: string
@db.index.unique 'sku_idx'
sku: string
price: number
@db.default 'active'
status: 'active' | 'archived' | 'draft'
createdAt: number.timestamp.created
}Foreign keys, navigation properties, and cascade rules belong with the data they describe — not in a separate config file you have to keep in sync by hand.
import { Customer } from './customer'
import { OrderItem } from './order-item'
@db.table 'orders'
export interface Order {
@meta.id
@db.default.uuid
id: string
@db.rel.FK
@db.rel.onDelete 'cascade'
customerId: Customer.id
// navigate to parent
@db.rel.to Customer 'customerId'
customer: Customer
// navigate to children
@db.rel.from OrderItem.orderId
items: OrderItem[]
total: number
createdAt: number.timestamp.created
} JOINs, filters, and GROUP BY aggregations — declared in your schema, not buried in SQL strings or query builder chains. Schema sync generates the CREATE VIEW for you.
import { Order } from './order'
import { Customer } from './customer'
@db.view 'order_stats'
@db.view.for Order
@db.view.joins Customer, `Customer.id = Order.customerId`
@db.view.filter `Order.status = 'completed'`
export interface OrderStats {
// GROUP BY column
region: Customer.region
@db.agg.sum "total"
revenue: number
@db.agg.count
orderCount: number
@db.agg.avg "total"
avgOrder: number
}Insert, query, update, and delete with a clean API that knows your schema. Filters, sorting, pagination, and full-text search — all type-checked against your model.
import { Product } from "./schema/product.as";
const products = db.getTable(Product);
// insert
await products.insertOne({
name: "Wireless Keyboard",
sku: "KB-200",
price: 79.99,
});
// query with filters and pagination
const results = await products.findMany({
filter: { status: "active", price: { $lte: 100 } },
controls: { $sort: { price: 1 }, $limit: 20 },
});
// full-text search
const matches = await products.search("wireless keyboard"); Migration files accumulate, drift, and break. Schema sync compares your .as definitions against the live database and applies the difference. Hash-gated — zero cost when nothing changed. Safe mode blocks destructive changes in production.
Writing CRUD controllers by hand is work that adds no value. Extend AsDbController, point it at a table, and get filtering, sorting, pagination, and search endpoints with zero boilerplate.
import { AsDbController, TableController } from "@atscript/moost-db";
import { productsTable } from "./db";
import { Product } from "./schema/product.as";
@TableController(productsTable)
export class ProductController extends AsDbController<typeof Product> {}
// GET, POST, PUT, PATCH, DELETE — ready.GET /products # list, filter, sort, paginate
GET /products/:id # read one
POST /products # create
PUT /products/:id # replace
PATCH /products/:id # partial update
DELETE /products/:id # delete
GET /products/search # full-text searchPrototype with SQLite. Ship with PostgreSQL. Switch to MongoDB. Your schema, queries, and controllers stay the same — only the adapter changes.