Skip to content

v3.7.0 · MongoDB & PostgreSQL. Install from npm or download the starters.

Middleware ​

Register global middleware with simfinity.use() to inspect arguments, enforce prerequisites, or attach request metadata before a generated operation executes.

javascript
// simfinity is your selected runtime, initialized during application setup.

simfinity.use(async ({ operation, type, context }, next) => {
  if (operation === 'delete' && context?.user?.role !== 'admin') {
    throw new simfinity.auth.ForbiddenError('Only admins can delete records');
  }

  console.log('Preparing operation', {
    operation,
    type: type?.gqltype.name,
  });
  await next();
});

Register middleware once during application startup. use() accepts only functions; any other value, such as null from enabled && middleware, throws INVALID_MIDDLEWARE (500). Registrations belong to the selected runtime and apply to its generated operations. The MongoDB facade and PostgreSQL module facade each expose a default runtime; a createPostgres() instance owns its own registrations. Register middleware on the same runtime used to register your types.

For MongoDB, simfinity is the namespace imported from @simtlix/simfinity-js. For PostgreSQL, use the instance returned by createPostgres() in the PostgreSQL quick start. Both expose use() and the shared auth.ForbiddenError.

Execution order ​

For root reads and generated non-embedded relationship reads, the sequence is middleware, query scope, query construction, and database execution. Root mutation middleware runs before the transactional mutation handler. Nested collection mutations run the target child's middleware inside that transaction before checking its persisted parent and performing the child operation.

What next() means

next() advances to the next registered middleware. The database resolver executes after the entire middleware chain returns. Code after await next() still runs before database execution, and omitting next() only skips the remaining middleware. Throw an error to cancel the operation.

Middleware runs in registration order, and the runtime waits for the whole chain before the operation runs:

  • The rest of the chain runs at most once. Calling next() again returns the same promise.
  • The runtime awaits the rest of the chain even if a middleware calls next() without awaiting it, and an error from the rest of the chain cancels the operation. If the middleware awaits other work after an un-awaited next(), that error cancels the operation when the middleware finishes; it never becomes an unhandled rejection. If the middleware itself throws or rejects after an un-awaited next(), the runtime still waits for the rest of the chain to settle, then rejects the operation with the middleware's own error.
  • An error thrown by a later middleware cancels the operation, even if an earlier middleware catches it around await next(). Use that try/catch for logging, not to recover.
  • Call next() before your middleware returns, or before the promise it returns settles. A next() called after that, for example from setTimeout(next) or another callback, does nothing and returns a resolved promise. The remaining middleware is skipped, exactly as when next() is omitted, and the operation still runs. Write middleware as async functions that await next().

Use controllers for before/after persistence hooks. Use your GraphQL server's execution hooks to measure complete request duration. MCP's separate tool middleware wraps actual tool execution and has a different contract.

Middleware context ​

javascript
simfinity.use(async (params, next) => {
  const { args, operation, context } = params;
  // Mutate args or context in place when preparing the operation.
  await next();
});
PropertyAvailability
argsGraphQL arguments: for example, args.input for add/update or args.id for delete.
operationOne of the operation values below.
contextThe GraphQL request context. For nested collection children of a programmatic saveObject() call, the context passed to saveObject(), which is undefined when omitted; guard with context?..
typeRegistered type metadata for generated entity operations; absent for custom mutations.
entryCustom mutation name when operation is custom_mutation.
actionName, actionFieldState-machine action name and configuration for state_changed.
OperationTrigger
findList query or generated non-embedded collection relation.
get_by_idSingle-record query or generated non-embedded single relation with a reference.
aggregateAggregation query.
saveGenerated add mutation or nested added child (including children of a saveObject() call).
updateGenerated update mutation or nested updated child (including children of a saveObject() call).
deleteGenerated delete mutation or nested deleted child (including children of a saveObject() call).
state_changedGenerated state-machine action.
custom_mutationRegistered custom mutation.

Prepare arguments ​

This middleware supplies default pagination for list queries that omit it:

javascript
simfinity.use(async ({ operation, args }, next) => {
  if (operation === 'find' && !args.pagination) {
    args.pagination = { page: 1, size: 25 };
  }
  await next();
});

Prefer mutating the existing args object. Generated list queries (including their total count), aggregations and collection-relationship reads pass the params.args object left after the middleware chain to the query scope and the database adapter, so find and aggregate middleware may also replace it. Mutations, nested collection writes and get_by_id reads keep the resolver's own reference, so replacing params.args there has no effect. With an adapter that batches collection reads, a generated collection read that can join a batch passes the adapter a copy of params.args, taken once the middleware chain and scopes finish, even when it ends up reading alone, and fields whose arguments hold the same data may share one adapter read, so change args before your middleware returns. Only reads that cannot join a batch pass params.args itself, as before: those without a context object, with an adapter that has no findChildrenBatch or whose batchesChildren returns false, for a parent without an ID the adapter casts unchanged, or with arguments that are not plain data, would form a key over 64 KiB or have invalid pagination. Client filter, sort and aggregation paths are checked before middleware runs, and paths that middleware adds are trusted; see path restrictions.

For record visibility, prefer the type's query scope. Generated relationship reads and nested collection writes invoke middleware with the target type, the same request context (for a programmatic saveObject() call, the context passed to it, possibly undefined), and root-compatible argument shapes. Existing custom resolvers and direct programmatic data access remain responsible for invoking their own checks. Review middleware that assumes a request only invokes it for its root operation.

Open source. Released under the Apache 2.0 License.