npm package: @simtlix/simfinity-js
Description: A powerful Node.js framework that automatically generates GraphQL schemas from your data models, bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
Author: Simtlix
Latest version: 2.5.0

# Simfinity.js

A powerful Node.js framework that automatically generates GraphQL schemas from your data models, bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.

## 📑 Table of Contents

- [Features](#-features)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Core Concepts](#-core-concepts)
  - [Connecting Models](#connecting-models)
  - [Creating Schemas](#creating-schemas)
  - [Global Configuration](#global-configuration)
- [Basic Usage](#-basic-usage)
  - [Automatic Query Generation](#automatic-query-generation)
  - [Automatic Mutation Generation](#automatic-mutation-generation)
  - [Filtering and Querying](#filtering-and-querying)
  - [Collection Field Filtering](#collection-field-filtering)
- [Relationships](#-relationships)
  - [Defining Relationships](#defining-relationships)
  - [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
  - [Adding Types Without Endpoints](#adding-types-without-endpoints)
  - [Embedded vs Referenced Relationships](#embedded-vs-referenced-relationships)
  - [Querying Relationships](#querying-relationships)
- [Validations](#-validations)
  - [Field-Level Validations](#field-level-validations)
  - [Type-Level Validations](#type-level-validations)
  - [Custom Validated Scalar Types](#custom-validated-scalar-types)
  - [Custom Error Classes](#custom-error-classes)
- [State Machines](#-state-machines)
- [Controllers & Lifecycle Hooks](#️-controllers--lifecycle-hooks)
  - [Hook Parameters](#hook-parameters)
- [Query Scope](#-query-scope)
  - [Overview](#overview)
  - [Defining Scope](#defining-scope)
  - [Scope for Find Operations](#scope-for-find-operations)
  - [Scope for Aggregate Operations](#scope-for-aggregate-operations)
  - [Scope for Get By ID Operations](#scope-for-get-by-id-operations)
  - [Scope Function Parameters](#scope-function-parameters)
- [Authorization](#-authorization)
  - [Quick Start](#quick-start-1)
  - [Permission Schema](#permission-schema)
  - [Rule Helpers](#rule-helpers)
  - [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
  - [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
  - [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
- [Middlewares](#-middlewares)
  - [Adding Middlewares](#adding-middlewares)
  - [Middleware Parameters](#middleware-parameters)
  - [Common Use Cases](#common-use-cases)
- [Advanced Features](#-advanced-features)
  - [Field Extensions](#field-extensions)
  - [Custom Mutations](#custom-mutations)
  - [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
  - [Programmatic Data Access](#programmatic-data-access)
- [Aggregation Queries](#-aggregation-queries)
- [Complete Example](#-complete-example)
- [Resources](#-resources)
- [License](#-license)
- [Contributing](#-contributing)

## ✨ Features

- **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
- **MongoDB Integration**: Seamless translation between GraphQL and MongoDB
- **Powerful Querying**: Any query that can be executed in MongoDB can be executed in GraphQL
- **Aggregation Queries**: Built-in support for GROUP BY queries with aggregation operations (SUM, COUNT, AVG, MIN, MAX)
- **Auto-Generated Resolvers**: Automatically generates resolve methods for relationship fields
- **Automatic Index Creation**: Automatically creates MongoDB indexes for all ObjectId fields, including nested embedded objects and relationship fields
- **Business Logic**: Implement business logic and domain validations declaratively
- **State Machines**: Built-in support for declarative state machine workflows
- **Lifecycle Hooks**: Controller methods for granular control over operations
- **Custom Validation**: Field-level and type-level custom validations
- **Relationship Management**: Support for embedded and referenced relationships
- **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support

## 📦 Installation

```bash
npm install mongoose graphql @simtlix/simfinity-js
```

**Prerequisites**: Simfinity.js requires `mongoose` and `graphql` as peer dependencies.

## 🚀 Quick Start

### 1. Basic Setup

```javascript
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const mongoose = require('mongoose');
const simfinity = require('@simtlix/simfinity-js');

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/bookstore', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const app = express();
```

### 2. Define Your GraphQL Type

```javascript
const { GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLID } = require('graphql');

const BookType = new GraphQLObjectType({
  name: 'Book',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLID) },
    title: { type: new GraphQLNonNull(GraphQLString) },
    author: { type: GraphQLString },
  }),
});
```

### 3. Connect to Simfinity

```javascript
// Connect the type to Simfinity
simfinity.connect(null, BookType, 'book', 'books');

// Create the GraphQL schema
const schema = simfinity.createSchema();
```

### 4. Setup GraphQL Endpoint

```javascript
app.use('/graphql', graphqlHTTP({
  schema,
  graphiql: true,
  formatError: simfinity.buildErrorFormatter((err) => {
    console.log(err);
  })
}));

app.listen(4000, () => {
  console.log('Server is running on port 4000');
});
```

### 5. Try It Out

Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:

**Create a book:**
```graphql
mutation {
  addBook(input: {
    title: "The Hitchhiker's Guide to the Galaxy"
    author: "Douglas Adams"
  }) {
    id
    title
    author
  }
}
```

**List all books:**
```graphql
query {
  books {
    id
    title
    author
  }
}
```

> For a full working application, see the [Series Sample Project](https://github.com/simtlix/series-sample) -- a complete TV series microservice with types, relationships, state machines, controllers, and authorization.

## 🔧 Core Concepts

### Connecting Models

The `simfinity.connect()` method links your GraphQL types to Simfinity's automatic schema generation:

```javascript
simfinity.connect(
  mongooseModel,           // Optional: Custom Mongoose model (null for auto-generation)
  graphQLType,            // Required: Your GraphQLObjectType
  singularEndpointName,   // Required: Singular name for mutations (e.g., 'book')
  pluralEndpointName,     // Required: Plural name for queries (e.g., 'books')
  controller,             // Optional: Controller with lifecycle hooks
  onModelCreated,         // Optional: Callback when Mongoose model is created
  stateMachine            // Optional: State machine configuration
);
```

### Creating Schemas

Generate your complete GraphQL schema with optional type filtering:

```javascript
const schema = simfinity.createSchema(
  includedQueryTypes,     // Optional: Array of types to include in queries
  includedMutationTypes,  // Optional: Array of types to include in mutations
  includedCustomMutations // Optional: Array of custom mutations to include
);
```

### Global Configuration

```javascript
// Prevent automatic MongoDB collection creation (useful for testing)
simfinity.preventCreatingCollection(true);
```

## 📋 Basic Usage

### Automatic Query Generation

Simfinity automatically generates queries for each connected type:

```javascript
// For a BookType, you get:
// - book(id: ID): Book          - Get single book by ID
// - books(...filters): [Book]   - Get filtered list of books
```

### Automatic Mutation Generation

Simfinity automatically generates mutations for each connected type:

```javascript
// For a BookType, you get:
// - addBook(input: BookInput): Book
// - updateBook(input: BookInputForUpdate): Book  
// - deleteBook(id: ID): Book
```

### Filtering and Querying

Query with powerful filtering options:

```graphql
query {
  books(
    title: { operator: LIKE, value: "Galaxy" }
    author: { operator: EQ, value: "Douglas Adams" }
    pagination: { page: 1, size: 10, count: true }
    sort: { terms: [{ field: "title", order: ASC }] }
  ) {
    id
    title
    author
  }
}
```

#### Available Operators

- `EQ` - Equal
- `NE` - Not equal
- `GT` - Greater than
- `LT` - Less than
- `GTE` - Greater than or equal
- `LTE` - Less than or equal
- `LIKE` - Pattern matching
- `IN` - In array
- `NIN` - Not in array
- `BTW` - Between two values

### Logical Filters (AND / OR)

By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the `AND` and `OR` query arguments.

#### Simple OR

Return books in either the Sci-Fi or Fantasy category:

```graphql
query {
  books(
    OR: [
      { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
      { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
    ]
  ) {
    id
    title
    category
  }
}
```

#### Flat Filters Combined with OR

Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:

```graphql
query {
  books(
    rating: { operator: GTE, value: 7.0 }
    OR: [
      { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
      { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
    ]
  ) {
    id
    title
  }
}
```

This translates to: `rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy")`.

#### Nested AND inside OR

```graphql
query {
  books(
    OR: [
      { AND: [
        { conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
        { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
      ]}
      { AND: [
        { conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
        { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
      ]}
    ]
  ) {
    id
    title
  }
}
```

This translates to: `(rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy")`.

#### Filtering on Relationships within AND/OR

Use the `path` parameter to filter on related entity fields:

```graphql
query {
  books(
    OR: [
      { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
      { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
    ]
  ) {
    id
    title
    author { name }
  }
}
```

#### Mixing Flat Filters with AND/OR

You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:

```graphql
query {
  books(
    rating: { operator: GTE, value: 7.0 }
    author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
    OR: [
      { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
      { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
    ]
  ) {
    id
    title
    rating
    category
    author { name country }
  }
}
```

This translates to: `rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy")`. The flat field filters (`rating`, `author`) use the existing syntax while the OR group uses the new `QLFilterGroup` syntax.

You can also combine flat filters with explicit AND groups for more complex logic:

```graphql
query {
  books(
    rating: { operator: GTE, value: 5.0 }
    AND: [
      {
        OR: [
          { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
          { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
        ]
      }
      {
        OR: [
          { conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
          { conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
        ]
      }
    ]
  ) {
    id
    title
  }
}
```

This translates to: `rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US")`.

#### Collection Filtering with AND/OR

AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same `AND` and `OR` arguments:

```graphql
query {
  series {
    seasons(
      OR: [
        { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
        { conditions: [{ field: "year", operator: EQ, value: 2021 }] }
      ]
    ) {
      number
      year
    }
  }
}
```

You can mix flat collection filters with AND/OR:

```graphql
query {
  series {
    seasons(
      number: { operator: GT, value: 1 }
      OR: [
        { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
        {
          AND: [
            { conditions: [{ field: "year", operator: GTE, value: 2022 }] }
            { conditions: [
                { field: "episodes", path: "name", operator: LIKE, value: "Final" }
            ] }
          ]
        }
      ]
    ) {
      number
      year
      episodes { name }
    }
  }
}
```

This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final"))`.

#### Filter Types Reference

| Type | Fields | Description |
|------|--------|-------------|
| `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
| `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |

- `field` identifies the entity field by name (e.g., `"title"`, `"author"`)
- `path` is required for object/relationship fields (e.g., `"name"`, `"country.name"`)
- Multiple `conditions` in the same group are combined with AND
- Maximum nesting depth: 5 levels

### Collection Field Filtering

Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.

#### Basic Collection Filtering

Filter collection fields using the same operators and format as main queries:

```graphql
query {
  series {
    seasons(number: { operator: EQ, value: 1 }) {
      number
      id
      year
    }
  }
}
```

#### Advanced Collection Filtering

You can use complex filtering with nested object properties:

```graphql
query {
  series {
    seasons(
      year: { operator: GTE, value: 2020 }
      episodes: {
        terms: [
          {
            path: "name",
            operator: LIKE,
            value: "Pilot"
          }
        ]
      }
    ) {
      number
      year
      episodes {
        name
        date
      }
    }
  }
}
```

#### Collection Filtering with Multiple Conditions

Combine multiple filter conditions for collection fields:

```graphql
query {
  series {
    seasons(
      number: { operator: GT, value: 1 }
      year: { operator: BTW, value: [2015, 2023] }
    ) {
      number
      year
      state
    }
  }
}
```

#### Nested Collection Filtering

Filter deeply nested collections using dot notation:

```graphql
query {
  series {
    seasons(
      episodes: {
        terms: [
          {
            path: "name",
            operator: LIKE,
            value: "Final"
          }
        ]
      }
    ) {
      number
      episodes {
        name
        date
      }
    }
  }
}
```

#### Collection Filtering with Array Operations

Use array operations for collection fields:

```graphql
query {
  series {
    seasons(
      categories: { operator: IN, value: ["Drama", "Crime"] }
    ) {
      number
      categories
    }
  }
}
```

**Note**: Collection field filtering uses the exact same format as main query filtering, ensuring consistency across your GraphQL API. All available operators (`EQ`, `NE`, `GT`, `LT`, `GTE`, `LTE`, `LIKE`, `IN`, `NIN`, `BTW`) work with collection fields.

## 🔗 Relationships

### Defining Relationships

Use the `extensions.relation` field to define relationships between types:

```javascript
const AuthorType = new GraphQLObjectType({
  name: 'Author',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLID) },
    name: { type: new GraphQLNonNull(GraphQLString) },
    books: {
      type: new GraphQLList(BookType),
      extensions: {
        relation: {
          connectionField: 'author',
          displayField: 'title'
        },
      },
      // resolve method automatically generated! 🎉
    },
  }),
});

const BookType = new GraphQLObjectType({
  name: 'Book',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLID) },
    title: { type: new GraphQLNonNull(GraphQLString) },
    author: {
      type: AuthorType,
      extensions: {
        relation: {
          displayField: 'name'
        },
      },
      // resolve method automatically generated! 🎉
    },
  }),
});
```

### Relationship Configuration

- `connectionField`: **(Required for collections)** The field storing the related object's ID - only needed for one-to-many relationships (GraphQLList). For single object relationships, the field name is automatically inferred from the GraphQL field name.
- `displayField`: **(Optional)** Field to use for display in UI components
- `embedded`: **(Optional)** Whether the relation is embedded (default: false)

### Auto-Generated Resolve Methods

🎉 **NEW**: Simfinity.js automatically generates resolve methods for relationship fields when types are connected, eliminating the need for manual resolver boilerplate.

#### Before (Manual Resolvers)

```javascript
const BookType = new GraphQLObjectType({
  name: 'Book',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLID) },
    title: { type: new GraphQLNonNull(GraphQLString) },
    author: {
      type: AuthorType,
      extensions: {
        relation: {
          displayField: 'name'
        },
      },
      // You had to manually write this
      resolve(parent) {
        return simfinity.getModel(AuthorType).findById(parent.author);
      }
    },
    comments: {
      type: new GraphQLList(CommentType),
      extensions: {
        relation: {
          connectionField: 'bookId',
          displayField: 'text'
        },
      },
      // You had to manually write this too
      resolve(parent) {
        return simfinity.getModel(CommentType).find({ bookId: parent.id });
      }
    }
  }),
});
```

#### After (Auto-Generated Resolvers)

```javascript
const BookType = new GraphQLObjectType({
  name: 'Book',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLID) },
    title: { type: new GraphQLNonNull(GraphQLString) },
    author: {
      type: AuthorType,
      extensions: {
        relation: {
          displayField: 'name'
        },
      },
      // resolve method automatically generated! 🎉
    },
    comments: {
      type: new GraphQLList(CommentType),
      extensions: {
        relation: {
          connectionField: 'bookId',
          displayField: 'text'
        },
      },
      // resolve method automatically generated! 🎉
    }
  }),
});
```

#### How It Works

- **Single Object Relationships**: Automatically generates `findById()` resolvers using the field name or `connectionField`
- **Collection Relationships**: Automatically generates `find()` resolvers using the `connectionField` to query related objects
- **Lazy Loading**: Models are looked up at runtime, so types can be connected in any order
- **Backwards Compatible**: Existing manual resolve methods are preserved and not overwritten
- **Type Safety**: Clear error messages if related types aren't properly connected

#### Connect Your Types

```javascript
// Connect all your types to Simfinity
simfinity.connect(null, AuthorType, 'author', 'authors');
simfinity.connect(null, BookType, 'book', 'books');
simfinity.connect(null, CommentType, 'comment', 'comments');

// Or use addNoEndpointType for types that don't need direct queries/mutations
simfinity.addNoEndpointType(AuthorType);
```

That's it! All relationship resolvers are automatically generated when you connect your types.

### Adding Types Without Endpoints

Use `addNoEndpointType()` for types that should be included in the GraphQL schema but don't need their own CRUD operations:

```javascript
simfinity.addNoEndpointType(TypeName);
```

**When to use `addNoEndpointType()` vs `connect()`:**

| Method | Use Case | Creates Endpoints | Use Example |
|--------|----------|-------------------|-------------|
| `connect()` | Types that need CRUD operations | ✅ Yes | User, Product, Order |
| `addNoEndpointType()` | Types only used in relationships | ❌ No | Address, Settings, Director |

#### Perfect Example: TV Series with Embedded Director

From the [series-sample](https://github.com/simtlix/series-sample) project:

```javascript
// Director type - Used only as embedded data, no direct API access needed
const directorType = new GraphQLObjectType({
  name: 'director',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: new GraphQLNonNull(GraphQLString) },
    country: { type: GraphQLString }
  })
});

// Add to schema WITHOUT creating endpoints
simfinity.addNoEndpointType(directorType);

// Serie type - Has its own endpoints and embeds director data
const serieType = new GraphQLObjectType({
  name: 'serie',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: new GraphQLNonNull(GraphQLString) },
    categories: { type: new GraphQLList(GraphQLString) },
    director: {
      type: new GraphQLNonNull(directorType),
      extensions: {
        relation: {
          embedded: true,  // Director data stored within serie document
          displayField: 'name'
        }
      }
    }
  })
});

// Create full CRUD endpoints for series
simfinity.connect(null, serieType, 'serie', 'series');
```

**Result:**
- ✅ `addserie`, `updateserie`, `deleteserie` mutations available
- ✅ `serie`, `series` queries available  
- ❌ No `adddirector`, `director`, `directors` endpoints (director is embedded)

**Usage:**
```graphql
mutation {
  addserie(input: {
    name: "Breaking Bad"
    categories: ["crime", "drama", "thriller"]
    director: { 
      name: "Vince Gilligan" 
      country: "United States" 
    }
  }) {
    id
    name
    director {
      name
      country
    }
  }
}
```

#### When to Use Each Approach

**Use `addNoEndpointType()` for:**
- Simple data objects with few fields
- Data that doesn't need CRUD operations  
- Objects that belong to a single parent (1:1 relationships)
- Configuration or settings objects
- **Examples**: Address, Director info, Product specifications

**Use `connect()` for:**
- Complex entities that need their own endpoints
- Data that needs CRUD operations
- Objects shared between multiple parents (many:many relationships)  
- Objects with business logic (controllers, state machines)
- **Examples**: User, Product, Order, Season, Episode

### Embedded vs Referenced Relationships

**Referenced Relationships** (default):
```javascript
// Stores author ID in the book document
author: {
  type: AuthorType,
  extensions: {
    relation: {
      // connectionField not needed for single object relationships
      embedded: false  // This is the default
    }
  }
}
```

**Embedded Relationships**:
```javascript
// Stores the full publisher object in the book document
publisher: {
  type: PublisherType,
  extensions: {
    relation: {
      embedded: true
    }
  }
}
```

### Querying Relationships

Query nested relationships with dot notation:

```graphql
query {
  books(author: {
    terms: [
      {
        path: "country.name",
        operator: EQ,
        value: "England"
      }
    ]
  }) {
    id
    title
    author {
      name
      country {
        name
      }
    }
  }
}
```

### Creating Objects with Relationships

**Link to existing objects:**
```graphql
mutation {
  addBook(input: {
    title: "New Book"
    author: {
      id: "existing_author_id"
    }
  }) {
    id
    title
    author {
      name
    }
  }
}
```

**Create embedded objects:**
```graphql
mutation {
  addBook(input: {
    title: "New Book"
    publisher: {
      name: "Penguin Books"
      location: "London"
    }
  }) {
    id
    title
    publisher {
      name
      location
    }
  }
}
```

### Collection Fields

Work with arrays of related objects:

```graphql
mutation {
  updateBook(input: {
    id: "book_id"
    reviews: {
      added: [
        { rating: 5, comment: "Amazing!" }
        { rating: 4, comment: "Good read" }
      ]
      updated: [
        { id: "review_id", rating: 3 }
      ]
      deleted: ["review_id_to_delete"]
    }
  }) {
    id
    title
    reviews {
      rating
      comment
    }
  }
}
```

## ✅ Validations

### Declarative Validation Helpers

Simfinity.js provides built-in validation helpers to simplify common validation patterns, eliminating verbose boilerplate code.

#### Using Validators

```javascript
const { validators } = require('@simtlix/simfinity-js');

const PersonType = new GraphQLObjectType({
  name: 'Person',
  fields: () => ({
    id: { type: GraphQLID },
    name: {
      type: GraphQLString,
      extensions: {
        validations: validators.stringLength('Name', 2, 100)
      }
    },
    email: {
      type: GraphQLString,
      extensions: {
        validations: validators.email()
      }
    },
    website: {
      type: GraphQLString,
      extensions: {
        validations: validators.url()
      }
    },
    age: {
      type: GraphQLInt,
      extensions: {
        validations: validators.numberRange('Age', 0, 120)
      }
    },
    price: {
      type: GraphQLFloat,
      extensions: {
        validations: validators.positive('Price')
      }
    }
  })
});
```

#### Available Validators

**String Validators:**
- `validators.stringLength(name, min, max)` - Validates string length with min/max bounds (required for CREATE)
- `validators.maxLength(name, max)` - Validates maximum string length
- `validators.pattern(name, regex, message)` - Validates against a regex pattern
- `validators.email()` - Validates email format
- `validators.url()` - Validates URL format

**Number Validators:**
- `validators.numberRange(name, min, max)` - Validates number range
- `validators.positive(name)` - Ensures number is positive

**Array Validators:**
- `validators.arrayLength(name, maxItems, itemValidator)` - Validates array length and optionally each item

**Date Validators:**
- `validators.dateFormat(name, format)` - Validates date format
- `validators.futureDate(name)` - Ensures date is in the future

#### Validator Features

- **Automatic Operation Handling**: Validators work for both `CREATE` (save) and `UPDATE` operations
- **Smart Validation**: For CREATE operations, values are required. For UPDATE operations, undefined/null values are allowed (field might not be updated)
- **Consistent Error Messages**: All validators throw `SimfinityError` with appropriate messages

#### Example: Multiple Validators

```javascript
const ProductType = new GraphQLObjectType({
  name: 'Product',
  fields: () => ({
    id: { type: GraphQLID },
    name: {
      type: GraphQLString,
      extensions: {
        validations: validators.stringLength('Product Name', 3, 200)
      }
    },
    sku: {
      type: GraphQLString,
      extensions: {
        validations: validators.pattern('SKU', /^[A-Z0-9-]+$/, 'SKU must be uppercase alphanumeric with hyphens')
      }
    },
    price: {
      type: GraphQLFloat,
      extensions: {
        validations: validators.positive('Price')
      }
    },
    tags: {
      type: new GraphQLList(GraphQLString),
      extensions: {
        validations: validators.arrayLength('Tags', 10)
      }
    }
  })
});
```

### Field-Level Validations (Manual)

For custom validation logic, you can still write manual validators:

```javascript
const { SimfinityError } = require('@simtlix/simfinity-js');

const validateAge = {
  validate: async (typeName, fieldName, value, session) => {
    if (value < 0 || value > 120) {
      throw new SimfinityError(`Invalid age: ${value}`, 'VALIDATION_ERROR', 400);
    }
  }
};

const PersonType = new GraphQLObjectType({
  name: 'Person',
  fields: () => ({
    id: { type: GraphQLID },
    name: { 
      type: GraphQLString,
      extensions: {
        validations: {
          save: [{
            validate: async (typeName, fieldName, value, session) => {
              if (!value || value.length < 2) {
                throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
              }
            }
          }],
          update: [{
            validate: async (typeName, fieldName, value, session) => {
              if (value && value.length < 2) {
                throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
              }
            }
          }]
        }
      }
    },
    age: {
      type: GraphQLInt,
      extensions: {
        validations: {
          save: [validateAge],
          update: [validateAge]
        }
      }
    }
  })
});
```

### Type-Level Validations

Validate objects as a whole:

```javascript
const orderValidator = {
  validate: async (typeName, args, modelArgs, session) => {
    // Cross-field validation
    if (modelArgs.deliveryDate < modelArgs.orderDate) {
      throw new SimfinityError('Delivery date cannot be before order date', 'VALIDATION_ERROR', 400);
    }
    
    // Business rule validation
    if (modelArgs.items.length === 0) {
      throw new SimfinityError('Order must contain at least one item', 'BUSINESS_ERROR', 400);
    }
  }
};

const OrderType = new GraphQLObjectType({
  name: 'Order',
  extensions: {
    validations: {
      save: [orderValidator],
      update: [orderValidator]
    }
  },
  fields: () => ({
    // ... fields
  })
});
```

### Custom Validated Scalar Types

Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`.

#### Pre-built Scalars

Simfinity.js provides ready-to-use validated scalars for common patterns:

```javascript
const { scalars } = require('@simtlix/simfinity-js');

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
    id: { type: GraphQLID },
    email: { type: scalars.EmailScalar },      // Type name: Email_String
    website: { type: scalars.URLScalar },       // Type name: URL_String
    age: { type: scalars.PositiveIntScalar },  // Type name: PositiveInt_Int
    price: { type: scalars.PositiveFloatScalar } // Type name: PositiveFloat_Float
  }),
});
```

**Available Pre-built Scalars:**
- `scalars.EmailScalar` - Validates email format (`Email_String`)
- `scalars.URLScalar` - Validates URL format (`URL_String`)
- `scalars.PositiveIntScalar` - Validates positive integers (`PositiveInt_Int`)
- `scalars.PositiveFloatScalar` - Validates positive floats (`PositiveFloat_Float`)

#### Factory Functions for Custom Scalars

Create custom validated scalars with parameters:

```javascript
const { scalars } = require('@simtlix/simfinity-js');

// Create a bounded string scalar (name length between 2-100 characters)
const NameScalar = scalars.createBoundedStringScalar('Name', 2, 100);

// Create a bounded integer scalar (age between 0-120)
const AgeScalar = scalars.createBoundedIntScalar('Age', 0, 120);

// Create a bounded float scalar (rating between 0-10)
const RatingScalar = scalars.createBoundedFloatScalar('Rating', 0, 10);

// Create a pattern-based string scalar (phone number format)
const PhoneScalar = scalars.createPatternStringScalar(
  'Phone',
  /^\+?[\d\s\-()]+$/,
  'Invalid phone number format'
);

// Use in your types
const PersonType = new GraphQLObjectType({
  name: 'Person',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: NameScalar },        // Type name: Name_String
    age: { type: AgeScalar },          // Type name: Age_Int
    rating: { type: RatingScalar },    // Type name: Rating_Float
    phone: { type: PhoneScalar }       // Type name: Phone_String
  }),
});
```

**Available Factory Functions:**
- `scalars.createBoundedStringScalar(name, min, max)` - String with length bounds
- `scalars.createBoundedIntScalar(name, min, max)` - Integer with range validation
- `scalars.createBoundedFloatScalar(name, min, max)` - Float with range validation
- `scalars.createPatternStringScalar(name, pattern, message)` - String with regex pattern validation

#### Creating Custom Scalars Manually

You can also create custom scalars using `createValidatedScalar` directly:

```javascript
const { GraphQLString, GraphQLInt } = require('graphql');
const { createValidatedScalar } = require('@simtlix/simfinity-js');

// Email scalar with validation (generates type name: Email_String)
const EmailScalar = createValidatedScalar(
  'Email',
  'A valid email address',
  GraphQLString,
  (value) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      throw new Error('Invalid email format');
    }
  }
);

// Positive integer scalar (generates type name: PositiveInt_Int)
const PositiveIntScalar = createValidatedScalar(
  'PositiveInt',
  'A positive integer',
  GraphQLInt,
  (value) => {
    if (value <= 0) {
      throw new Error('Value must be positive');
    }
  }
);

// Use in your types
const UserType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
    id: { type: GraphQLID },
    email: { type: EmailScalar }, // Type name: Email_String
    age: { type: PositiveIntScalar }, // Type name: PositiveInt_Int
  }),
});
```

### Custom Error Classes

Create domain-specific error classes:

```javascript
const { SimfinityError } = require('@simtlix/simfinity-js');

// Business logic error
class BusinessError extends SimfinityError {
  constructor(message) {
    super(message, 'BUSINESS_ERROR', 400);
  }
}

// Authorization error
class AuthorizationError extends SimfinityError {
  constructor(message) {
    super(message, 'UNAUTHORIZED', 401);
  }
}

// Not found error
class NotFoundError extends SimfinityError {
  constructor(message) {
    super(message, 'NOT_FOUND', 404);
  }
}
```

## 🔄 State Machines

Implement declarative state machine workflows:

### 1. Define States

```javascript
const { GraphQLEnumType } = require('graphql');

const OrderState = new GraphQLEnumType({
  name: 'OrderState',
  values: {
    PENDING: { value: 'PENDING' },
    PROCESSING: { value: 'PROCESSING' },
    SHIPPED: { value: 'SHIPPED' },
    DELIVERED: { value: 'DELIVERED' },
    CANCELLED: { value: 'CANCELLED' }
  }
});
```

### 2. Define Type with State Field

```javascript
const OrderType = new GraphQLObjectType({
  name: 'Order',
  fields: () => ({
    id: { type: GraphQLID },
    customer: { type: GraphQLString },
    state: { type: OrderState }
  })
});
```

### 3. Configure State Machine

```javascript
const stateMachine = {
  initialState: { name: 'PENDING', value: 'PENDING' },
  actions: {
    process: {
      from: { name: 'PENDING', value: 'PENDING' },
      to: { name: 'PROCESSING', value: 'PROCESSING' },
      description: 'Process the order',
      action: async (args, session) => {
        // Business logic for processing
        console.log(`Processing order ${args.id}`);
        // You can perform additional operations here
      }
    },
    ship: {
      from: { name: 'PROCESSING', value: 'PROCESSING' },
      to: { name: 'SHIPPED', value: 'SHIPPED' },
      description: 'Ship the order',
      action: async (args, session) => {
        // Business logic for shipping
        console.log(`Shipping order ${args.id}`);
      }
    },
    deliver: {
      from: { name: 'SHIPPED', value: 'SHIPPED' },
      to: { name: 'DELIVERED', value: 'DELIVERED' },
      description: 'Mark as delivered'
    },
    cancel: {
      from: { name: 'PENDING', value: 'PENDING' },
      to: { name: 'CANCELLED', value: 'CANCELLED' },
      description: 'Cancel the order'
    }
  }
};
```

### 4. Connect with State Machine

```javascript
simfinity.connect(null, OrderType, 'order', 'orders', null, null, stateMachine);
```

### 5. Use State Machine Mutations

The state machine automatically generates mutations for each action:

```graphql
mutation {
  process_order(input: {
    id: "order_id"
  }) {
    id
    state
    customer
  }
}
```

**Important Notes**:
- The `state` field is automatically read-only and managed by the state machine
- State transitions are only allowed based on the defined actions
- Business logic in the `action` function is executed during transitions
- Invalid transitions throw errors automatically

## 🎛️ Controllers & Lifecycle Hooks

Controllers provide fine-grained control over operations with lifecycle hooks:

```javascript
const bookController = {
  onSaving: async (doc, args, session, context) => {
    // Before saving - doc is a Mongoose document
    if (!doc.title || doc.title.trim().length === 0) {
      throw new Error('Book title cannot be empty');
    }
    // Access user from context to set owner
    if (context && context.user) {
      doc.owner = context.user.id;
    }
    console.log(`Creating book: ${doc.title}`);
  },

  onSaved: async (doc, args, session, context) => {
    // After saving - doc is a plain object
    console.log(`Book saved: ${doc._id}`);
    // Can access context.user for post-save operations like notifications
  },

  onUpdating: async (id, doc, session, context) => {
    // Before updating - doc contains only changed fields
    // Validate user has permission to update
    if (context && context.user && context.user.role !== 'admin') {
      throw new simfinity.SimfinityError('Only admins can update books', 'FORBIDDEN', 403);
    }
    console.log(`Updating book ${id}`);
  },

  onUpdated: async (doc, session, context) => {
    // After updating - doc is the updated document
    console.log(`Book updated: ${doc.title}`);
  },

  onDelete: async (doc, session, context) => {
    // Before deleting - doc is the document to be deleted
    // Validate user has permission to delete
    if (context && context.user && context.user.role !== 'admin') {
      throw new simfinity.SimfinityError('Only admins can delete books', 'FORBIDDEN', 403);
    }
    console.log(`Deleting book: ${doc.title}`);
  }
};

// Connect with controller
simfinity.connect(null, BookType, 'book', 'books', bookController);
```

### Hook Parameters

**`onSaving(doc, args, session, context)`**:
- `doc`: Mongoose Document instance (not yet saved)
- `args`: Raw GraphQL mutation input
- `session`: Mongoose session for transaction
- `context`: GraphQL context object (includes request info, user data, etc.)

**`onSaved(doc, args, session, context)`**:
- `doc`: Plain object of saved document
- `args`: Raw GraphQL mutation input
- `session`: Mongoose session for transaction
- `context`: GraphQL context object (includes request info, user data, etc.)

**`onUpdating(id, doc, session, context)`**:
- `id`: Document ID being updated
- `doc`: Plain object with only changed fields
- `session`: Mongoose session for transaction
- `context`: GraphQL context object (includes request info, user data, etc.)

**`onUpdated(doc, session, context)`**:
- `doc`: Full updated Mongoose document
- `session`: Mongoose session for transaction
- `context`: GraphQL context object (includes request info, user data, etc.)

**`onDelete(doc, session, context)`**:
- `doc`: Plain object of document to be deleted
- `session`: Mongoose session for transaction
- `context`: GraphQL context object (includes request info, user data, etc.)

### Using Context in Controllers

The `context` parameter provides access to the GraphQL request context, which typically includes user information, request metadata, and other application-specific data. This is particularly useful for:

- **Setting ownership**: Automatically assign the current user as the owner of new entities
- **Authorization checks**: Validate user permissions before allowing operations
- **Audit logging**: Track who performed which operations
- **User-specific business logic**: Apply different logic based on user roles or attributes

**Example: Setting Owner on Creation**

```javascript
const documentController = {
  onSaving: async (doc, args, session, context) => {
    // Automatically set the owner to the current user
    if (context && context.user) {
      doc.owner = context.user.id;
    }
  }
};
```

**Example: Role-Based Authorization**

```javascript
const adminOnlyController = {
  onUpdating: async (id, doc, session, context) => {
    if (!context || !context.user || context.user.role !== 'admin') {
      throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
    }
  },
  
  onDelete: async (doc, session, context) => {
    if (!context || !context.user || context.user.role !== 'admin') {
      throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
    }
  }
};
```

**Note**: When using `saveObject` programmatically (outside of GraphQL), the `context` parameter is optional and may be `undefined`. Always check for context existence before accessing its properties.

## 🔒 Query Scope

### Overview

Query scope allows you to automatically modify query arguments based on context (e.g., user permissions). This enables automatic filtering so that users can only see documents they're authorized to access. Scope functions are executed after middleware and before query execution, allowing you to append filter conditions to queries and aggregations.

### Defining Scope

Define scope in the type extensions, similar to how validations are defined:

```javascript
const EpisodeType = new GraphQLObjectType({
  name: 'episode',
  extensions: {
    validations: {
      create: [validateEpisodeFields],
      update: [validateEpisodeBusinessRules]
    },
    scope: {
      find: async ({ type, args, operation, context }) => {
        // Modify args in place to add filter conditions
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      },
      aggregate: async ({ type, args, operation, context }) => {
        // Apply same scope to aggregate queries
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      },
      get_by_id: async ({ type, args, operation, context }) => {
        // For get_by_id, scope is automatically merged with id filter
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      }
    }
  },
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    owner: {
      type: new GraphQLNonNull(simfinity.getType('user')),
      extensions: {
        relation: {
          connectionField: 'owner',
          displayField: 'name'
        }
      }
    }
  })
});
```

### Scope for Find Operations

Scope functions for `find` operations modify the query arguments that are passed to `buildQuery`. The modified arguments are automatically used to filter results:

```javascript
const DocumentType = new GraphQLObjectType({
  name: 'Document',
  extensions: {
    scope: {
      find: async ({ type, args, operation, context }) => {
        // Only show documents owned by the current user
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      }
    }
  },
  fields: () => ({
    id: { type: GraphQLID },
    title: { type: GraphQLString },
    owner: {
      type: new GraphQLNonNull(simfinity.getType('user')),
      extensions: {
        relation: {
          connectionField: 'owner',
          displayField: 'name'
        }
      }
    }
  })
});
```

**Result**: All `documents` queries will automatically filter to only return documents where `owner.id` equals `context.user.id`.

### Scope for Aggregate Operations

Scope functions for `aggregate` operations work the same way, ensuring aggregation queries also respect the scope:

```javascript
const OrderType = new GraphQLObjectType({
  name: 'Order',
  extensions: {
    scope: {
      aggregate: async ({ type, args, operation, context }) => {
        // Only aggregate orders for the current user's organization
        args.organization = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.organizationId
            }
          ]
        };
      }
    }
  },
  fields: () => ({
    // ... fields
  })
});
```

**Result**: All `orders_aggregate` queries will automatically filter to only aggregate orders from the user's organization.

### Scope for Get By ID Operations

For `get_by_id` operations, scope functions modify a temporary query arguments object that includes the id filter. The system automatically combines the id filter with scope filters:

```javascript
const PrivateDocumentType = new GraphQLObjectType({
  name: 'PrivateDocument',
  extensions: {
    scope: {
      get_by_id: async ({ type, args, operation, context }) => {
        // Ensure user can only access their own documents
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      }
    }
  },
  fields: () => ({
    // ... fields
  })
});
```

**Result**: When querying `privatedocument(id: "some_id")`, the system will:
1. Create a query that includes both the id filter and the owner scope filter
2. Only return the document if it matches both conditions
3. Return `null` if the document exists but doesn't match the scope

### Scope Function Parameters

Scope functions receive the same parameters as middleware for consistency:

```javascript
{
  type,        // Type information (model, gqltype, controller, etc.)
  args,        // GraphQL arguments passed to the operation (modify this object)
  operation,   // Operation type: 'find', 'aggregate', or 'get_by_id'
  context      // GraphQL context object (includes request info, user data, etc.)
}
```

### Filter Structure

When modifying `args` in scope functions, use the appropriate filter structure:

**For scalar fields:**
```javascript
args.fieldName = {
  operator: 'EQ',
  value: 'someValue'
};
```

**For object/relation fields (QLTypeFilterExpression):**
```javascript
args.relationField = {
  terms: [
    {
      path: 'fieldName',
      operator: 'EQ',
      value: 'someValue'
    }
  ]
};
```

### Complete Example

Here's a complete example showing scope for all query operations:

```javascript
const EpisodeType = new GraphQLObjectType({
  name: 'episode',
  extensions: {
    validations: {
      save: [validateEpisodeFields],
      update: [validateEpisodeBusinessRules]
    },
    scope: {
      find: async ({ type, args, operation, context }) => {
        // Only show episodes from seasons the user has access to
        args.season = {
          terms: [
            {
              path: 'owner.id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      },
      aggregate: async ({ type, args, operation, context }) => {
        // Apply same scope to aggregations
        args.season = {
          terms: [
            {
              path: 'owner.id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      },
      get_by_id: async ({ type, args, operation, context }) => {
        // Ensure user can only access their own episodes
        args.owner = {
          terms: [
            {
              path: 'id',
              operator: 'EQ',
              value: context.user.id
            }
          ]
        };
      }
    }
  },
  fields: () => ({
    id: { type: GraphQLID },
    number: { type: GraphQLInt },
    name: { type: GraphQLString },
    season: {
      type: new GraphQLNonNull(simfinity.getType('season')),
      extensions: {
        relation: {
          connectionField: 'season',
          displayField: 'number'
        }
      }
    },
    owner: {
      type: new GraphQLNonNull(simfinity.getType('user')),
      extensions: {
        relation: {
          connectionField: 'owner',
          displayField: 'name'
        }
      }
    }
  })
});
```

### Important Notes

- **Execution Order**: Scope functions are executed **after** middleware, so middleware can set up context (e.g., user info) that scope functions can use
- **Modify Args In Place**: Scope functions should modify the `args` object directly
- **Filter Structure**: Use the correct filter structure (`QLFilter` for scalars, `QLTypeFilterExpression` for relations)
- **All Query Operations**: Scope applies to `find`, `aggregate`, and `get_by_id` operations
- **Automatic Merging**: For `get_by_id`, the id filter is automatically combined with scope filters
- **Context Access**: Use `context.user`, `context.ip`, or other context properties to determine scope

### Use Cases

- **Multi-tenancy**: Filter documents by organization or tenant
- **User-specific data**: Only show documents owned by the current user
- **Role-based access**: Filter based on user roles or permissions
- **Department/Team scoping**: Show only data relevant to user's department
- **Geographic scoping**: Filter by user's location or region

## 🔐 Authorization

Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.

### Quick Start

```javascript
const { auth } = require('@simtlix/simfinity-js');
const { createYoga } = require('graphql-yoga');

const { createAuthPlugin, requireAuth, requireRole } = auth;

// Define your permission schema
// Query/Mutation names match the ones generated by simfinity.connect()
const permissions = {
  Query: {
    series: requireAuth(),
    seasons: requireAuth(),
  },
  Mutation: {
    deleteserie: requireRole('admin'),
    deletestar: requireRole('admin'),
  },
  serie: {
    '*': requireAuth(),           // Wildcard: all fields require auth
  },
};

// Create the Envelop auth plugin and pass it to your server
const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'ALLOW' });
const yoga = createYoga({ schema, plugins: [authPlugin] });
```

### Permission Schema

The permission schema defines authorization rules per type and field:

```javascript
const permissions = {
  // Operation types (Query, Mutation, Subscription)
  Query: {
    fieldName: ruleOrRules,
  },
  
  // Object types
  TypeName: {
    '*': wildcardRule,      // Applies to all fields unless overridden
    fieldName: specificRule, // Overrides wildcard for this field
  },
};
```

**Resolution Order:**
1. Check exact field rule: `permissions[TypeName][fieldName]`
2. Fallback to wildcard: `permissions[TypeName]['*']`
3. Apply default policy (ALLOW or DENY)

**Rule Types:**
- **Function**: `(parent, args, ctx, info) => boolean | void | Promise<boolean | void>`
- **Array of functions**: All rules must pass (AND logic)
- **Policy expression**: JSON AST object (see below)

**Rule Semantics:**
- `return true` or `return void` → allow
- `return false` → deny
- `throw Error` → deny with error

### Rule Helpers

Simfinity.js provides reusable rule builders:

```javascript
const { auth } = require('@simtlix/simfinity-js');

const {
  resolvePath,       // Utility to resolve dotted paths in objects
  requireAuth,       // Requires ctx.user to exist
  requireRole,       // Requires specific role(s)
  requirePermission, // Requires specific permission(s)
  composeRules,      // Combine rules (AND logic)
  anyRule,           // Combine rules (OR logic)
  isOwner,           // Check resource ownership
  allow,             // Always allow
  deny,              // Always deny
  createRule,        // Create custom rule
} = auth;
```

#### requireAuth(userPath?)

Requires the user to be authenticated. Supports custom user paths in context:

```javascript
const permissions = {
  Query: {
    // Default: checks ctx.user
    me: requireAuth(),
    
    // Custom path: checks ctx.auth.currentUser
    profile: requireAuth('auth.currentUser'),
    
    // Deep path: checks ctx.session.data.user
    settings: requireAuth('session.data.user'),
  },
};
```

#### requireRole(role, options?)

Requires the user to have a specific role. Supports custom paths:

```javascript
const permissions = {
  Query: {
    // Default: checks ctx.user.role
    adminDashboard: requireRole('ADMIN'),
    modTools: requireRole(['ADMIN', 'MODERATOR']), // Any of these roles
    
    // Custom paths: checks ctx.auth.user.profile.role
    superAdmin: requireRole('SUPER_ADMIN', { 
      userPath: 'auth.user', 
      rolePath: 'profile.role',
    }),
  },
};
```

#### requirePermission(permission, options?)

Requires the user to have specific permission(s). Supports custom paths:

```javascript
const permissions = {
  Mutation: {
    // Default: checks ctx.user.permissions
    deletePost: requirePermission('posts:delete'),
    manageUsers: requirePermission(['users:read', 'users:write']), // All required
    
    // Custom paths: checks ctx.session.user.access.grants
    admin: requirePermission('admin:all', {
      userPath: 'session.user',
      permissionsPath: 'access.grants',
    }),
  },
};
```

#### composeRules(...rules)

Combines multiple rules with AND logic (all must pass):

```javascript
const permissions = {
  Mutation: {
    updatePost: composeRules(
      requireAuth(),
      requireRole('EDITOR'),
      async (post, args, ctx) => post.authorId === ctx.user.id,
    ),
  },
};
```

#### anyRule(...rules)

Combines multiple rules with OR logic (any must pass):

```javascript
const permissions = {
  Post: {
    content: anyRule(
      requireRole('ADMIN'),
      async (post, args, ctx) => post.authorId === ctx.user.id,
    ),
  },
};
```

#### isOwner(ownerField, userIdField)

Checks if the authenticated user owns the resource:

```javascript
const permissions = {
  Post: {
    '*': composeRules(
      requireAuth(),
      isOwner('authorId', 'id'), // Compares post.authorId with ctx.user.id
    ),
  },
};
```

### Policy Expressions (JSON AST)

For declarative rules, use JSON AST policy expressions:

```javascript
const permissions = {
  Post: {
    content: {
      anyOf: [
        { eq: [{ ref: 'parent.published' }, true] },
        { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] },
      ],
    },
  },
};
```

**Supported Operators:**

| Operator | Description | Example |
|----------|-------------|---------|
| `eq` | Equals | `{ eq: [{ ref: 'parent.status' }, 'active'] }` |
| `in` | Value in array | `{ in: [{ ref: 'ctx.user.role' }, ['ADMIN', 'MOD']] }` |
| `allOf` | All must be true (AND) | `{ allOf: [expr1, expr2] }` |
| `anyOf` | Any must be true (OR) | `{ anyOf: [expr1, expr2] }` |
| `not` | Negation | `{ not: { eq: [{ ref: 'parent.deleted' }, true] } }` |

**References:**

Use `{ ref: 'path' }` to reference values:
- `parent.*` - Parent resolver result (the object being resolved)
- `args.*` - GraphQL arguments
- `ctx.*` - GraphQL context

**Security:**
- Only `parent`, `args`, and `ctx` roots are allowed
- Unknown operators fail closed (deny)
- No `eval()` or `Function()` - pure object traversal

### Integration with GraphQL Yoga / Envelop

The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.

```javascript
const { createYoga } = require('graphql-yoga');
const { createServer } = require('http');
const simfinity = require('@simtlix/simfinity-js');

const { auth } = simfinity;
const { createAuthPlugin, requireAuth, requireRole, composeRules, isOwner, deny } = auth;

// Define your types and connect them
simfinity.connect(null, SerieType, 'serie', 'series');
simfinity.connect(null, SeasonType, 'season', 'seasons');
simfinity.connect(null, StarType, 'star', 'stars');

// Create base schema
const schema = simfinity.createSchema();

// Define permissions
// Query/Mutation names match the ones generated by simfinity.connect()
const permissions = {
  Query: {
    series: requireAuth(),
    seasons: requireAuth(),
    stars: requireAuth(),
  },
  Mutation: {
    addserie: requireAuth(),
    updateserie: composeRules(requireAuth(), isOwner('createdBy')),
    deleteserie: requireRole('admin'),
    deletestar: requireRole('admin'),
  },
  serie: {
    '*': requireAuth(),
  },
  season: {
    '*': requireAuth(),
  },
};

// Create auth plugin
const authPlugin = createAuthPlugin(permissions, {
  defaultPolicy: 'ALLOW',
  debug: false,
});

// Setup Yoga with the auth plugin
const yoga = createYoga({
  schema,
  plugins: [authPlugin],
  context: (req) => ({
    user: req.user,  // Set by your authentication layer
  }),
});

const server = createServer(yoga);
server.listen(4000);
```

### Legacy: Integration with graphql-middleware

> **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
> which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
> Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.

```javascript
const { applyMiddleware } = require('graphql-middleware');
const simfinity = require('@simtlix/simfinity-js');

const { auth } = simfinity;
const { createAuthMiddleware, requireAuth, requireRole } = auth;

const baseSchema = simfinity.createSchema();

const authMiddleware = createAuthMiddleware(permissions, {
  defaultPolicy: 'DENY',
});

const schema = applyMiddleware(baseSchema, authMiddleware);
```

### Plugin / Middleware Options

```javascript
const plugin = createAuthPlugin(permissions, {
  defaultPolicy: 'DENY',  // 'ALLOW' or 'DENY' (default: 'DENY')
  debug: false,           // Enable debug logging
});
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `defaultPolicy` | `'ALLOW' \| 'DENY'` | `'DENY'` | Policy when no rule matches |
| `debug` | `boolean` | `false` | Log authorization decisions |

### Error Handling

The auth middleware uses Simfinity error classes:

```javascript
const { auth } = require('@simtlix/simfinity-js');

const { UnauthenticatedError, ForbiddenError } = auth;

// UnauthenticatedError: code 'UNAUTHENTICATED', status 401
// ForbiddenError: code 'FORBIDDEN', status 403
```

Custom error handling in rules:

```javascript
const permissions = {
  Mutation: {
    deleteserie: async (parent, args, ctx) => {
      if (!ctx.user) {
        throw new auth.UnauthenticatedError('Please log in');
      }
      if (ctx.user.role !== 'admin') {
        throw new auth.ForbiddenError('Only admins can delete series');
      }
      return true;
    },
  },
};
```

### Best Practices

1. **Default to DENY**: Use `defaultPolicy: 'DENY'` for security
2. **Use wildcards wisely**: `'*'` rules provide baseline security per type
3. **Prefer helper rules**: Use `requireAuth()`, `requireRole()` over custom functions
4. **Fail closed**: Custom rules should deny on unexpected conditions
5. **Keep rules simple**: Complex logic belongs in controllers, not auth rules
6. **Test thoroughly**: Auth rules are critical - test all scenarios

## 🔧 Middlewares

Middlewares provide a powerful way to intercept and process all GraphQL operations before they execute. Use them for cross-cutting concerns like authentication, logging, validation, and performance monitoring.

### Adding Middlewares

Register middlewares using `simfinity.use()`. Middlewares execute in the order they're registered:

```javascript
// Basic logging middleware
simfinity.use((params, next) => {
  console.log(`Executing ${params.operation} on ${params.type?.name || 'custom mutation'}`);
  next();
});
```

### Middleware Parameters

Each middleware receives a `params` object containing:

```javascript
simfinity.use((params, next) => {
  // params object contains:
  const {
    type,        // Type information (model, gqltype, controller, etc.)
    args,        // GraphQL arguments passed to the operation
    operation,   // Operation type: 'save', 'update', 'delete', 'get_by_id', 'find', 'state_changed', 'custom_mutation'
    context,     // GraphQL context object (includes request info, user data, etc.)
    actionName,  // For state machine actions (only present for state_changed operations)
    actionField, // State machine action details (only present for state_changed operations)
    entry        // Custom mutation name (only present for custom_mutation operations)
  } = params;
  
  // Always call next() to continue the middleware chain
  next();
});
```

### Common Use Cases

#### 1. Authentication & Authorization

```javascript
simfinity.use((params, next) => {
  const { context, operation, type } = params;
  
  // Skip authentication for read operations
  if (operation === 'get_by_id' || operation === 'find') {
    return next();
  }
  
  // Check if user is authenticated
  if (!context.user) {
    throw new simfinity.SimfinityError('Authentication required', 'UNAUTHORIZED', 401);
  }
  
  // Check permissions for specific types
  if (type?.name === 'User' && context.user.role !== 'admin') {
    throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
  }
  
  next();
});
```

#### 2. Request Logging & Monitoring

```javascript
simfinity.use((params, next) => {
  const { operation, type, args, context } = params;
  const startTime = Date.now();
  
  console.log(`[${new Date().toISOString()}] Starting ${operation}${type ? ` on ${type.name}` : ''}`);
  
  // Continue with the operation
  next();
  
  const duration = Date.now() - startTime;
  console.log(`[${new Date().toISOString()}] Completed ${operation} in ${duration}ms`);
});
```

#### 3. Input Validation & Sanitization

```javascript
simfinity.use((params, next) => {
  const { operation, args, type } = params;
  
  // Validate input for save operations
  if (operation === 'save' && args.input) {
    // Trim string fields
    Object.keys(args.input).forEach(key => {
      if (typeof args.input[key] === 'string') {
        args.input[key] = args.input[key].trim();
      }
    });
    
    // Validate required business rules
    if (type?.name === 'Book' && args.input.title && args.input.title.length < 3) {
      throw new simfinity.SimfinityError('Book title must be at least 3 characters', 'VALIDATION_ERROR', 400);
    }
  }
  
  next();
});
```

#### 4. Rate Limiting

```javascript
const requestCounts = new Map();

simfinity.use((params, next) => {
  const { context, operation } = params;
  const userId = context.user?.id || context.ip;
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = 100;
  
  // Only apply rate limiting to mutations
  if (operation === 'save' || operation === 'update' || operation === 'delete') {
    const userRequests = requestCounts.get(userId) || [];
    const recentRequests = userRequests.filter(time => now - time < windowMs);
    
    if (recentRequests.length >= maxRequests) {
      throw new simfinity.SimfinityError('Rate limit exceeded', 'TOO_MANY_REQUESTS', 429);
    }
    
    recentRequests.push(now);
    requestCounts.set(userId, recentRequests);
  }
  
  next();
});
```

#### 5. Audit Trail

```javascript
simfinity.use((params, next) => {
  const { operation, type, args, context } = params;
  
  // Log all mutations for audit purposes
  if (operation === 'save' || operation === 'update' || operation === 'delete') {
    const auditEntry = {
      timestamp: new Date(),
      user: context.user?.id,
      operation,
      type: type?.name,
      entityId: args.id || 'new',
      data: operation === 'delete' ? null : args.input,
      ip: context.ip,
      userAgent: context.userAgent
    };
    
    // Save to audit log (could be database, file, or external service)
    console.log('AUDIT:', JSON.stringify(auditEntry));
  }
  
  next();
});
```

### Multiple Middlewares

Middlewares execute in registration order. Each middleware must call `next()` to continue the chain:

```javascript
// Middleware 1: Authentication
simfinity.use((params, next) => {
  console.log('1. Checking authentication...');
  // Authentication logic here
  next(); // Continue to next middleware
});

// Middleware 2: Authorization  
simfinity.use((params, next) => {
  console.log('2. Checking permissions...');
  // Authorization logic here
  next(); // Continue to next middleware
});

// Middleware 3: Logging
simfinity.use((params, next) => {
  console.log('3. Logging request...');
  // Logging logic here
  next(); // Continue to GraphQL operation
});
```

### Error Handling in Middlewares

Middlewares can throw errors to stop the operation:

```javascript
simfinity.use((params, next) => {
  const { context, operation } = params;
  
  try {
    // Validation logic
    if (!context.user && operation