Models in code

Selected databaseMongoDB

Models are schema in normal English: who the data is about, what you store, and how things link (“each order belongs to one customer”). You do not need to write field types like name (string) — say “their full name” and “unique login email” instead. The compiler maps that to collections/tables, columns, and relationships. Your queries then use that schema.

You can put MaskModels.define('...') anywhere in your codebase. The compiler finds all define calls across your project — just like in a normal Node app where schemas can live in any file. No need for one file per model unless you prefer it.

Describe collections the way you would to a teammate

With MongoDB (native driver), MaskModels.define() is optional but recommended: it gives the compiler structured metadata so natural-language queries line up with your real collections and fields. Without it, the AI has less context. If you need real Mongoose schemas and validation at the ODM layer, set database to "mongoose" in overrideConfig in mask.compile.cjs instead — there, model definitions are required and become mongoose.Schema (see the Mongoose docs tab).

  • Use normal sentences: who the data is about, what you store, what must be unique.
  • Spell out relationships in words: belongs to, references, one customer has many orders.
  • Split big domains into several MaskModels.define() calls — one per collection (or small group).

Example: customers and regions

A simple catalog of who you sell to.

src/models/Customers.js (or anywhere in your app)
const { MaskModels } = require('mask-databases');

MaskModels.define(
  'Customers. Collection customers. Each document is a customer we work with. ' +
  'Store the name we know them by, which sales region they sit in, and when we first added them to the system.'
);

Example: orders tied to customers

Orders belong to exactly one customer. Say that in plain language so joins and lookups compile correctly.

MaskModels.define(
  'Orders. Collection orders. Every order was placed by a single customer — keep a reference to that customer. ' +
  'Track the date the order was placed and whether it is still open, already paid, or cancelled.'
);

Example: line items on an order

Each line belongs to one order and describes one product line — quantity and money for that line.

MaskModels.define(
  'Order line items. Collection order_lines. Each document is one line on an order. ' +
  'It belongs to exactly one order and refers to one product from the catalog. ' +
  'Record how many units and the total amount charged for that line.'
);

Example: product catalog and staff users

Products are what you sell; users are people who log into your app.

MaskModels.define(
  'Products. Collection products. Things we sell: the title customers see, a short description, and the usual price per unit.'
);

MaskModels.define(
  'Users. Collection users. People who sign into the app. Their full name, the email they log in with ' +
  '(two people must not share the same email), and whether the account is active or turned off.'
);

Inspecting a model (debugging)

Use MaskModels.getModelForPrompt('...') to inspect the compiled schema for a model without instantiating it. Pass the exact same prompt string you used in MaskModels.define('...'). This is useful for verifying that the compiler generated the right collection name, fields, types, and relations from your natural language description.

Returns an object with spec (the full schema), readable (a human-readable summary), and hash (identifier), or null if the prompt is not compiled.

const info = MaskModels.getModelForPrompt(
  'User model. Collection users. Fields: name (string), email (string, unique), role (string, enum: admin, user).'
);

if (info) {
  console.log(info.readable);
  // Model: User
  // Collection: users
  // Fields:
  //   name: string
  //   email: string, unique
  //   role: string, enum:[admin,user]

  console.log(info.spec);
  // {
  //   modelName: 'User',
  //   collection: 'users',
  //   fields: {
  //     name: { type: 'string' },
  //     email: { type: 'string', unique: true },
  //     role: { type: 'string', enum: ['admin', 'user'] }
  //   },
  //   relations: []
  // }

  console.log(info.hash);
  // "a1b2c3d4"
}

What the readable output shows

FieldDescription
readableA human-readable summary of the model: name, collection, all fields with types and constraints, and relations.
specThe full schema object: modelName, collection, fields (with type, required, unique, ref, enum), and relations.
hashA stable identifier for this prompt.

Inspecting relations

If the model has relations (references to other models), they appear in both spec.relations and the readable string. Each relation shows the local field, foreign collection, foreign field, and relationship type.

const info = MaskModels.getModelForPrompt(
  'Order model. Collection orders. Fields: product (string), quantity (number), userId (objectId, ref User).'
);

console.log(info.readable);
// Model: Order
// Collection: orders
// Fields:
//   product: string
//   quantity: number
//   userId: objectId, ref:User
// Relations:
//   userOrders: many-to-one (userId -> users._id)

For query debugging, see getQueryForPrompt on the Queries page.

Formatting a model spec

If you already have a spec object (e.g. from getModelForPrompt().spec), you can format it into a readable string using MaskModels.formatModelSpec(spec).

const readable = MaskModels.formatModelSpec({
  modelName: 'User',
  collection: 'users',
  fields: {
    name: { type: 'string', required: true },
    email: { type: 'string', unique: true }
  },
  relations: []
});

console.log(readable);
// Model: User
// Collection: users
// Fields:
//   name: string, required
//   email: string, unique

After changing models

Run node mask.compile.cjs so the schema and your queries stay in sync. See Compiling.

Legacy app mode: use exact existing schema

For already-running apps, you can put your existing schema details directly in MaskModels.define('...') instead of only descriptive prose. If the input is actual schema text (for example explicit field/type/ref details, or SQL-style table definition details), Mask preserves those semantics instead of redesigning your model.

  • MongoDB / Mongoose: keep your exact field names, types, required/unique rules, and references in the define text.
  • SQL databases: include exact table/column types and constraints in your model prompt text so compile output stays aligned.
  • Neo4j: include exact label/property/relationship details when migrating existing graph models.

This is the recommended migration path for old projects: keep model intent exact first, then optionally simplify prompt wording later.

Migration examples: old files to Mask classes

// BEFORE (existing app file: src/models/User.js)
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: { type: String, required: true },
  status: { type: String, enum: ['active', 'disabled'], default: 'active' },
  teamId: { type: mongoose.Schema.Types.ObjectId, ref: 'Team', required: true }
}, { timestamps: true });

module.exports = mongoose.model('User', UserSchema);
// AFTER (Mask model file — paste full old schema)
const { MaskModels } = require('@local/mask');

const UserSchema = MaskModels.define(`
new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: { type: String, required: true },
  status: { type: String, enum: ['active', 'disabled'], default: 'active' },
  teamId: { type: mongoose.Schema.Types.ObjectId, ref: 'Team', required: true }
}, { timestamps: true })
`);
// BEFORE (existing migration file)
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);
// AFTER (Mask model file — paste full SQL DDL)
const { MaskModels } = require('@local/mask');

const OrdersSchema = MaskModels.define(`
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL,
  CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);
`);
// BEFORE (existing graph schema notes/code)
// (:Person { id, name, email })
// (:Company { id, name })
// (:Person)-[:WORKS_AT]->(:Company)
// AFTER (Mask model file — paste full Neo4j schema/cypher intent)
const { MaskModels } = require('@local/mask');

const PersonSchema = MaskModels.define(`
// Nodes
(:Person { id, name, email })
(:Company { id, name })

// Relationship
(:Person)-[:WORKS_AT]->(:Company)

// Constraints
CREATE CONSTRAINT person_id_unique IF NOT EXISTS
FOR (p:Person) REQUIRE p.id IS UNIQUE;
CREATE CONSTRAINT company_id_unique IF NOT EXISTS
FOR (c:Company) REQUIRE c.id IS UNIQUE;
`);

Migration rule: paste old schema text as-is in MaskModels.define(...), and keep model declarations in the same sequence as old schema definitions so refs/relations stay aligned across compile and runtime.