# Model Diagrams (/docs/diagrams) Zog can render model schemas and validated references as ASCII or Mermaid diagrams. This is useful for documenting MongoDB collections without turning Zog into an ORM or requiring a CLI to understand your TypeScript project. If your models declare references, diagrams include those references as annotations or relationship edges. Use the pure renderers when you want to print or compose the output yourself: ```ts import { renderDbDiagram } from "@mp-lb/zog"; import { postModel, userModel } from "./models"; const models = [userModel, postModel] as const; console.log(renderDbDiagram(models)); console.log(renderDbDiagram(models, { format: "mermaid" })); ``` The default ASCII view combines schema shape and references: ```txt posts collection (posts) ├── _id <- id: string [primary] ├── authorId: string -> users.id └── comments: object[] ├── authorId: string -> users.id └── body: string ``` Mermaid output uses a flowchart so embedded documents and cross-model references can appear in one diagram. ```ts renderDbDiagram(models, { format: "mermaid" }); ``` Use `view` when you only want one part of the model map: ```ts renderDbDiagram(models, { view: "schema" }); renderDbDiagram(models, { view: "relationships" }); renderDbDiagram(models, { format: "mermaid", view: "relationships" }); ``` ## Write Diagram Files [#write-diagram-files] Most applications should call the writer from an app-owned script. This avoids requiring a Zog CLI to understand every TypeScript loader, bundler, path alias, and environment setup. ```ts // scripts/write-model-diagram.ts import { writeDbDiagramFile } from "@mp-lb/zog"; import { models } from "../src/models"; await writeDbDiagramFile("docs/data-models.md", models, { format: "mermaid", title: "Data Models", }); ``` Run it with the TypeScript runner your project already uses: ```sh tsx scripts/write-model-diagram.ts ``` Markdown files (`.md` and `.mdx`) are written with fenced code blocks. Other file extensions are written as raw diagram text. ```ts await writeDbDiagramFile("docs/data-models.md", models); await writeDbDiagramFile("docs/data-models.txt", models); await writeDbDiagramFile("docs/data-models.mmd", models, { format: "mermaid", }); ``` For one model, use `renderModelDiagram` or `writeModelDiagramFile`. Diagram rendering requires Zod 4 schemas because it introspects schema internals for this feature. # Quick Start (/docs) Install Zog with Zod and the MongoDB driver: ```sh pnpm add @mp-lb/zog zod mongodb ``` ## 1. Define A Model [#1-define-a-model] ```ts import { MongoClient } from "mongodb"; import { z } from "zod"; import { createModel, defineDb } from "@mp-lb/zog"; const userSchema = z.object({ id: z.string().trim().min(1), email: z.string().email(), name: z.string().trim().min(1), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }); type User = z.infer; const userModel = createModel("users", userSchema, { primaryKey: "id", }); ``` `createModel` accepts any schema-like object with a synchronous `parse()` method. Repository reads return the parsed output type. ## 2. Create The Database Adapter [#2-create-the-database-adapter] ```ts const mongoClient = new MongoClient(process.env.MONGODB_URI!); await mongoClient.connect(); const db = defineDb([userModel] as const, { mongoClient, databaseName: "app", }); ``` The `as const` is important. It lets TypeScript infer `db.users` from the literal model name. ## 3. Read And Write [#3-read-and-write] ```ts const now = new Date().toISOString(); const user: User = { id: "user_1", email: "test@example.com", name: "Test User", createdAt: now, updatedAt: now, }; await db.users.insertOne(user); const byId = await db.users.findById("user_1"); const byEmail = await db.users.findOne({ email: "test@example.com" }); const page = await db.users.find({}).sort({ email: 1 }).limit(20).toArray(); await db.users.updateOne( { id: "user_1" }, { $set: { name: "Updated User", updatedAt: new Date().toISOString() } }, ); await db.users.deleteOne({ id: "user_1" }); ``` Writes store one canonical Mongo primary key, `_id`, mapped from the domain primary key. On reads, Zog maps `_id` back to the configured domain primary key before parsing through the schema. ```ts // Escape hatch: raw MongoDB collection, no Zog parsing or primary-key mapping. await db.users.raw.aggregate([]).toArray(); ``` # Indexes (/docs/indexes) Define indexes on the model: ```ts import { createModel, defineDb, uniqueIndex } from "@mp-lb/zog"; const userModel = createModel("users", userSchema, { primaryKey: "id", indexes: [uniqueIndex({ email: 1 }, { name: "users_email_unique" })], }); ``` Then create declared indexes from the database adapter: ```ts const db = defineDb([userModel] as const, { mongoClient, databaseName: "app", }); await db.ensureIndexes(); ``` `ensureIndexes()` is additive: it creates declared indexes and leaves existing indexes alone. ## Diff And Sync [#diff-and-sync] Use the diff and sync APIs when you want to reconcile MongoDB with your model definitions: ```ts const diff = await db.diffIndexes(); const dryRun = await db.syncIndexes({ dryRun: true }); await db.syncIndexes(); ``` `diffIndexes()` reports matching, missing, changed, and extra indexes for each model. `syncIndexes()` drops changed indexes, creates missing and changed declared indexes, and drops undeclared extra indexes by default. Use `{ dropExtra: false }` to keep extra indexes while still fixing changed declared indexes: ```ts await db.syncIndexes({ dropExtra: false }); ``` ## Legacy Cleanup [#legacy-cleanup] For legacy index cleanup that needs custom MongoDB calls, use `beforeEnsureIndexes`: ```ts const fileModel = createModel("files", fileSchema, { primaryKey: "id", beforeEnsureIndexes: async (collection) => { await collection.dropIndex("ownerId_1"); }, indexes: [uniqueIndex({ workspaceId: 1, path: 1 })], }); ``` `beforeEnsureIndexes` runs before Zog creates the declared indexes for that model. # References (/docs/references) References describe fields that contain another model's primary key. They do not enforce that the referenced document exists in MongoDB, but they do verify that your model definitions agree with your Zod schemas. ```ts import { createModel, ref } from "@mp-lb/zog"; import { z } from "zod"; const userSchema = z.object({ id: z.string(), email: z.string().email(), }); export const userModel = createModel("users", userSchema, { primaryKey: "id", }); const postSchema = z.object({ id: z.string(), authorId: z.string(), }); export const postModel = createModel("posts", postSchema, { primaryKey: "id", references: [ref("authorId", userModel)], }); ``` When `postModel` is created, Zog checks that `authorId` exists, is required, is non-null, and has the same schema type as `userModel`'s primary key. ## Embedded Documents [#embedded-documents] Reference paths can point through embedded documents and arrays with dotted paths and `[]` array markers. ```ts const teamSchema = z.object({ id: z.string(), members: z.array( z.object({ userId: z.string(), role: z.enum(["owner", "member"]), }), ), }); export const teamModel = createModel("teams", teamSchema, { primaryKey: "id", references: [ref("members[].userId", userModel)], }); ``` Nested arrays work the same way: ```ts ref("teams[].members[].userId", userModel); ``` ## Optional And Nullable References [#optional-and-nullable-references] References are required and non-null by default. Mark optional or nullable references explicitly so the model documents the intended shape. ```ts const postSchema = z.object({ id: z.string(), authorId: z.string(), reviewedByUserId: z.string().optional(), deletedByUserId: z.string().nullable(), }); export const postModel = createModel("posts", postSchema, { primaryKey: "id", references: [ ref("authorId", userModel), ref("reviewedByUserId", userModel, { optional: true }), ref("deletedByUserId", userModel, { nullable: true }), ], }); ``` ## What Zog Checks [#what-zog-checks] For each reference, Zog verifies that: * the referenced path exists in the source model's schema * every object and array segment in the path is valid * the source field is required unless `{ optional: true }` is set * the source field is non-null unless `{ nullable: true }` is set * the source field type matches the target model's primary key type * the target model's primary key is required and non-null Zog does not check whether the referenced document exists in MongoDB. That would require database reads and application-specific consistency choices. Reference validation requires Zod 4 schemas because Zog introspects schema internals for this feature. # Schema Evolution (/docs/schema-evolution) Zog keeps migration concerns outside the current schema. Use this page when the stored shape has history, but application code should only see the current model. ## Collection Names [#collection-names] If the logical model name differs from the physical Mongo collection, pass `collectionName`: ```ts const userModel = createModel("users", userSchema, { primaryKey: "id", collectionName: "user_accounts", }); ``` If a collection used to have another name, declare it as legacy: ```ts const userModel = createModel("users", userSchema, { primaryKey: "id", collectionName: "user_accounts", legacyCollectionNames: ["users"], }); ``` If `user_accounts` does not exist yet but `users` does, repository and index operations use the declared legacy collection. If both names exist, Zog throws instead of choosing between split data. ## Collection Name Policy [#collection-name-policy] Use `collectionNamePolicy` when you want a naming rule for every model: ```ts const db = defineDb([userModel] as const, { mongoClient, databaseName: "app", collectionNamePolicy: "snake", collectionNameCompatibility: "error", }); ``` `collectionNameCompatibility: "error"` adds a guard before repository and index operations, so an undeclared old collection is reported before Zog creates a new one. ## Key Renames [#key-renames] For stored documents that still use old key names, declare read-time key renames: ```ts const userModel = createModel("users", userSchema, { primaryKey: "id", legacyKeyRenames: [ { from: "full_name", to: "name" }, { from: "profile.display_name", to: "profile.displayName" }, { from: "teams[].members[].full_name", to: "teams[].members[].name" }, ], }); ``` `legacyKeyRenames` runs before schema parsing on reads. `[]` applies the rename to every object in an array. If both the legacy and current keys exist, the current key wins. Zog removes the legacy key from the parse candidate, but does not rewrite MongoDB automatically. ## Custom Legacy Shapes [#custom-legacy-shapes] Use `normalizeLegacy` when the old shape needs custom code: ```ts const userModel = createModel("users", userSchema, { primaryKey: "id", normalizeLegacy: (doc) => ({ ...doc, name: doc.name ?? [doc.firstName, doc.lastName].filter(Boolean).join(" "), }), }); ``` Declarative key renames run before `normalizeLegacy`, so the custom function can focus on the cases that are not simple renames.