> ## Documentation Index
> Fetch the complete documentation index at: https://woltz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema Registry

> Map domain entities to database tables and fields

## Overview

`EntitySchemaRegistry` provides a centralized mapping between your domain entities and database tables. It handles field name translations, foreign key relationships, and collection configurations - making your mappers cleaner and more maintainable.

```typescript theme={null}
import { EntitySchemaRegistry } from "@woltz/rich-domain";

const registry = new EntitySchemaRegistry()
  .register({
    entity: "User",
    table: "users",
    fields: { email: "user_email" },
  })
  .register({
    entity: "Post",
    table: "blog_posts",
    fields: { content: "post_content" },
    parentFk: { field: "author_id", parentEntity: "User" },
  });
```

## Why Use Schema Registry?

<CardGroup cols={2}>
  <Card title="Centralized Mapping" icon="sitemap">
    Define all entity-to-table mappings in one place
  </Card>

  <Card title="Field Translation" icon="language">
    Map camelCase domain fields to snake\_case database columns
  </Card>

  <Card title="FK Management" icon="link">
    Configure parent-child relationships and foreign keys
  </Card>

  <Card title="Collection Types" icon="layer-group">
    Distinguish between owned (1:N) and reference (N:N) relations
  </Card>
</CardGroup>

## Basic Usage

### Registering Entities

```typescript theme={null}
import { EntitySchemaRegistry } from "@woltz/rich-domain";

const registry = new EntitySchemaRegistry();

// Simple entity - same field names in domain and database
registry.register({
  entity: "Category",
  table: "categories",
});

// Entity with field mapping
registry.register({
  entity: "User",
  table: "users",
  fields: {
    firstName: "first_name",  // domain → database
    lastName: "last_name",
    createdAt: "created_at",
  },
});

// Chained registration
registry
  .register({ entity: "User", table: "users" })
  .register({ entity: "Post", table: "posts" })
  .register({ entity: "Comment", table: "comments" });
```

### Bulk Registration

```typescript theme={null}
registry.registerAll([
  { entity: "User", table: "users" },
  { entity: "Post", table: "posts" },
  { entity: "Comment", table: "comments" },
  { entity: "Tag", table: "tags" },
]);
```

## Field Mapping

Map domain property names to database column names:

```typescript theme={null}
registry.register({
  entity: "User",
  table: "users",
  fields: {
    firstName: "first_name",
    lastName: "last_name",
    emailAddress: "email",
    phoneNumber: "phone",
    createdAt: "created_at",
    updatedAt: "updated_at",
  },
});

// Get mapped field name
registry.mapFieldName("User", "firstName"); // "first_name"
registry.mapFieldName("User", "status");    // "status" (unchanged)

// Get all field mappings
registry.getFieldsMap("User");
// { firstName: "first_name", lastName: "last_name", ... }
```

### Mapping Entity Data

Transform a complete domain entity to database format:

```typescript theme={null}
const user = new User({
  firstName: "John",
  lastName: "Doe",
  email: "john@example.com",
  posts: [...], // Ignored - it's a collection
});

const dbData = registry.mapEntity("User", user);
// {
//   id: "user-123",
//   first_name: "John",
//   last_name: "Doe",
//   email: "john@example.com"
// }
```

### Mapping Partial Fields

Transform only specific fields (useful for updates):

```typescript theme={null}
const changedFields = { firstName: "Jane", updatedAt: new Date() };

const dbFields = registry.mapFields("User", changedFields);
// { first_name: "Jane", updated_at: Date }
```

## Parent-Child Relationships

Configure foreign key relationships for 1:N (owned) relationships:

```typescript theme={null}
registry
  .register({
    entity: "User",
    table: "users",
  })
  .register({
    entity: "Post",
    table: "posts",
    parentFk: {
      field: "author_id",      // FK column name in database
      parentEntity: "User",    // Parent entity name
    },
  })
  .register({
    entity: "Comment",
    table: "comments",
    parentFk: {
      field: "post_id",
      parentEntity: "Post",
    },
  });
```

### Getting FK Information

```typescript theme={null}
// Get parent entity name
registry.getParentEntity("Post");    // "User"
registry.getParentEntity("Comment"); // "Post"
registry.getParentEntity("User");    // null (root entity)

// Get FK field name
registry.getParentFkField("Post");    // "author_id"
registry.getParentFkField("Comment"); // "post_id"

// Get FK object for insert
registry.getParentFk("Post", "user-123");
// { author_id: "user-123" }

registry.getParentFk("Comment", "post-456");
// { post_id: "post-456" }
```

## Custom Primary Keys

By default, the registry assumes every table uses an `id` column as the primary key. For tables that use a different PK (common in 1:1 child tables sharing the parent's identity), configure `primaryKey`.

`primaryKey` is the **database column name**, not a domain property. Adapters always read the identity from `entity.id` and map it to that column. Setting `primaryKey: "factoryId"` produces `where: { factoryId: entity.id.value }` — not `{ factoryId: entity.factoryId }`.

```typescript theme={null}
registry
  .register({
    entity: "Factory",
    table: "factory",
  })
  .register({
    entity: "Profile",
    table: "factoryProfile",
    primaryKey: "factoryId",
    parentFk: { field: "factoryId", parentEntity: "Factory" },
  });

// Domain: profile.id.value === factory.id.value
// DB update: WHERE factoryId = profile.id.value
```

For 1:1 shared PK, assign `Profile.id` the same value as `Factory.id` when creating the child. Batch executors and repositories use `buildWhereById` / `buildWhereByIds` to resolve the correct column automatically.

Composite primary keys are not supported in v1.

## Collection Configuration

Configure how collections (1:N and N:N) should be handled:

```typescript theme={null}
registry.register({
  entity: "Post",
  table: "posts",
  parentFk: { field: "author_id", parentEntity: "User" },
  collections: {
    // 1:N owned relationship - comments belong to post
    comments: { type: "owned" },

    // Field name related to the relationship in the domain;
    // 'posts.tags' <- Domain Relation field name is 'tags'
    tags: {
      // N:N reference relationship - tags exist independently
      type: "reference",
      entity: "Tag",
      // Optional: use when the Prisma relation field name differs from the domain property name
      // e.g. domain: "tags", Prisma schema field: "post_tags"
      relationName: "post_tags",
      // Optional: junction table config (for ORMs that need it)
      junction: {
        table: "post_tags",
        sourceKey: "post_id",
        targetKey: "tag_id",
      },
    },
  },
});
```

### Collection Types

| Type        | Relationship | Behavior                                         |
| ----------- | ------------ | ------------------------------------------------ |
| `owned`     | 1:N          | Children are created/deleted with parent         |
| `reference` | N:N          | Only links are created/removed, entities persist |

### Checking Collection Types

```typescript theme={null}
// Check if it's a reference (N:N) collection
registry.isReferenceCollection("Post", "tags");      // true
registry.isReferenceCollection("Post", "comments");  // false

// Check if it's an owned (1:N) collection
registry.isOwnedCollection("Post", "comments");      // true
registry.isOwnedCollection("Post", "tags");          // false

// Get full collection config
const tagConfig = registry.getCollectionConfig("Post", "tags");
// { type: "reference", entity: "Tag", junction: { ... } }
```

### Mapping Domain Field Names to ORM Field Names

When the domain property name and the ORM relation field name differ, use `relationName` to configure the mapping. ORM adapters such as `PrismaBatchExecutor` use `getRelationFieldName()` to resolve the correct field for `connect`/`disconnect` operations.

```typescript theme={null}
// Prisma schema
// model Post {
//   id       String  @id
//   post_tags Tag[]  @relation("PostToTag")
// }

// Domain entity has a property named "tags", but Prisma expects "post_tags"
registry.register({
  entity: "Post",
  table: "post",
  collections: {
    tags: {
      type: "reference",
      entity: "Tag",
      relationName: "post_tags", // ← ORM field name used for connect/disconnect
    },
  },
});

// Resolve the ORM field name at runtime
registry.getRelationFieldName("Post", "tags"); // "post_tags"

// Without relationName it falls back to the domain property name
registry.getRelationFieldName("Post", "comments"); // "comments"
```

<Warning>
  If you omit `relationName` and your Prisma relation field name differs from the domain property name, `PrismaBatchExecutor` will pass the wrong field to Prisma's `connect`/`disconnect` operations and the call will fail. Always set `relationName` when the names diverge.
</Warning>

## Query Methods

```typescript theme={null}
// Check if entity is registered
registry.has("User");  // true
registry.has("Order"); // false

// Get schema (throws if not found)
const schema = registry.getSchema("User");

// Try get schema (returns null if not found)
const schema = registry.tryGetSchema("Order"); // null

// Get table name
registry.getTable("User"); // "users"

// Get all registered entity names
registry.getRegisteredEntities(); // ["User", "Post", "Comment"]

// Get all schemas
registry.getAllSchemas(); // [{ entity: "User", ... }, ...]

// Clear all registrations
registry.clear();
```

## Using with Mappers

The registry is commonly used in persistence mappers:

```typescript theme={null}
import { Mapper, EntitySchemaRegistry } from "@woltz/rich-domain";

class UserToPersistenceMapper extends Mapper<User, UserRecord> {
  constructor(private registry: EntitySchemaRegistry) {
    super();
  }

  build(user: User): UserRecord {
    // Use registry to map the entity
    const data = this.registry.mapEntity("User", user);
    
    return {
      ...data,
      // Add any custom transformations
      status: user.status.toUpperCase(),
    };
  }
}
```

## Using with BatchExecutor

The registry integrates with `PrismaBatchExecutor` for automatic field mapping:

```typescript theme={null}
import { PrismaBatchExecutor } from "@woltz/rich-domain-prisma";

const executor = new PrismaBatchExecutor(prismaContext, {
  registry: schemaRegistry,
});

// BatchExecutor uses registry to:
// - Get correct table names
// - Map field names
// - Handle FK relationships
await executor.execute(changes);
```

## Complete Example

```typescript theme={null}
import { EntitySchemaRegistry } from "@woltz/rich-domain";

// Define the schema registry for your domain
export const schemaRegistry = new EntitySchemaRegistry()
  // Root aggregate
  .register({
    entity: "User",
    table: "users",
    fields: {
      firstName: "first_name",
      lastName: "last_name",
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
    collections: {
      posts: { type: "owned" },
      followers: { type: "reference", entity: "User" },
    },
  })
  // Child entity (1:N owned)
  .register({
    entity: "Post",
    table: "posts",
    fields: {
      createdAt: "created_at",
      updatedAt: "updated_at",
    },
    parentFk: {
      field: "author_id",
      parentEntity: "User",
    },
    collections: {
      comments: { type: "owned" },
      tags: { 
        type: "reference", 
        entity: "Tag",
        junction: {
          table: "post_tags",
          sourceKey: "post_id",
          targetKey: "tag_id",
        },
      },
    },
  })
  // Nested child (1:N owned)
  .register({
    entity: "Comment",
    table: "comments",
    fields: {
      createdAt: "created_at",
    },
    parentFk: {
      field: "post_id",
      parentEntity: "Post",
    },
  })
  // Independent entity (referenced via N:N)
  .register({
    entity: "Tag",
    table: "tags",
  });

// Usage in mapper
class UserToPersistenceMapper extends PrismaToPersistence<User> {
  protected readonly registry = schemaRegistry;

  protected async onCreate(user: User): Promise<void> {
    await this.context.user.create({
      data: this.registry.mapEntity("User", user),
    });
  }

  // onUpdate uses PrismaBatchExecutor by default — override only if needed
}
```

## API Reference

### EntitySchema Interface

```typescript theme={null}
interface EntitySchema {
  entity: string;                           // Domain entity name
  table: string;                            // Database table name
  fields?: Record<string, string>;          // Field name mappings
  parentFk?: {
    field: string;                          // FK column name
    parentEntity: string;                   // Parent entity name
  };
  collections?: Record<string, CollectionConfig>;
  primaryKey?: string;                      // DB PK column (defaults to "id"); value always from entity.id
}

interface CollectionConfig {
  type: "owned" | "reference";
  entity?: string;                          // Target entity (for reference)
  relationName?: string;                    // ORM relation field name when it differs from the domain property name
  junction?: {
    table: string;                          // Junction table name
    sourceKey: string;                      // FK to source entity
    targetKey: string;                      // FK to target entity
  };
}
```

### Registry Methods

| Method                                 | Returns                    | Description                                                                                      |
| -------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------ |
| `register(schema)`                     | `this`                     | Register an entity schema                                                                        |
| `registerAll(schemas)`                 | `this`                     | Register multiple schemas                                                                        |
| `has(entity)`                          | `boolean`                  | Check if entity is registered                                                                    |
| `getSchema(entity)`                    | `EntitySchema`             | Get schema (throws if not found)                                                                 |
| `tryGetSchema(entity)`                 | `EntitySchema \| null`     | Get schema or null                                                                               |
| `getTable(entity)`                     | `string`                   | Get table name                                                                                   |
| `getPrimaryKeyField(entity)`           | `string`                   | Get database PK column (defaults to `"id"`)                                                      |
| `buildWhereById(entity, id)`           | `object`                   | Build ORM where clause for a single ID                                                           |
| `buildWhereByIds(entity, ids)`         | `object`                   | Build ORM where clause for multiple IDs                                                          |
| `getFieldsMap(entity)`                 | `Record<string, string>`   | Get field mappings                                                                               |
| `mapFieldName(entity, field)`          | `string`                   | Map single field name                                                                            |
| `mapFields(entity, data)`              | `object`                   | Map partial fields                                                                               |
| `mapEntity(entity, domainEntity)`      | `object`                   | Map complete entity                                                                              |
| `getParentEntity(entity)`              | `string \| null`           | Get parent entity name                                                                           |
| `getParentFkField(entity)`             | `string \| null`           | Get FK field name                                                                                |
| `getParentFk(entity, parentId)`        | `object \| null`           | Get FK object                                                                                    |
| `getCollectionConfig(entity, field)`   | `CollectionConfig \| null` | Get collection config                                                                            |
| `getRelationFieldName(entity, field)`  | `string`                   | Resolve ORM relation field name (returns `relationName` if set, otherwise the domain field name) |
| `isReferenceCollection(entity, field)` | `boolean`                  | Check if N:N relation                                                                            |
| `isOwnedCollection(entity, field)`     | `boolean`                  | Check if 1:N relation                                                                            |
| `getRegisteredEntities()`              | `string[]`                 | Get all entity names                                                                             |
| `getAllSchemas()`                      | `EntitySchema[]`           | Get all schemas                                                                                  |
| `clear()`                              | `void`                     | Remove all registrations                                                                         |
