Models and definitions
Declare keyspaces, metadata, indexes, keys, and typed registries.
A definition describes a document type: keyspace, indexes, codecs, keys, and hooks. It performs no I/O. Binding it to a client (or constructing a legacy singleton Model) is what gives you reads and writes.
Legacy singleton Model
import { Model } from 'couchset/next';
const orders = new Model('Order', {
scope: 'sales',
collection: 'orders',
softDelete: true,
dateFields: ['submittedAt'],
indexes: [{ name: 'idx_order_status', fields: ['tenantId', 'status'] }],
});name becomes _type. scope and collection select the physical keyspace; omitted parts use _default. Writes add id, createdAt, updatedAt, _type, _scope, and, for soft deletion, deleted (plus deletedAt when soft-deleting).
Typed definitions and registry
import { createCouchsetClient, dateCodec, defineModel } from 'couchset/next';
type Order = {
id: string;
tenantId: string;
status: string;
submittedAt?: Date;
};
const definition = defineModel<Order>({
name: 'Order',
scope: 'sales',
collection: 'orders',
codecs: { submittedAt: dateCodec },
collectionSettings: { maxExpiry: 86_400, history: false },
indexes: [
{ name: 'idx_order_status', fields: ['tenantId', { submittedAt: 'DESC' }] },
],
});
const db = createCouchsetClient({ bucketName: 'app', models: [definition] });
const orders = db.model(definition);
db.definitions(); // manifest of registered definitions
await db.registerModel(otherDefinition); // runtime registrationDefinitions can be reused across clients. db.model(definition) binds and registers. Neither defineModel nor binding creates Couchbase resources — see Provisioning and transactions.
defaultWhere takes precedence over soft delete
If defaultWhere is truthy, it replaces CouchSet's automatic {deleted: {$isMissing:true}} default; the two are not combined. To use both, write one explicit $and predicate containing your tenant condition and the deleted-is-missing condition. withDeleted() and withoutDefaultWhere() then remove the entire default, while onlyDeleted() replaces it with {deleted: {$eq:true}}.
Document modeling tips
Prefer paging lightweight list/metadata documents and fetching heavy payloads by id (getById / findDocById) when a detail view needs them. Denormalize card fields onto the list model; keep large nested state in a companion document. Includes can join companions for focused detail reads — avoid joining large payloads into list pages. Stable id formulas (report::123 / report-payload::report::123) make backfills obvious.
See document modeling (repo) for a fuller split/migration sketch.
Consumer-defined keys
key: {
create: (user) => `user::${user.email.toLowerCase()}`,
parse: (id) => (id.startsWith('user::') ? id.slice(6) : null),
explicitId: 'validate', // default: allow explicit IDs; 'reject' refuses them
}| Piece | Behavior |
|---|---|
create | Synchronous; runs only when insert/upsert has no ID |
parse | Used when explicitId: 'validate' — must return a truthy synchronous result |
explicitId: 'reject' | Refuses caller-supplied IDs |
Strategies do not rewrite reads, patch, replace, delete, or existing keys. Transaction inserts need explicit IDs, so reject disallows those inserts.
Bound model surfaces
Bound models expose getCollection(), bucket(), keyspace(), and from(alias?) for low-level integrations. Typed client models also carry codecs, hooks, search indexes, and the safe read grammar from CRUD and queries.
Pitfalls
- Treating definitions as provisioned resources.
- Combining soft-delete defaults with a custom
defaultWherewithout an explicit$and. - Paging huge documents that only need a few list fields.
- Expecting key strategies to rewrite existing document ids on read.
See also
- Connections and clients — singleton vs client-owned
- Validation and codecs — schema, codecs, validators
- Plugins, hooks, scopes, and operations — plugins and hooks
- CRUD and queries — reads/writes on bound models
- Provisioning and transactions — ensure/plan
- API reference —
defineModeloptions