Skip to content

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

Core API ​

Import the MongoDB facade as an ES module:

javascript
import * as simfinity from '@simtlix/simfinity-js';

For PostgreSQL, import createPostgres from @simtlix/simfinity-postgres and use the returned runtime instance. Both packages provide named exports and TypeScript declarations. Registrations and middleware belong to a runtime instance; register application types once during startup.

Shared examples can import the selected instance from your application’s runtime module. Build the schema once after registrations; PostgreSQL must initialize its generated storage before serving operations.

Find an operation ​

TaskAPIResult
Register a domain typeconnectType registration; no return value
Build the executable APIcreateSchemaGraphQLSchema
Register an application operationregisterMutationCustom mutation registration
Retrieve a registered typegetTypeType, undefined, or null as described below
Work with its modelgetModelRegistered backend-native Model
Inspect a create inputgetInputTypeGenerated GraphQLInputObjectType
Create in an owned or existing transactionsaveObjectSaved object
Set the maximum query page sizeconfigureQueryLimitsProcess-wide configuration
Cap nested collection operationsconfigureMutationLimitsProcess-wide configuration

createMongoAdapter ​

javascript
const adapter = simfinity.createMongoAdapter({ referentialIntegrity: 'transactional' });
const runtime = simfinity.createRuntime(adapter);
// Connect MongoDB, register all types and call runtime.createSchema().
await adapter.initialize();

createMongoAdapter(options?: MongoAdapterOptions): MongoAdapter accepts referentialIntegrity: 'off' | 'transactional', defaulting to 'off'. The mode is immutable and exposed as readonly adapter.referentialIntegrity. Transactional mode checks references and restricts target deletion using coordinated MongoDB transactions. initialize(): Promise<void> validates startup and existing data; await it before serving protected operations. It is a no-op in off mode. See the complete contract.

configureQueryLimits ​

javascript
simfinity.configureQueryLimits({ maxPageSize: 500 });

Sets the process-wide maximum for explicit list and aggregate page sizes. Configure it once at startup. maxPageSize must be a positive safe integer and defaults to 1000; calling without arguments resets that default. Invalid configuration throws INVALID_QUERY_LIMITS (400). Unpaged lists use Math.min(100, maxPageSize) and unpaged aggregate queries remain unbounded. See pagination for request validation and compatibility details.

configureMutationLimits ​

javascript
simfinity.configureMutationLimits({ maxNestedOperations: 100 });

Caps the nested collection operations of one generated mutation. Configure it once at startup. Like configureQueryLimits, the setting is process-wide: every runtime and facade shares it.

maxNestedOperations is a non-negative safe integer, or null for no limit, which is the default. 0 forbids nested operations while plain creates and updates still work. Calling the function without arguments, with {} or with { maxNestedOperations: null } restores the default. Options that are not a plain object, any option other than maxNestedOperations (such as the misspelled maxNestedOperation), or an invalid value throw INVALID_MUTATION_LIMITS (400) and keep the current limit.

The limit applies to generated add, update and state-action mutations. Simfinity counts the added, updated and deleted entries of non-embedded collection fields at every nesting level, including null entries. It counts after root middleware and before the transaction starts, and a mutation over the limit fails with NESTED_OPERATIONS_EXCEEDED (400) without writing anything. Root deletes, saveObject(), custom mutations and embedded lists are not counted. See limit nested collection operations.

connect ​

javascript
simfinity.connect(
  model,
  gqltype,
  simpleEntityEndpointName,
  listEntitiesEndpointName,
  controller,
  onModelCreated,
  stateMachine,
);

Registers a GraphQLObjectType and its generated operations. Returns undefined. It throws INVALID_SCOPE (500) for an invalid extensions.scope and TYPE_BOUND_TO_OTHER_RUNTIME (409) for a type that another runtime bound or reserved; a rejected type is not registered.

ParameterTypeRequiredBehavior
modelMongoose model or nullYesMongoDB accepts an existing model; pass null to generate storage. PostgreSQL requires null.
gqltypeGraphQLObjectTypeYesThe exact type instance to register. A type is bound to the first runtime whose schema generates its relation resolvers, or reserved by the first schema that reaches it with an unresolved relation field.
simpleEntityEndpointNamestringYesSingle-record query name and CRUD suffix; no inferred default.
listEntitiesEndpointNamestringYesList query name and aggregation prefix; no inferred default.
controllerController object or nullNoLifecycle hooks; omitted hooks add no application behavior.
onModelCreated(model) => void or nullNoSynchronous callback for a generated model, not a supplied model.
stateMachineState machine object or nullNoState transitions; omitted means no configured machine.
javascript
simfinity.connect(null, SerieType, 'serie', 'series');

This creates serie, series, series_aggregate, addserie, updateserie, and deleteserie. Names are used as supplied; Simfinity does not pluralize them for you. Generated models/tables use the GraphQL type name, such as Serie.

addNoEndpointType ​

javascript
simfinity.addNoEndpointType(gqltype);

Registers a supporting object type without root CRUD endpoints. Use it for embedded or supporting types needed by connected types. It validates the type like connect(). Model creation is conditional on the persistent graph: a scalar-only value type embedded in an owner stays inline, while a supporting type referenced by a connected type receives backend storage without gaining root operations. A referenced supporting type is an entity, so its id resolves to its stored identity; a type used only as an embedded value returns its declared id member.

createSchema ​

javascript
const schema = simfinity.createSchema(
  includedQueryTypes,
  includedMutationTypes,
  includedCustomMutations,
);

Builds models, generated resolvers, input types, and an executable GraphQLSchema. The root names are RootQueryType and Mutation.

Before it creates any model, createSchema() validates the scope of every registered type (INVALID_SCOPE) and rejects the schema with TYPE_BOUND_TO_OTHER_RUNTIME when it reaches, through fields, interfaces, union members or custom mutation results, a type that another runtime bound or reserved, or a field copied with toConfig() after another runtime generated its relation resolver. A list relation, embedded object or embedded list whose type is not registered throws UNREGISTERED_RELATION_TARGET (500). A writable non-embedded list relation whose child connectionField is missing or empty, and whose resolver Simfinity would generate, throws INVALID_MODEL (400, Type.field requires a child connectionField) before the adapter prepares storage. In default MongoDB mode and with custom adapters, such a collection with its own resolver, or marked readOnly, builds and logs a Configuration issue warning instead, except a readOnly one with its own resolver, which is silent; its nested writes fail with INVALID_MODEL (500). PostgreSQL and transactional MongoDB reject every collection without connectionField. Fields named like generated query arguments also log a warning; see fields named like query arguments. Interface and union fields build on default MongoDB and custom adapters as output-only fields, with a Configuration issue warning when they are not readOnly; PostgreSQL and transactional MongoDB reject them with INVALID_MODEL. See what gets generated. Generated inputs that would have no fields are left out with a warning, together with the added or updated collection operations, embedded fields and add, update or state-action mutations that would take them; a non-null embedded field whose embedded type has no writable fields throws INVALID_MODEL (400) instead when the generated mutations reach its owner. PostgreSQL and transactional MongoDB reject embedded types that contain each other with INVALID_MODEL (Embedded cycle at …); default MongoDB mode rejects them when Simfinity generates a model whose embedded fields reach them, and does not check a cycle that only supplied models reach. An application type named FieldExtensionsType or RelationType in the first Simfinity schema of a process renames Simfinity's introspection metadata types to SimfinityFieldExtensionsType and SimfinityRelationType. That first schema fixes the names: a later createSchema() with an application type that has one of the current names, FieldExtensionsType and RelationType or the fallback names after a rename, throws RESERVED_TYPE_NAME (500), before any model is created when the type is an included endpoint type, a registered mutation's input or output type, an output type they reach or a field argument's type, and when the schema is constructed otherwise. The first schema binds the types whose relation resolvers it generates to this runtime. It also reserves reachable types that still have an unresolved relation field; see types belong to one runtime.

ArgumentMatching behavior
includedQueryTypesArray of the exact object-type instances passed to connect().
includedMutationTypesArray of the exact object-type instances passed to connect().
includedCustomMutationsArray of registered custom mutation names.

Omitting an argument or passing null includes all registered entries in that category. An empty array excludes that category's fields. String type names do not work in the first two arrays.

The allowlists select root fields only. Every type registered with connect(), and every type that a non-embedded relation of a registered type targets, resolves id to its stored identity wherever the schema reaches it, unless id has your own resolver: _id, falling back to id, which is _id on MongoDB. Types used only as embedded values return their declared id member. Embedded copies of an entity type return their declared id too, which is null for copies created through add inputs, except that on MongoDB copies in an embedded list report the automatic subdocument _id; see supporting types without endpoints.

javascript
const schema = simfinity.createSchema(
  [SerieType, SeasonType],
  [SerieType],
  ['importSeries'],
);

Ensure the resulting schema has fields in both generated roots. createSchema() always creates a Mutation object; excluding all mutations and all custom mutations leaves an empty root that GraphQL rejects. Allowlists control exposed fields, not whether registered models are initialized.

registerMutation ​

javascript
simfinity.registerMutation(name, description, inputModel, outputModel, callback);

Registers a custom mutation before creating the schema. When inputModel is a GraphQLInputObjectType, the mutation receives a required input argument. Passing null or undefined omits that argument. outputModel is a GraphQL output type.

The callback runs as callback(input, session, context) inside the mutation transaction. Use the supplied session for related writes. On MongoDB, in the default referentialIntegrity: 'off' mode, a custom mutation's session uses the default Mongoose connection, or the one MongoDB client of the registered models while the default connection is unused; with transactional reference integrity it uses the first protected model's connection. See which connection the session uses.

javascript
import { GraphQLInputObjectType, GraphQLNonNull, GraphQLString } from 'graphql';
import { simfinity } from './runtime.js';

const ImportSerieInput = new GraphQLInputObjectType({
  name: 'ImportSerieInput',
  fields: { name: { type: new GraphQLNonNull(GraphQLString) } },
});

simfinity.registerMutation(
  'importSeries',
  'Create a series through the import workflow.',
  ImportSerieInput,
  SerieType,
  (input, session, context) => simfinity.saveObject('Serie', input, session, context),
);

getType ​

javascript
simfinity.getType('Serie');
simfinity.getType({ name: 'Serie' });

Returns the registered GraphQLObjectType. A missing registered name returns undefined; an unsupported argument returns null. For circular imports, perform lookups inside GraphQL's lazy fields: () => ({ ... }) function after the referenced types have been registered.

getModel ​

javascript
const SerieModel = simfinity.getModel(SerieType);

Returns the model associated with a registered type. Retrieve generated models after createSchema(). The argument must have a name property; this function does not take a string name.

The MongoDB facade returns a Mongoose model. PostgreSQL returns a PostgresModel with findById, find, create, update, and delete; records are plain objects with one UUID exposed as both id and _id. It has no Mongoose query chaining or .lean(). Direct native calls on either backend bypass generated GraphQL permissions, scopes, middleware, validators, and controller pipelines.

getInputType ​

javascript
const SerieInput = simfinity.getInputType(SerieType);

Returns the registered create input type after schema generation. For a type without writable fields it has no fields, and generated schemas do not use it; give a registered mutation that creates such records no input or one of its own. For the generated update input, inspect the schema's updateserie field instead of assuming that getInputType() returns the update variant.

saveObject ​

javascript
await simfinity.saveObject('Serie', input, session, context);

Runs create materialization, field/type validators, collection processing, state initialization, and create controller hooks for a registered type. The type name is a string. Generated models must already exist.

Without session, saveObject() owns the backend transaction, including bounded retries and cleanup. Parent and nested writes commit or roll back together. MongoDB requires a transaction-capable deployment; PostgreSQL uses repeatable-read isolation.

With session, the caller must already have an active session valid for the selected backend and owns commit, retry and cleanup. In default Mongo mode the caller also owns abort. With transactional Mongo reference integrity, a reference violation or guarded-write error aborts even the supplied transaction; the mode requires snapshot read concern and majority write concern. MongoDB throws ACTIVE_TRANSACTION_REQUIRED (400) for an inactive session. PostgreSQL requires an active Simfinity session from the same runtime and throws INVALID_SESSION for inactive sessions, arbitrary pg.Client objects, or sessions from another runtime. Pass the provided session inside a controller or custom mutation to share its transaction.

MongoDB-owned transactions retry transient failures up to five times after the first attempt, each after a random delay of less than 10, 20, 40, 80 and 160 ms; uncertain commit results retry only commit up to five times. PostgreSQL retries confirmed serialization failures and deadlocks up to five times, after the same random delays, but never retries an unknown commit outcome. In either case an uncertain commit may already have succeeded. An owned transaction whose concurrent-write conflict outlasts the retries fails with TRANSACTION_RETRY_EXCEEDED (409); on MongoDB the driver error is error.cause. With a supplied session, these errors reach the caller unchanged, with MongoDB's errorLabels, because the caller owns retries. See transaction boundaries. Hooks run before commit and may repeat on a transient transaction retry. Calling saveObject() directly skips GraphQL input coercion, field authorization and the global middleware of the root record. Nested non-embedded collection children (added, updated, deleted) still run their target type's global middleware, controllers and ownership checks, with the context you pass, which is undefined when omitted. Without a session, a rejection rolls back the whole workflow; with a supplied session the error reaches the caller, who owns the abort in default MongoDB mode. Validate and authorize programmatic callers accordingly. Relation inputs need { id }: on MongoDB a missing or malformed id, including a lean { _id } document, fails with NOT_VALID_ID (400). Nested items through a collection without connectionField fail with INVALID_MODEL (500) before anything is written.

Configuration and helpers ​

ExportPurpose
use(middleware)Register a global pre-operation middleware. Throws INVALID_MIDDLEWARE (500) for a non-function.
preventCreatingCollection(prevent)MongoDB toggles explicit collection creation. On PostgreSQL, call it before createSchema() to force read-only storage validation during initialization.
createValidatedScalar(name, description, baseScalarType, validate)Create a validated scalar.
buildErrorFormatter(callback)Create an error formatter for your GraphQL server. The formatter returns a GraphQLError whose originalError is the classified Simfinity error.
buildQuery(input, gqltype, isCount = false)Build a MongoDB aggregation pipeline from list-query arguments. Does not execute scopes or middleware. Relation lookups use reserved __sf_lN aliases, removed by a trailing $unset stage. input.aggregation is a filter on a field named aggregation, and fails with INVALID_FILTER_FIELD when the type has none; buildQuery never builds the aggregate stage. Embedded .id paths use the embedded id rule.
buildFilterGroupMatch(group, gqltype, clauses, included, depth = 0)Low-level recursive filter compiler; mutates the supplied lookup accumulators. Use one included object per clauses array; callers must $unset the reserved __sf_lN aliases it adds.

The auth, validators, scalars, and plugins helper objects are shared by both database facades. The MongoDB facade retains its mcp compatibility namespace. PostgreSQL applications import MCP factories from the opt-in @simtlix/simfinity-mcp package; see the MCP API.

PostgreSQL initialization and transactions ​

javascript
import pg from 'pg';
import { createPostgres } from '@simtlix/simfinity-postgres';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const postgres = createPostgres({ pool, schema: 'series_api' });
postgres.connect(null, SerieType, 'serie', 'series');
const schema = postgres.createSchema();
await postgres.initializeDatabase({ mode: 'create' });

initializeDatabase({ mode: 'create' | 'validate' }) must be awaited after createSchema() and before operations are served. Create mode adds missing generated objects without altering incompatible existing objects; it locks no table when nothing is missing. Otherwise it locks the tables of roots whose keys it rebuilds, and its DDL locks the tables it changes and the existing tables that a new foreign key references. Validate mode performs no DDL. Both detect catalog drift and leave schema migrations to the operator. Other modes fail with INVALID_INITIALIZATION_MODE (400). After preventCreatingCollection(true), a missing or false mode validates and every other mode except validate fails with DATABASE_CREATION_DISABLED (409) instead. A rejected request leaves readiness as it was; a ready instance keeps serving while it is initialized again, and any failure of the initialization started last makes operations fail with DATABASE_NOT_INITIALIZED (503) until a later one succeeds. postgres.compileDatabaseSchema() returns the instance's DDL for review or export without touching storage, and postgres.describeDatabase() its physical description. The caller owns pool and closes it with await pool.end().

postgres.withTransaction(session, callback) joins an active supplied PostgreSQL session or owns a new repeatable-read transaction when session is null. The callback receives a PostgresSession with query(), inTransaction(), and a native client valid for the callback lifetime. Confirmed serialization failures and deadlocks are retried up to five times for owned transactions, each retry after a random delay of less than 10, 20, 40, 80 and 160 ms. Errors the callback throws propagate unchanged, and PostgreSQL errors are mapped as described in PostgreSQL database errors. A statement that fails aborts an owned transaction, even when the callback catches its error, and the call then fails with DATABASE_ERROR (500); roll back to a savepoint to continue after an expected error. See the PostgreSQL quick start for a complete server and native API example.

PostgreSQL options ​

createPostgres(options), postgresPlugin(options) and the module's configure(options) take pool, schema and these optional settings, bound once per runtime:

OptionDefaultEffect
multiRowInsertstrueThe owned rows of a write are inserted with multi-row INSERT statements of at most 32,767 values. false keeps one INSERT per row.
explicitNullsOrderfalseSorts on id and required root fields omit the NULLS clause, so indexes can serve them. true keeps the clause on every sort term, as 3.5.9 did.
refreshTriggers'perRow'For roots created while it is set, 'perRoot' refreshes the owned shape and unique keys of each changed root once per transaction instead of once per changed row, as long as the root's guard trigger stays deferred (the default).
ownerIndexes'all''minimal' leaves out the (__owner_id) index of new owned and guard tables, which their primary key or a unique index covers.

refreshTriggers and ownerIndexes change the generated description and DDL, also through the low-level describeDatabase(registrations, { schema, refreshTriggers, ownerIndexes }). An unknown value throws INVALID_DATABASE_SCHEMA (400) from createSchema() or describeDatabase(). Initialization keeps each existing root on the refresh-trigger layout its database holds and accepts both layouts. With ownerIndexes: 'minimal' it accepts existing owner indexes; a database created with 'minimal' fails validation under the default 'all' (SCHEMA_MISMATCH, index is missing), and create mode adds the indexes back. Versions up to 3.5.9 cannot validate databases created with the opt-in values; read the upgrade notes before enabling them.

Open source. Released under the Apache 2.0 License.