@simtlix/simfinity-js 2.6.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,27 @@
1
+ ![Simfinity.js — Define once. Build beyond. GraphQL, MongoDB, PostgreSQL, and optional MCP.](.github/assets/readme-cover.png)
2
+
1
3
  # Simfinity.js
2
4
 
3
- 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.
5
+ A Node.js framework that turns GraphQL object types into generated queries, mutations, relationships, and database storage. Use the established MongoDB/Mongoose facade or the PostgreSQL 15+ facade with real tables and foreign keys.
6
+
7
+ Read the [documentation website](https://simtlix.github.io/simfinity.js/): start with the [quick start](https://simtlix.github.io/simfinity.js/guide/getting-started.html), explore the [guides](https://simtlix.github.io/simfinity.js/guide/schema.html), or consult the [API reference](https://simtlix.github.io/simfinity.js/reference/api.html). The website source is in [`docs/`](docs/).
8
+
9
+ Run the documentation website locally with Node.js 22+:
10
+
11
+ ```sh
12
+ npm run docs:install
13
+ npm run docs:dev
14
+ ```
15
+
16
+ For builds and hosting, see the [website maintainer guide](docs/.vitepress/README.md). The website documents the current source; some older examples later in this README retain historical conventions.
17
+
18
+ > **Documentation for both databases:** The [public website](https://simtlix.github.io/simfinity.js/guide/databases.html) now covers MongoDB and PostgreSQL, including shared APIs, relationships, generated FKs, scopes, and MCP. Both adapters are available in **v3.2.0** on npm. Follow the quick starts for installation, or download the runnable starters and verified release archives.
4
19
 
5
20
  ## 📑 Table of Contents
6
21
 
7
22
  - [Features](#-features)
8
23
  - [Installation](#-installation)
24
+ - [PostgreSQL support](#postgresql-support)
9
25
  - [Quick Start](#-quick-start)
10
26
  - [Core Concepts](#-core-concepts)
11
27
  - [Connecting Models](#connecting-models)
@@ -53,6 +69,18 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
53
69
  - [Custom Mutations](#custom-mutations)
54
70
  - [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
55
71
  - [Programmatic Data Access](#programmatic-data-access)
72
+ - [MCP Generation](#-mcp-generation)
73
+ - [Generating tool definitions](#generating-tool-definitions)
74
+ - [Standalone MCP server (stdio)](#standalone-mcp-server-stdio)
75
+ - [HTTP MCP endpoint (alongside GraphQL)](#http-mcp-endpoint-alongside-graphql)
76
+ - [Options](#options)
77
+ - [Selecting and naming tools](#selecting-and-naming-tools)
78
+ - [Customizing tools](#customizing-tools)
79
+ - [Limits](#limits)
80
+ - [Tool middleware](#tool-middleware)
81
+ - [Remote execution](#remote-execution)
82
+ - [Tool results and errors](#tool-results-and-errors)
83
+ - [Authentication](#authentication)
56
84
  - [Aggregation Queries](#-aggregation-queries)
57
85
  - [Complete Example](#-complete-example)
58
86
  - [Resources](#-resources)
@@ -62,8 +90,8 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
62
90
  ## ✨ Features
63
91
 
64
92
  - **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
65
- - **MongoDB Integration**: Seamless translation between GraphQL and MongoDB
66
- - **Powerful Querying**: Any query that can be executed in MongoDB can be executed in GraphQL
93
+ - **MongoDB or PostgreSQL**: Choose the database facade once during application startup
94
+ - **Powerful Querying**: Typed filters, nested paths, pagination, sorting, and aggregations across the supported contract
67
95
  - **Aggregation Queries**: Built-in support for GROUP BY queries with aggregation operations (SUM, COUNT, AVG, MIN, MAX)
68
96
  - **Auto-Generated Resolvers**: Automatically generates resolve methods for relationship fields
69
97
  - **Automatic Index Creation**: Automatically creates MongoDB indexes for all ObjectId fields, including nested embedded objects and relationship fields
@@ -77,101 +105,86 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
77
105
  ## 📦 Installation
78
106
 
79
107
  ```bash
80
- npm install mongoose graphql @simtlix/simfinity-js
108
+ npm install mongoose@^8.16.2 graphql@^16.11.0 @simtlix/simfinity-js@3.2.0
81
109
  ```
82
110
 
83
111
  **Prerequisites**: Simfinity.js requires `mongoose` and `graphql` as peer dependencies.
84
112
 
85
- ## 🚀 Quick Start
86
-
87
- ### 1. Basic Setup
113
+ ## PostgreSQL support
88
114
 
89
- ```javascript
90
- const express = require('express');
91
- const { graphqlHTTP } = require('express-graphql');
92
- const mongoose = require('mongoose');
93
- const simfinity = require('@simtlix/simfinity-js');
115
+ Version 3.2.0 releases `@simtlix/simfinity-core`, `@simtlix/simfinity-mcp`, `@simtlix/simfinity-postgres`, and the root MongoDB facade in lockstep. Install the selected adapter from npm; shared dependencies resolve automatically. PostgreSQL runs the shared GraphQL query/mutation engine, including scopes, controllers, validators, state transitions and nested writes. It generates and validates tables, indexes, and **real foreign keys**, including inverse relations, explicit many-to-many linking entities, and references inside embedded objects. PostgreSQL installation does not pull Mongoose, MongoDB, or MCP dependencies.
94
116
 
95
- // Connect to MongoDB
96
- mongoose.connect('mongodb://localhost:27017/bookstore', {
97
- useNewUrlParser: true,
98
- useUnifiedTopology: true,
99
- });
117
+ Enum filters resolve member names first, then declared internal values by strict equality, on both backends. For example, with `ONE: { value: 'TWO' }` and `TWO: { value: 'two' }`, filter `"TWO"` selects member `TWO`. Numeric internal values require numbers, not numeric strings. This applies to scalar lists, embedded/reference leaves and state filters across EQ, NE, LT, LTE, GT, GTE, BTW, IN and NIN. LIKE accepts string fields only. PostgreSQL writes and state guards continue to use internal enum values.
100
118
 
101
- const app = express();
102
- ```
119
+ The shared runtime preserves the v3.1 contract: generated relationships run target middleware/scopes with protected identity and parent filters; nested mutations enforce child middleware and persisted ownership; standalone `saveObject()` wraps the complete workflow in a transaction. Accepted empty strings are preserved. Both facades expose `configureQueryLimits()` for bounded pagination.
103
120
 
104
- ### 2. Define Your GraphQL Type
121
+ Choose the backend at application setup. The existing package continues to use MongoDB; PostgreSQL uses `createPostgres({ pool, schema })`, the same `connect(null, Type, ...)`/`createSchema()` signatures, and an awaited `initializeDatabase()` before serving requests. See the canonical [PostgreSQL quick start](docs/guide/postgresql.md), detailed [storage reference](docs/postgresql.md), and [compatibility contract](docs/compatibility.md) before adopting this version.
105
122
 
106
- ```javascript
107
- const { GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLID } = require('graphql');
123
+ Both database facades expose the same `auth`, `validators`, `scalars`, and `plugins` helper objects. PostgreSQL keeps MCP optional; install the database-independent integration and its transport SDK only when needed:
108
124
 
109
- const BookType = new GraphQLObjectType({
110
- name: 'Book',
111
- fields: () => ({
112
- id: { type: new GraphQLNonNull(GraphQLID) },
113
- title: { type: new GraphQLNonNull(GraphQLString) },
114
- author: { type: GraphQLString },
115
- }),
116
- });
125
+ ```sh
126
+ npm install @simtlix/simfinity-mcp@3.2.0 @modelcontextprotocol/sdk@^1.13.0
117
127
  ```
118
128
 
119
- ### 3. Connect to Simfinity
129
+ Import `generateMCPTools`, `createMCPServer`, or the transport helpers from `@simtlix/simfinity-mcp` and pass the schema returned by `createPostgres().createSchema()`.
120
130
 
121
- ```javascript
122
- // Connect the type to Simfinity
123
- simfinity.connect(null, BookType, 'book', 'books');
131
+ Compatibility tests run the same GraphQL schemas and query corpus against both databases. IDs use UUIDs on PostgreSQL, native model/session APIs differ, and some mappings remain explicitly unsupported, including whole embedded-object uniqueness and whole-object sorting/grouping. Nested embedded scalar-list filters/sorts, ragged array group keys and array aggregate facts now match the supported Mongo query contract. Native embedded writes retain explicit nulls, apply descendant list defaults and minimize empty inline objects; historical Date parameters use UTC. PostgreSQL enforces scalar/list multikey uniqueness within embedded trees through typed owner-key tables, validates JSONB embedded shapes, and checks owned marker/row consistency with deferred database triggers. MongoDB fixes also cover list nullability wrappers, scalar-ID inverse relations, inverse aggregation paths, embedded array replacement after clearing, and isolation of mutation input across transaction retries. Existing API signatures remain unchanged.
124
132
 
125
- // Create the GraphQL schema
126
- const schema = simfinity.createSchema();
133
+ ## 🚀 Quick Start
134
+
135
+ The [complete quick start](docs/guide/getting-started.md) includes MongoDB setup, installation, and working create, read, update, and delete operations. Simfinity uses ES modules and named exports.
136
+
137
+ ```sh
138
+ npm install @simtlix/simfinity-js graphql@^16.11.0 mongoose@^8.16.2 graphql-yoga@^5
127
139
  ```
128
140
 
129
- ### 4. Setup GraphQL Endpoint
141
+ Set `"type": "module"` in your application's `package.json`, then create `server.js`:
130
142
 
131
143
  ```javascript
132
- app.use('/graphql', graphqlHTTP({
133
- schema,
134
- graphiql: true,
135
- formatError: simfinity.buildErrorFormatter((err) => {
136
- console.log(err);
137
- })
138
- }));
144
+ import { createServer } from 'node:http';
145
+ import { GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
146
+ import { createYoga } from 'graphql-yoga';
147
+ import mongoose from 'mongoose';
148
+ import * as simfinity from '@simtlix/simfinity-js';
149
+
150
+ const SerieType = new GraphQLObjectType({
151
+ name: 'Serie',
152
+ fields: {
153
+ id: { type: GraphQLID },
154
+ name: { type: new GraphQLNonNull(GraphQLString) },
155
+ },
156
+ });
139
157
 
140
- app.listen(4000, () => {
141
- console.log('Server is running on port 4000');
158
+ await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/series?replicaSet=rs0&directConnection=true');
159
+ simfinity.connect(null, SerieType, 'serie', 'series');
160
+ const schema = simfinity.createSchema();
161
+ const yoga = createYoga({ schema });
162
+ createServer(yoga).listen(4000, '127.0.0.1', () => {
163
+ console.log('GraphQL ready at http://127.0.0.1:4000/graphql');
142
164
  });
143
165
  ```
144
166
 
145
- ### 5. Try It Out
167
+ Generated mutations require a MongoDB replica set or sharded cluster. Follow the [local MongoDB setup](docs/guide/getting-started.md#2-start-mongodb), then run `node server.js` and open `http://127.0.0.1:4000/graphql`:
146
168
 
147
- Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:
148
-
149
- **Create a book:**
150
169
  ```graphql
151
170
  mutation {
152
- addBook(input: {
153
- title: "The Hitchhiker's Guide to the Galaxy"
154
- author: "Douglas Adams"
155
- }) {
171
+ addserie(input: { name: "The Expanse" }) {
156
172
  id
157
- title
158
- author
173
+ name
159
174
  }
160
175
  }
161
176
  ```
162
177
 
163
- **List all books:**
164
178
  ```graphql
165
179
  query {
166
- books {
180
+ series {
167
181
  id
168
- title
169
- author
182
+ name
170
183
  }
171
184
  }
172
185
  ```
173
186
 
174
- > 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.
187
+ For a full working application, see the [Series Sample Project](https://github.com/simtlix/series-sample), with relationships, state machines, controllers, and authorization.
175
188
 
176
189
  ## 🔧 Core Concepts
177
190
 
@@ -203,6 +216,12 @@ const schema = simfinity.createSchema(
203
216
  );
204
217
  ```
205
218
 
219
+ Importing Simfinity initializes its global `__Field.extensions` introspection field safely whether GraphQL's fields have already been materialized or a schema already exists. Repeated module evaluation with the same GraphQL peer reuses the existing extension field and metadata types. Application field metadata is preserved.
220
+
221
+ The extension remains shared by every schema using that GraphQL peer; Simfinity's type and middleware registries belong to each runtime instance. Schemas constructed after import include `FieldExtensionsType` and `RelationType` in their type maps. Earlier schemas keep their original type maps: ordinary operations and direct metadata selections work, but named fragments on the new metadata types require a schema constructed after import.
222
+
223
+ Schema-cloning tools remain unsupported. In a process that has imported Simfinity, `buildClientSchema()` on a post-import schema's introspection result can also fail with duplicate metadata type names. Use Envelop plugins and in-place resolver wrapping; see the [introspection metadata reference](docs/reference/extensions.md).
224
+
206
225
  ### Global Configuration
207
226
 
208
227
  ```javascript
@@ -233,8 +252,25 @@ Simfinity automatically generates mutations for each connected type:
233
252
  // - deleteBook(id: ID): Book
234
253
  ```
235
254
 
255
+ Creation inputs retain required scalar, enum, embedded-object, and list fields. Update inputs remove the outer non-null wrapper so fields can be omitted; the entity `id` remains required for `GraphQLID` and `GraphQLID!`. Other ID fields remain optional on update. List item nullability is preserved:
256
+
257
+ | Output field | Create input | Update input |
258
+ | --- | --- | --- |
259
+ | `[String]` | `[String]` | `[String]` |
260
+ | `[String]!` | `[String]!` | `[String]` |
261
+ | `[String!]` | `[String!]` | `[String!]` |
262
+ | `[String!]!` | `[String!]!` | `[String!]` |
263
+
264
+ The same wrapper handling applies to enum lists, supported custom scalar lists, and embedded lists (using their nested input types). Referenced collections keep the `added` / `updated` / `deleted` input object: a required collection requires that object on create, and non-null object items make `added` and `updated` items non-null. Nullable collection-operation items are ignored. Use `deleted` to delete children.
265
+
266
+ Empty strings, `false`, `0`, and empty arrays are persisted after validators accept them. Omitting an update field leaves its value unchanged. Explicit `null` clears a nullable scalar, embedded field, or single-object reference; references clear the actual stored `connectionField`, or the GraphQL field name when no override is configured. Multiple nullable fields can be cleared in one update. An explicit `null` for an originally non-null field leaves its stored value unchanged; use an update validator to reject that input if needed.
267
+
236
268
  ### Filtering and Querying
237
269
 
270
+ Every relationship or embedded `terms` entry is combined with AND, including repeated paths such as `age GTE 18` and `age LTE 30`. These conditions remain ANDed with top-level logical groups and scope filters. Filter and list-sort paths must resolve to declared scalar or enum fields. `id` paths refer to the stored `_id`; comparisons use the connected Mongoose model's schema, so supplied string or numeric ID models retain their identifier representation.
271
+
272
+ Filter values must use the field's JSON scalar type. `IN` and `NIN` require flat lists, `BTW` requires exactly two non-null bounds, and `LIKE` requires a string search fragment. Literal objects, nested lists, null list elements, invalid operators, unknown paths, and malformed groups are rejected with a structured 400 error instead of being ignored. Explicit `null` remains supported by `EQ` and `NE`. Enum names or declared internal enum values are converted to the stored representation; state-machine `state` filters preserve stored state names. Date filters convert valid date values without mutating the query input. Validated string scalars support partial `LIKE` searches, and range filters use their base scalar type rather than the field's create/update validation constraints.
273
+
238
274
  Query with powerful filtering options:
239
275
 
240
276
  ```graphql
@@ -648,7 +684,7 @@ const BookType = new GraphQLObjectType({
648
684
 
649
685
  ### Relationship Configuration
650
686
 
651
- - `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.
687
+ - `connectionField`: **(Required for referenced collections)** The child field linking back to the parent. For a single-object reference, this optionally overrides the stored ObjectId field; when omitted, model generation, writes, clears, and resolvers use the GraphQL field name.
652
688
  - `displayField`: **(Optional)** Field to use for display in UI components
653
689
  - `embedded`: **(Optional)** Whether the relation is embedded (default: false)
654
690
 
@@ -969,6 +1005,8 @@ mutation {
969
1005
  }
970
1006
  ```
971
1007
 
1008
+ For non-embedded collections, each `added`, `updated`, or `deleted` child runs the target type's global middleware with the request context. The operation and argument shapes match root mutations: `save`/`update` receive `{ input }`, and `delete` receives `{ id }`. Updated and deleted children must already belong to the current parent in the transaction; a missing child raises `NOT_VALID_ID`, and a different parent raises `FORBIDDEN`. Nested updates cannot move a child between parents. A rejection aborts the parent mutation and its child changes.
1009
+
972
1010
  ## ✅ Validations
973
1011
 
974
1012
  ### Declarative Validation Helpers
@@ -1431,7 +1469,7 @@ const bookController = {
1431
1469
  onSaved: async (doc, args, session, context) => {
1432
1470
  // After saving - doc is a plain object
1433
1471
  console.log(`Book saved: ${doc._id}`);
1434
- // Can access context.user for post-save operations like notifications
1472
+ // Use context.user for transactional audit/outbox records on this session
1435
1473
  },
1436
1474
 
1437
1475
  onUpdating: async (id, doc, session, context) => {
@@ -1483,7 +1521,7 @@ simfinity.connect(null, BookType, 'book', 'books', bookController);
1483
1521
  - `context`: GraphQL context object (includes request info, user data, etc.)
1484
1522
 
1485
1523
  **`onUpdated(doc, session, context)`**:
1486
- - `doc`: Full updated Mongoose document
1524
+ - `doc`: Full updated Mongoose document, after the parent update and nested collection writes have completed
1487
1525
  - `session`: Mongoose session for transaction
1488
1526
  - `context`: GraphQL context object (includes request info, user data, etc.)
1489
1527
 
@@ -1492,6 +1530,10 @@ simfinity.connect(null, BookType, 'book', 'books', bookController);
1492
1530
  - `session`: Mongoose session for transaction
1493
1531
  - `context`: GraphQL context object (includes request info, user data, etc.)
1494
1532
 
1533
+ The parent update executes before nested child operations. An update with no matching parent throws `NOT_VALID_ID` (404), without running child operations or `onUpdated`.
1534
+
1535
+ Lifecycle hooks, including `onSaved` and `onUpdated`, run before the transaction commits. They can run again after a transient transaction failure, with up to five retries after the first attempt. Keep additional database writes on the supplied session. For email or webhooks, write an outbox record in that transaction and deliver it after commit; external calls inside hooks cannot be rolled back. An uncertain commit result retries only the commit, up to five times, without repeating hooks. If the result remains uncertain, the error is returned and the database may already have committed; reconcile the outcome before repeating the operation.
1536
+
1495
1537
  ### Using Context in Controllers
1496
1538
 
1497
1539
  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:
@@ -1834,6 +1876,8 @@ const EpisodeType = new GraphQLObjectType({
1834
1876
  - **Modify Args In Place**: Scope functions should modify the `args` object directly
1835
1877
  - **Filter Structure**: Use the correct filter structure (`QLFilter` for scalars, `QLTypeFilterExpression` for relations)
1836
1878
  - **All Query Operations**: Scope applies to `find`, `aggregate`, and `get_by_id` operations
1879
+ - **Generated Relationships**: Non-embedded single relations run the target type's `get_by_id` middleware and scope; collection relations run its `find` middleware and scope with the same request context. A separate database predicate preserves the referenced ID or parent connection even when filters are changed, including by scope functions. A scoped-out single relation returns `null`; a collection omits scoped-out children.
1880
+ - **Custom Resolvers and Writes**: Existing relationship resolvers remain unchanged. Query scope does not authorize mutations; use global middleware or controller checks for write permissions, including permissions on nested children.
1837
1881
  - **Automatic Merging**: For `get_by_id`, the id filter is automatically combined with scope filters
1838
1882
  - **Context Access**: Use `context.user`, `context.ip`, or other context properties to determine scope
1839
1883
 
@@ -1904,14 +1948,20 @@ const permissions = {
1904
1948
 
1905
1949
  **Rule Types:**
1906
1950
  - **Function**: `(parent, args, ctx, info) => boolean | void | Promise<boolean | void>`
1907
- - **Array of functions**: All rules must pass (AND logic)
1908
- - **Policy expression**: JSON AST object (see below)
1951
+ - **Nonempty array of rules**: All rules must pass (AND logic); functions, expressions, and nested nonempty arrays can be mixed
1952
+ - **Policy expression**: JSON AST object or boolean literal (see below)
1953
+
1954
+ Permission maps must be plain objects (including objects with a null prototype). Only their own type and field entries are used. The default policy applies only when an exact field entry and wildcard are both absent. `defaultPolicy` must be exactly `'ALLOW'` or `'DENY'`; omitting it defaults to `'DENY'`.
1955
+
1956
+ `createAuthPlugin`, `createAuthMiddleware`, and `createFieldMiddleware` throw `TypeError` when configured with an invalid rule, empty rule array, malformed policy expression, permission map, or default policy. A configured `null`, `undefined`, or unknown object never falls back to `ALLOW`. Use `allow()` or `true` to grant access explicitly, or omit the entry to use the default policy.
1909
1957
 
1910
1958
  **Rule Semantics:**
1911
1959
  - `return true` or `return void` → allow
1912
1960
  - `return false` → deny
1913
1961
  - `throw Error` → deny with error
1914
1962
 
1963
+ Only `true` and `undefined` allow access. Other results, including `null`, `0`, an empty string, objects, and other truthy values, deny access. This contract is identical for direct rules, `composeRules`, `anyRule`, and `createRule`, including async results. `composeRules`, `anyRule`, and `createRule` require function arguments. `anyRule` may continue after a denial or error to a different granting rule.
1964
+
1915
1965
  ### Rule Helpers
1916
1966
 
1917
1967
  Simfinity.js provides reusable rule builders:
@@ -1956,6 +2006,8 @@ const permissions = {
1956
2006
 
1957
2007
  Requires the user to have a specific role. Supports custom paths:
1958
2008
 
2009
+ The required role must be a nonempty string or nonempty array of nonempty strings; invalid configuration throws `TypeError`. The user's single role value must match an allowed string exactly.
2010
+
1959
2011
  ```javascript
1960
2012
  const permissions = {
1961
2013
  Query: {
@@ -1976,6 +2028,8 @@ const permissions = {
1976
2028
 
1977
2029
  Requires the user to have specific permission(s). Supports custom paths:
1978
2030
 
2031
+ The required permission must be a nonempty string or nonempty array of nonempty strings; invalid configuration throws `TypeError`. The user's claim must be an array of nonempty strings. Entries match exactly; only a standalone `'*'` entry grants every permission. A claim such as `'posts:read'` must be supplied as `['posts:read']`. String claims, substrings, and embedded wildcard characters do not grant access.
2032
+
1979
2033
  ```javascript
1980
2034
  const permissions = {
1981
2035
  Mutation: {
@@ -2027,6 +2081,8 @@ const permissions = {
2027
2081
 
2028
2082
  Checks if the authenticated user owns the resource:
2029
2083
 
2084
+ IDs must be nonempty strings, finite numbers (including zero), or Mongoose `ObjectId` instances. Numbers compare by their string representation, and ObjectIds compare by hexadecimal value. Missing/null IDs, empty strings, arrays, booleans, and arbitrary objects deny access. Use the path or extractor arguments to obtain a supported ID from a custom identity object.
2085
+
2030
2086
  ```javascript
2031
2087
  const permissions = {
2032
2088
  Post: {
@@ -2074,7 +2130,13 @@ Use `{ ref: 'path' }` to reference values:
2074
2130
 
2075
2131
  **Security:**
2076
2132
  - Only `parent`, `args`, and `ctx` roots are allowed
2077
- - Unknown operators fail closed (deny)
2133
+ - Reference paths support root values, dotted fields, document getters, and numeric array indices. Empty path segments and `__proto__`, `prototype`, or `constructor` segments are invalid
2134
+ - `eq` and `in` require exactly two operands; `in` requires an array or a reference resolving to an array. `allOf` and `anyOf` require arrays of valid expressions; `not` requires a valid expression
2135
+ - Unknown operators or malformed nested expressions invalidate the entire configured policy, even inside an otherwise granting `anyOf`
2136
+ - Factories reject malformed expressions with `TypeError`; `isPolicyExpression` validates the complete AST, and direct `evaluateExpression` calls return `false` for malformed ASTs
2137
+ - Missing references and invalid runtime membership operands cannot grant access, including under `not` or repeated negation. They propagate through `allOf` and implicit AND. A separate valid `anyOf` branch can still grant access, such as a published post without an authenticated user
2138
+ - Explicit `null`, `false`, `0`, and empty-string comparison values remain valid; equality is strict. Use `requireAuth()` or `isOwner()` for identity checks rather than treating two explicit null IDs as ownership
2139
+ - Boolean expressions and logical identities remain valid: `{ allOf: [] }` is true, `{ anyOf: [] }` is false, and `{ not: false }` is true. Multiple operator keys form an implicit AND
2078
2140
  - No `eval()` or `Function()` - pure object traversal
2079
2141
 
2080
2142
  ### Integration with GraphQL Yoga / Envelop
@@ -2234,6 +2296,8 @@ simfinity.use((params, next) => {
2234
2296
 
2235
2297
  Each middleware receives a `params` object containing:
2236
2298
 
2299
+ Generated non-embedded relationship reads and nested collection mutations also invoke middleware for the related type. Middleware and scope callbacks are awaited. Nested child operations use the same argument shapes and request context as their root equivalents; review middleware that previously assumed it ran only once per root operation.
2300
+
2237
2301
  ```javascript
2238
2302
  simfinity.use((params, next) => {
2239
2303
  // params object contains:
@@ -2560,6 +2624,289 @@ console.log(UserType.getFields()); // Access GraphQL fields
2560
2624
  const BookInput = simfinity.getInputType(BookType);
2561
2625
  ```
2562
2626
 
2627
+ ## 🤖 MCP Generation
2628
+
2629
+ Simfinity can automatically expose every generated GraphQL operation as a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) tool. For each connected type you get one MCP tool per generated query and mutation — exactly mirroring the GraphQL surface:
2630
+
2631
+ | GraphQL operation | MCP tool name |
2632
+ | --- | --- |
2633
+ | Single-entity query | `book` |
2634
+ | List query | `books` |
2635
+ | Aggregate query | `books_aggregate` |
2636
+ | Add mutation | `addbook` |
2637
+ | Update mutation | `updatebook` |
2638
+ | Delete mutation | `deletebook` |
2639
+ | State-machine action | `publish_book` |
2640
+ | Custom mutation | _the registered name_ |
2641
+
2642
+ Each generated tool follows MCP best practices so an agent can use it without extra context:
2643
+
2644
+ - **`description`**: reuses the GraphQL type/field descriptions and adds an actionable summary. List and aggregate tools spell out the available filter operators (`EQ, NE, LT, LTE, GT, GTE, IN, NIN, BTW, LIKE`), `AND`/`OR` groups, pagination and sorting; aggregate tools additionally document the `SUM/COUNT/AVG/MIN/MAX` operations and the `aggregation` argument.
2645
+ - **`title`**: a human-readable label (e.g. `List Book`, `Create Book`, `Aggregate Book`).
2646
+ - **`inputSchema`**: JSON Schema derived from the GraphQL arguments (scalars, enums, input objects, lists, non-null wrappers, and recursive filter types such as `QLFilterGroup` via `$defs`/`$ref`). Nullable arguments, input fields and list items accept explicit `null`; non-null positions reject it. Nullable references use `anyOf` with a null alternative. GraphQL type and field descriptions are propagated, with curated descriptions for synthetic types. `Date`/`DateTime`/`Time` scalars use the matching JSON Schema `format`. Defaulted arguments are optional; representable defaults appear in the schema and generated GraphQL variables, with enum member names rather than internal values. Omitted opaque scalar defaults that have no GraphQL literal representation use the field's original default.
2647
+ - **`outputSchema`**: JSON Schema describing the returned data (mirroring the auto-generated selection set), with type/field descriptions. Every nullable GraphQL position also accepts `null` (e.g. `type: ['string', 'null']`; enums get `null` appended), so validating MCP clients accept the `null`s GraphQL legitimately returns in `structuredContent` (get-by-id misses, unset optional fields).
2648
+ - **`annotations`**: behavioral hints — queries are `readOnlyHint: true`, `delete*` is `destructiveHint: true`, `update*` is `idempotentHint: true`. Generated CRUD mutations are recognized by the placeholder descriptions Simfinity stamps on them, so a custom mutation that happens to be named `updateReport` (and carries its own description) keeps that description and gets no inferred hints.
2649
+
2650
+ On execution, each tool returns both a serialized JSON `text` content block and a machine-readable `structuredContent` (the GraphQL `data`) that conforms to the `outputSchema`. When the caller requests `pagination: { count: true }` on a **list** tool, the total record count is delivered in the tool result `_meta.count` (aggregate tools ignore the flag — their resolver never computes a total).
2651
+
2652
+ > The MCP transports require the optional `@modelcontextprotocol/sdk` dependency (`^1.13.0` or newer). Install it with `npm install @modelcontextprotocol/sdk@^1.13.0`. `generateMCPTools` works without it. SDK load problems raise distinct error codes: `MCP_SDK_NOT_INSTALLED` (not installed), `MCP_SDK_INCOMPATIBLE` (installed but too old to provide the requested transport — upgrade it), `MCP_SDK_LOAD_FAILED` (any other import failure).
2653
+
2654
+ ### Installation
2655
+
2656
+ ```bash
2657
+ npm install @modelcontextprotocol/sdk@^1.13.0
2658
+ ```
2659
+
2660
+ ### Generating tool definitions
2661
+
2662
+ `generateMCPTools(schema, options)` returns the tool list, a `callTool` executor and a `getOperation` inspector. It does not require the SDK and is handy for inspection or custom wiring:
2663
+
2664
+ ```javascript
2665
+ import * as simfinity from '@simtlix/simfinity-js';
2666
+
2667
+ const schema = simfinity.createSchema();
2668
+ const { tools, callTool, getOperation } = simfinity.generateMCPTools(schema);
2669
+
2670
+ // tools: [{ name, title, description, inputSchema, outputSchema, annotations, kind }, ...]
2671
+ const result = await callTool('addbook', { input: { title: 'Dune' } });
2672
+ // result: { content: [{ type: 'text', text: '...JSON...' }],
2673
+ // structuredContent: { addbook: { ... } }, isError: false }
2674
+
2675
+ // getOperation returns the prebuilt GraphQL document a tool executes
2676
+ console.log(getOperation('books'));
2677
+ // query booksOperation($id: QLFilter, $pagination: QLPagination, ...) {
2678
+ // books(id: $id, pagination: $pagination, ...) { id title ... }
2679
+ // }
2680
+ ```
2681
+
2682
+ Both `callTool` and `getOperation` throw a `SimfinityError` with code `MCP_TOOL_NOT_FOUND` for unknown tool names.
2683
+
2684
+ ### Standalone MCP server (stdio)
2685
+
2686
+ Use stdio for a standalone executable consumed by clients like Cursor or Claude Desktop:
2687
+
2688
+ ```javascript
2689
+ import * as simfinity from '@simtlix/simfinity-js';
2690
+
2691
+ const schema = simfinity.createSchema();
2692
+ await simfinity.startStdioMCPServer(schema, {
2693
+ serverName: 'my-api-mcp',
2694
+ context: { user: { id: 'system' } },
2695
+ });
2696
+ ```
2697
+
2698
+ ### HTTP MCP endpoint (alongside GraphQL)
2699
+
2700
+ Mount an MCP endpoint (Streamable HTTP) next to your existing GraphQL server:
2701
+
2702
+ ```javascript
2703
+ import express from 'express';
2704
+ import * as simfinity from '@simtlix/simfinity-js';
2705
+
2706
+ const schema = simfinity.createSchema();
2707
+ const app = express();
2708
+ app.use(express.json());
2709
+
2710
+ const mcpHandler = await simfinity.createHTTPMCPHandler(schema, {
2711
+ // In HTTP mode the context factory receives the Express `req`,
2712
+ // so you can authenticate per request (e.g. from the Authorization header).
2713
+ context: (req) => ({ user: resolveUserFrom(req.headers.authorization) }),
2714
+ });
2715
+ app.post('/mcp', mcpHandler);
2716
+ ```
2717
+
2718
+ For deployments reachable from a browser (especially localhost), enable the SDK's DNS-rebinding protection via `transportOptions`, and observe failures with `onError`:
2719
+
2720
+ ```javascript
2721
+ const mcpHandler = await simfinity.createHTTPMCPHandler(schema, {
2722
+ context: (req) => ({ user: resolveUserFrom(req.headers.authorization) }),
2723
+ // Spread into the SDK's StreamableHTTPServerTransport options
2724
+ transportOptions: {
2725
+ enableDnsRebindingProtection: true,
2726
+ allowedHosts: ['127.0.0.1:3000', 'localhost:3000'],
2727
+ allowedOrigins: ['http://localhost:3000'],
2728
+ },
2729
+ onError: (err, req, res) => console.error('MCP request failed', err),
2730
+ });
2731
+ ```
2732
+
2733
+ The handler catches all errors itself: `onError` is for logging/metrics, and a JSON-RPC internal error (HTTP 500) is sent automatically when response headers were not already written.
2734
+
2735
+ ### Options
2736
+
2737
+ | Option | Default | Description |
2738
+ | --- | --- | --- |
2739
+ | `execution` | `{ mode: 'in-process' }` | `in-process` runs against the in-memory `schema`. `remote` issues HTTP GraphQL requests: `{ mode: 'remote', endpoint, headers, timeoutMs, retry: { attempts, backoffMs } }`. See [Remote execution](#remote-execution). |
2740
+ | `context` | `{}` | GraphQL context value, or a factory. For in-process/stdio it is called as `(extra) => context`; for the HTTP handler it is called as `(req, extra) => context` so you can authenticate per request. Passed through to execution so `scope` functions and auth rules work (they read `context.user`). |
2741
+ | `include` / `exclude` | — | String or array: raw GraphQL field names, prefixed tool names, or the categories `'query'` / `'mutation'`. `exclude` wins over `include`. |
2742
+ | `includeTypes` / `excludeTypes` | — | String or array of entity (return) type names — one entry covers every tool for that entity (`'Book'` matches `book`, `books`, `books_aggregate`, `addbook`, ...). `excludeTypes` wins. |
2743
+ | `selectionDepth` | `1` | Nesting depth for the auto-generated output selection set (and the mirrored `outputSchema`). |
2744
+ | `includeId` | `true` | Select a scalar or enum `id` as a fallback when nothing else is selectable; otherwise use `__typename`. |
2745
+ | `toolNamePrefix` | — | Prefix prepended to every published tool name (e.g. `'catalog_'`). Must match `/^[a-zA-Z0-9_-]+$/`; the resulting names must match `/^[a-zA-Z0-9_-]{1,128}$/` (`MCP_INVALID_TOOL_NAME` otherwise). |
2746
+ | `toolOverrides` | `{}` | Per-tool overrides keyed by tool (or unprefixed field) name: `{ description, title, annotations, selectionDepth, includeId, selection }`. See [Customizing tools](#customizing-tools). |
2747
+ | `toolMiddleware` | — | Array of koa-style `async (call, next)` functions run around every tool call. See [Tool middleware](#tool-middleware). |
2748
+ | `limits` | — | Guardrails: `{ maxPageSize, defaultPagination, maxResultBytes }`. See [Limits](#limits). |
2749
+ | `schemaPlugins` | — | Envelop-style plugins whose `onSchemaChange` hook is applied once before serving (in-process execution only). See [Authentication](#authentication). |
2750
+ | `serverName` / `serverVersion` | `'simfinity-mcp'` / `'1.0.0'` | Identity reported by the MCP server. |
2751
+ | `transportOptions` | — | `createHTTPMCPHandler` only: stateless SDK transport options (e.g. `enableDnsRebindingProtection`, `allowedHosts`, `allowedOrigins`). Setting `sessionIdGenerator`, `onsessioninitialized`, `onsessionclosed` or `eventStore` raises `MCP_INVALID_TRANSPORT_OPTIONS` at setup. |
2752
+ | `onError` | — | `createHTTPMCPHandler` only: `(err, req, res)` callback invoked when a request fails; the handler still responds with a JSON-RPC internal error (HTTP 500) if headers were not sent. |
2753
+
2754
+ Selection sets always fall back to `id` and then `__typename` when nothing else is selectable (interface/union return types select `__typename`), so the generated documents are always statically valid — and the output schemas mirror the same fallbacks.
2755
+
2756
+ ### Selecting and naming tools
2757
+
2758
+ Trim and namespace the published surface with `include`/`exclude`, `includeTypes`/`excludeTypes` and `toolNamePrefix`:
2759
+
2760
+ ```javascript
2761
+ const { tools, callTool, getOperation } = simfinity.generateMCPTools(schema, {
2762
+ exclude: 'mutation', // read-only server
2763
+ excludeTypes: 'AuditLog', // hide every tool whose entity type is AuditLog
2764
+ toolNamePrefix: 'catalog_', // books -> catalog_books
2765
+ });
2766
+
2767
+ // callTool/getOperation use the published (prefixed) name;
2768
+ // the underlying GraphQL document still uses the raw field name.
2769
+ console.log(getOperation('catalog_books'));
2770
+ // query booksOperation(...) { books(...) { ... } }
2771
+ ```
2772
+
2773
+ - `include`/`exclude` entries match the raw GraphQL field name, the prefixed tool name, or the categories `'query'` / `'mutation'`. `exclude` always wins over `include`.
2774
+ - `includeTypes`/`excludeTypes` match the entity (return) type. Aggregate queries resolve their entity through the sibling list field (`books_aggregate` → `books`), so `excludeTypes: 'Book'` also hides the aggregate tool. With an `includeTypes` allowlist, tools without an object return type are dropped too.
2775
+ - Duplicate tool names (e.g. a Query field and a Mutation field sharing a name) are not allowed by MCP: the first tool wins and the later one is skipped with a `console.warn` — rename one of the GraphQL fields or use `include`/`exclude`.
2776
+
2777
+ ### Customizing tools
2778
+
2779
+ `toolOverrides` tunes individual tools without touching the schema. Keys accept either the published tool name or the unprefixed field name:
2780
+
2781
+ ```javascript
2782
+ const { tools } = simfinity.generateMCPTools(schema, {
2783
+ toolOverrides: {
2784
+ books: {
2785
+ title: 'Search books',
2786
+ description: 'Search the book catalog. Filter by title, author or publication year.',
2787
+ selectionDepth: 2, // deeper selection set for this tool only
2788
+ },
2789
+ publish_book: {
2790
+ annotations: { idempotentHint: true }, // merged over the generated annotations
2791
+ },
2792
+ book: {
2793
+ selection: 'id title author { name }', // explicit selection set
2794
+ },
2795
+ },
2796
+ });
2797
+ ```
2798
+
2799
+ `selection` accepts `'id title'` or `'{ id title }'` and replaces the auto-generated selection set. When it is set, the `outputSchema` is **omitted** for that tool — it can no longer be guaranteed to match the returned data, and publishing a wrong schema would make validating clients reject valid results.
2800
+
2801
+ ### Limits
2802
+
2803
+ `limits` protects agents (and your database) from oversized requests and responses. Violations come back as regular `isError` tool results, not exceptions:
2804
+
2805
+ ```javascript
2806
+ const { callTool } = simfinity.generateMCPTools(schema, {
2807
+ limits: {
2808
+ maxPageSize: 100, // MCP_PAGE_SIZE_EXCEEDED above this
2809
+ defaultPagination: { page: 1, size: 20 }, // injected when the caller sends none
2810
+ maxResultBytes: 262144, // MCP_RESULT_TOO_LARGE above this
2811
+ },
2812
+ });
2813
+ ```
2814
+
2815
+ - `maxPageSize`: rejects calls whose `pagination.size` exceeds the cap.
2816
+ - `defaultPagination`: injected only for tools that have a `pagination` argument, and only when the caller did not send one. `page` and `size` are both required (QLPagination declares them non-null), and `size` must not exceed `maxPageSize` — that misconfiguration throws `MCP_INVALID_LIMITS` at setup.
2817
+ - `maxResultBytes`: caps the UTF-8 byte length of the pretty-printed logical payload: `data` on success, or `{ errors, data? }` on failure. Oversized errors and partial data are replaced by `MCP_RESULT_TOO_LARGE`. This diagnostic and pagination-limit diagnostics are exempt from the cap, so even a tiny cap can return an actionable error. The cap excludes MCP envelope overhead, the duplicate `structuredContent`, and middleware-created results; it runs after execution and does not limit database work or remote download size.
2818
+
2819
+ ### Tool middleware
2820
+
2821
+ `toolMiddleware` wraps every tool execution with koa-style `(call, next)` functions — for logging, metrics, argument rewriting or policy checks. `call` is `{ name, args, extra, kind, operation }`:
2822
+
2823
+ ```javascript
2824
+ const { callTool } = simfinity.generateMCPTools(schema, {
2825
+ toolMiddleware: [
2826
+ async (call, next) => {
2827
+ const started = Date.now();
2828
+ const result = await next();
2829
+ console.log(`[mcp] ${call.name} (${call.kind}) took ${Date.now() - started}ms`);
2830
+ return result;
2831
+ },
2832
+ async (call, next) => {
2833
+ if (call.kind === 'mutation' && process.env.MCP_READ_ONLY === 'true') {
2834
+ // Short-circuit: return a result without calling next()
2835
+ return {
2836
+ content: [{ type: 'text', text: 'Mutations are disabled on this server.' }],
2837
+ isError: true,
2838
+ };
2839
+ }
2840
+ return next();
2841
+ },
2842
+ ],
2843
+ });
2844
+ ```
2845
+
2846
+ Middleware can mutate `call.args` before calling `next()`, short-circuit by returning a result, or throw. Calling `next()` twice rejects with `MCP_MIDDLEWARE_ERROR`.
2847
+
2848
+ ### Remote execution
2849
+
2850
+ `execution.mode: 'remote'` turns the MCP server into a thin proxy that POSTs each operation to an existing GraphQL endpoint. Harden it with `timeoutMs` and `retry`:
2851
+
2852
+ ```javascript
2853
+ const { callTool } = simfinity.generateMCPTools(schema, {
2854
+ execution: {
2855
+ mode: 'remote',
2856
+ endpoint: 'https://api.example.com/graphql',
2857
+ headers: { Authorization: `Bearer ${token}` },
2858
+ timeoutMs: 10000, // abort requests slower than 10s
2859
+ retry: { attempts: 2, backoffMs: 250 }, // up to 2 extra attempts, linear backoff
2860
+ },
2861
+ });
2862
+ ```
2863
+
2864
+ - `timeoutMs` aborts slow attempts, including reading the response body (`AbortSignal.timeout`); the timeout signal is combined with the MCP client's cancellation signal. Client cancellation rejects during either fetch or body reading and is never retried.
2865
+ - `retry` re-issues failed **queries only — never mutations** — after network errors/timeouts, HTTP 5xx and 429 responses. Backoff is linear: the first retry waits `backoffMs`, the second `2 × backoffMs`, and so on (default `backoffMs`: 250).
2866
+ - Transport failures surface as `isError` tool results carrying GraphQL-shaped errors with the codes `MCP_REMOTE_HTTP_ERROR` (non-2xx response without a valid GraphQL body, includes `extensions.status`), `MCP_REMOTE_REQUEST_FAILED` (network error/timeout, including body-read failures) and `MCP_REMOTE_INVALID_RESPONSE` (non-JSON body or invalid GraphQL response shape). A valid body has object `data` or a nonempty `errors` array with string messages; `data: null` requires errors. Invalid successful HTTP responses are not retried. Client cancellation rejects instead of returning an error tool result.
2867
+ - If the remote response carries a numeric `extensions.count` (e.g. exposed via `simfinity.plugins.envelopCountPlugin()`), it is surfaced as `_meta.count` on the tool result.
2868
+
2869
+ `schemaPlugins` is not applied in remote mode — the remote GraphQL server enforces its own plugins.
2870
+
2871
+ ### Tool results and errors
2872
+
2873
+ Successful calls return `{ content: [textJSON], structuredContent: data, isError: false }`. Failures and edge cases behave as follows:
2874
+
2875
+ - **Total count**: with `pagination: { count: true }` on a **list** tool, the count lands in `_meta.count` on the tool result — in-process it is read back from a per-call context layer (Simfinity's find resolver writes it there; counts never leak across calls), in remote mode from the response `extensions.count`. Aggregate tools never deliver a count.
2876
+ - **Execution errors**: `isError: true` with a `text` payload of `{ errors, data? }` — partial data is preserved alongside errors when it fits `maxResultBytes`. `structuredContent` is omitted on errors.
2877
+ - **Remote transport failures** and **limit violations** are also `isError` results (see [Remote execution](#remote-execution) and [Limits](#limits) for the codes).
2878
+ - **Unknown tool names** are the exception: `callTool` and `getOperation` throw `MCP_TOOL_NOT_FOUND`. An aborted `extra.signal` throws `MCP_CALL_CANCELLED` before execution, including cancellation while awaiting an asynchronous context factory. This prevents starting the GraphQL operation after cancellation; it does not undo or cancel database work already started.
2879
+
2880
+ ### Authentication
2881
+
2882
+ The MCP tools execute the same resolvers as GraphQL, so the GraphQL `context` flows through unchanged: pass credentials via `options.context` (a value or a factory). [Query scope](#-query-scope) functions read the context on every execution and need no extra wiring.
2883
+
2884
+ Field-level rules from [`createAuthPlugin`](#-authorization) are different. The auth plugin is an Envelop plugin that wraps resolvers from its `onSchemaChange` hook — a hook that only fires when an Envelop/Yoga server boots. A standalone MCP server executes operations with bare `graphql()`, so without help the hook never fires and the field-level rules are silently skipped. Pass the plugin via `schemaPlugins` to get auth parity: every plugin's `onSchemaChange` is invoked once before serving (in-process execution only):
2885
+
2886
+ ```javascript
2887
+ import * as simfinity from '@simtlix/simfinity-js';
2888
+
2889
+ const { createAuthPlugin, requireAuth, requireRole } = simfinity.auth;
2890
+
2891
+ // Permissions are keyed by the schema's type names. Simfinity names its
2892
+ // roots `RootQueryType` and `Mutation`.
2893
+ const permissions = {
2894
+ RootQueryType: { books: requireAuth() },
2895
+ Mutation: { deletebook: requireRole('admin') },
2896
+ };
2897
+
2898
+ const schema = simfinity.createSchema();
2899
+ await simfinity.startStdioMCPServer(schema, {
2900
+ // requireRole reads `user.role` by default (see rolePath to customize).
2901
+ context: { user: { id: 'agent', role: 'admin' } },
2902
+ schemaPlugins: [createAuthPlugin(permissions, { defaultPolicy: 'ALLOW' })],
2903
+ });
2904
+ ```
2905
+
2906
+ Mounting the MCP handler next to a running Envelop/Yoga server that shares the same `schema` object also works: Yoga fires `onSchemaChange` at startup and `createAuthPlugin` wraps the resolvers in place, so the MCP tools execute the already-wrapped schema.
2907
+
2908
+ For per-request HTTP auth, use a function `context`: the HTTP handler invokes it as `(req, extra) => context`, so you can read the `Authorization` header from the Express `req` and map it to `context.user` exactly like your GraphQL server does.
2909
+
2563
2910
  ## 📊 Aggregation Queries
2564
2911
 
2565
2912
  Simfinity.js now supports powerful GraphQL aggregation queries with GROUP BY functionality, allowing you to perform aggregate operations (SUM, COUNT, AVG, MIN, MAX) on your data.
@@ -2761,9 +3108,10 @@ The `groupId` and `path` parameters support:
2761
3108
 
2762
3109
  ### Pagination Notes
2763
3110
 
2764
- - The `page` and `size` parameters work as expected
3111
+ - Explicit `page` and `size` must be positive safe integers, and `size` must fit the configured maximum (1000 by default). Invalid input produces `INVALID_PAGINATION` with status 400.
2765
3112
  - The `count` parameter is **ignored** for aggregation queries
2766
3113
  - Pagination is applied **after** grouping and sorting
3114
+ - Aggregation queries remain unbounded when pagination is omitted; they do not inherit the list default limit.
2767
3115
 
2768
3116
  ### MongoDB Translation
2769
3117
 
@@ -3206,6 +3554,16 @@ query {
3206
3554
 
3207
3555
  Simfinity.js supports built-in pagination with optional total count:
3208
3556
 
3557
+ Configure the process-wide maximum once during application startup:
3558
+
3559
+ ```javascript
3560
+ import * as simfinity from '@simtlix/simfinity-js';
3561
+
3562
+ simfinity.configureQueryLimits({ maxPageSize: 500 });
3563
+ ```
3564
+
3565
+ The default maximum is **1000**. Explicit `page` and `size` must be positive safe integers, `size` must not exceed the maximum, and the computed skip must remain a safe integer. Invalid pagination throws `INVALID_PAGINATION` (400); invalid configuration throws `INVALID_QUERY_LIMITS` (400). With no pagination, a list returns at most `Math.min(100, maxPageSize)` records. Any positive safe-integer maximum is allowed, including values below 100. Call `configureQueryLimits()` to restore the default maximum. This deliberately tightens previously unbounded explicit page sizes; applications needing larger pages can configure an appropriate maximum. Unpaginated aggregate queries retain their existing behavior.
3566
+
3209
3567
  ```graphql
3210
3568
  query {
3211
3569
  series(
@@ -3293,6 +3651,8 @@ See the [Plugins for Count in Extensions](#-plugins-for-count-in-extensions) sec
3293
3651
 
3294
3652
  ### 11. Sorting
3295
3653
 
3654
+ List sort fields must resolve to declared scalar or enum paths, and every term requires `ASC` or `DESC`. Root `id` and related `author.id` sort by their stored `_id` paths. Embedded paths remain dotted, and repeated sort terms through one relationship reuse the same lookup. Supply at least one term; invalid sort input is rejected before executing the list query.
3655
+
3296
3656
  Simfinity.js supports sorting with multiple fields and sort orders:
3297
3657
 
3298
3658
  ```graphql
@@ -3910,17 +4270,21 @@ console.log(BookInput.getFields()); // Input fields for mutations
3910
4270
 
3911
4271
  ### `saveObject(typeName, args, session?, context?)`
3912
4272
 
3913
- Programmatically save an object outside of GraphQL mutations.
4273
+ Programmatically save an object through the creation pipeline, including validation, controllers, and nested writes.
3914
4274
 
3915
4275
  **Parameters:**
3916
4276
  - `typeName` (string): The name of the GraphQL type
3917
4277
  - `args` (object): The data to save
3918
- - `session` (MongooseSession, optional): Database session for transactions
4278
+ - `session` (MongooseSession, optional): Caller-owned session with an active transaction, from the same MongoDB client as the registered model
3919
4279
  - `context` (object, optional): GraphQL context object (includes request info, user data, etc.)
3920
4280
 
3921
4281
  **Returns:**
3922
4282
  - `Promise<object>`: The saved object
3923
4283
 
4284
+ Without a session, `saveObject()` starts a transaction on the registered model's connection, commits the parent and nested writes together, and awaits session cleanup. The same bounded transaction and commit retries described above apply. This requires a transaction-capable MongoDB deployment, such as a replica set.
4285
+
4286
+ With a session, it participates in the caller's active transaction. It never starts, commits, aborts, retries, or ends that caller-owned transaction/session; the caller must handle failures and finish the transaction. An inactive supplied session is rejected with `ACTIVE_TRANSACTION_REQUIRED` (400) before writes. Pass the provided session when calling from a controller or custom mutation so the writes share its transaction. Direct calls bypass GraphQL input coercion, field authorization, and global middleware.
4287
+
3924
4288
  **Example:**
3925
4289
 
3926
4290
  ```javascript
@@ -3957,5 +4321,3 @@ const schema = simfinity.createSchema();
3957
4321
  ```
3958
4322
 
3959
4323
  *Built with ❤️ by [Simtlix](https://github.com/simtlix)*
3960
-
3961
-