Skip to content

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

Extensions ​

Simfinity reads standard GraphQL extensions metadata when building models, input types, and resolvers. Define field extensions on individual field configurations and type extensions on GraphQLObjectType.

Field extensions ​

ExtensionValueEffect
relationRelationship configurationDeclares how an object or object collection is stored and resolved.
readOnlyBooleanOmits the field from generated create and update inputs.
queryableBooleanfalse rejects client filter, sort and aggregation paths through the field; true allows them for a field with its own resolve.
uniqueBooleanAdds a backend uniqueness structure for supported fields.
validationsOperation-keyed validatorsRuns field validation during materialization.
stateMachineBoolean, generatedMarks a state field managed by a connected state machine.

relation ​

javascript
const field = {
  type: CountryType,
  extensions: {
    relation: {
      embedded: false,
      connectionField: 'country',
      displayField: 'name',
    },
  },
};
PropertyDescription
embeddedtrue gives the value parent-owned storage: inline on MongoDB, JSONB or owned tables on PostgreSQL; false uses referenced records.
connectionFieldFor a reference, the stored ObjectId/UUID field; for a reverse collection, the required child field linking back to the parent.
displayFieldDescriptive field name exposed through introspection for client tooling.

For a single-object reference, connectionField defaults to the GraphQL field name for model generation, reads, writes, and explicit-null clears. Set it to use a different stored reference field. On MongoDB, a reference cannot be stored under constructor, which Mongoose drops: createSchema() rejects it with INVALID_MODEL (400), and a collection must not use it either; see fields named like Object.prototype members.

For referenced collections, connectionField is required: name the child's back-reference, either the child's single reference field or its stored field name. When it is missing or an empty string, createSchema() throws INVALID_MODEL (400, Type.field requires a child connectionField) on every adapter. In default MongoDB mode and with custom adapters only, a collection with its own resolver, or marked readOnly, is exempt: it builds with a Configuration issue warning, or silently when it is readOnly and has its own resolver, and nested writes through it fail with INVALID_MODEL (500). PostgreSQL and referentialIntegrity: 'transactional' always require it. When the child declares no field for the back-reference, MongoDB stores the link in a private ObjectId field of the generated child model, also when the child has a collection with the same connectionField, as in chained or self-referencing collections. PostgreSQL uses the metadata to create real FKs for single, inverse, explicit-link, and embedded references.

Scalar lists do not need relationship metadata. Object fields and object lists do. Embedded self-references are rejected by model generation. See relationships for complete forward and reverse examples.

readOnly ​

javascript
createdBy: {
  type: GraphQLString,
  extensions: { readOnly: true },
}

The field remains in the output type and generated model. Assign it in a controller when it is server-owned. readOnly is an input-generation setting, not output authorization or a database-level immutability constraint.

queryable ​

javascript
fields: () => ({
  internalCode: {
    type: GraphQLString,
    extensions: { queryable: false },
  },
  title: {
    type: GraphQLString,
    extensions: { queryable: true },
    resolve: (parent) => parent.title,
  },
}),

Client filter, sort, groupId and fact paths compare stored values, not resolver output. A field with its own resolve, such as a masking resolver, a manual relationship resolver or a custom id resolver, is therefore non-queryable unless it sets queryable: true; only the resolvers Simfinity generates keep a field queryable. Set queryable: true when the resolver returns the stored value, and queryable: false to make any field non-queryable.

A client path with a non-queryable segment fails with FORBIDDEN_FILTER_PATH (403), although the generated filter argument remains in the schema. Paths added by middleware or scope functions are not checked. queryable: true does not bypass authorization rules or the find scopes of related types, and introspection does not expose it. See path restrictions.

unique ​

javascript
slug: {
  type: GraphQLString,
  extensions: { unique: true },
}

Uniqueness is a database constraint, not a pre-save validator. The MongoDB generator creates indexes for supported string, enum, and numeric mappings. PostgreSQL supports all mapped native scalar types, single references, scalar lists, and scalar/reference fields inside embedded trees. Root unique indexes use NULLS NOT DISTINCT; embedded and multikey declarations use typed owner-key tables so duplicate values within one root are allowed but another root cannot claim the same key. Whole embedded-object uniqueness is rejected. Manage existing duplicates and schema evolution through an explicit database process.

validations ​

javascript
title: {
  type: new GraphQLNonNull(GraphQLString),
  extensions: {
    validations: simfinity.validators.stringLength('Title', 2, 120),
  },
}

The field-level shape is { CREATE: [validator], UPDATE: [validator] }. Each validator exposes validate(typeName, fieldName, value, session). Async validators are awaited. See validation for helpers and custom rules.

Type extensions ​

ExtensionValueEffect
validations{ CREATE, UPDATE } validator arraysValidates the full input and materialized model after field validation.
scopePlain object with optional find, get_by_id and aggregate functionsAdds filters to generated root reads. Any other shape throws INVALID_SCOPE.
indexes{ fields: string[], unique?: boolean }[]Declares PostgreSQL composite indexes over stored scalar/reference fields; MongoDB uses its model index configuration.

Type validators receive (typeName, args, modelArgs, session). Scope callbacks receive { type, args, operation, context } and mutate args in place. Omit a key to leave an operation unscoped; misspelled keys, non-function values, arrays and scope: undefined or null throw INVALID_SCOPE (500) at registration and createSchema(), and on reads if the scope is changed later. A type's find scope also restricts the referenced records that client filter, sort and aggregation paths reach. See query scope for supported operations, relationship paths and their authorization boundaries.

Automatic MongoDB indexes ​

When generating a Mongoose model, the MongoDB adapter registers ascending indexes for direct GraphQLID fields, stored single-object references, and ObjectId leaves inside embedded objects and arrays of embedded objects. For example, a direct countryId field gets { countryId: 1 }; the same leaf inside address gets { 'address.countryId': 1 }. Nested embedded paths are traversed recursively.

References use their stored connectionField name, or the GraphQL field name when it is omitted. The adapter also indexes private child connection fields that it generates for inverse collections. These are reference indexes; MongoDB does not enforce PostgreSQL-style foreign keys.

Inspect generated declarations after registering your types and calling createSchema():

javascript
const model = simfinity.getModel(SerieType);
const indexes = model.schema.indexes();

Generated models also declare { id: 1 } on the top-level id field. The record key is _id, and generated reads, filters, sorts, aggregations, relation lookups and MCP tools never use that index. To stop declaring it for a connect() type, remove it in onModelCreated, the sixth argument of connect():

javascript
simfinity.connect(null, SerieType, 'serie', 'series', controller, (model) => model.schema.removeIndex({ id: 1 }));

Types registered with addNoEndpointType() and generated supporting models have no onModelCreated. Mongoose autoIndex builds the index at startup on any process whose schema still declares it, so drop an existing one with db.getCollection('Serie').dropIndex('id_1') only once every process runs with the recipe, and expect syncIndexes() to drop it as soon as the recipe is in place. Keep the index when your code queries the stored id, or when a link is stored under id, as with connectionField: 'id'. Version 4.0 plans to stop declaring it by default and to remove the stored id path and the duplicate $set.id that generated updates write.

This automatic traversal does not add indexes for arbitrary string/number fields or direct lists of scalar IDs. Supported unique flags are handled separately as described above. Supplied Mongoose models keep their own schemas and index definitions; use their native configuration for additional or composite indexes.

PostgreSQL generates indexes and real foreign keys from its storage description instead. See PostgreSQL storage and constraints for its index declarations and initialization contract.

Introspection metadata ​

Simfinity extends GraphQL's __Field introspection type with extensions. Clients can inspect readOnly, stateMachine, and relation metadata:

graphql
query SerieMetadata {
  __type(name: "Serie") {
    fields {
      name
      extensions {
        readOnly
        stateMachine
        relation {
          embedded
          connectionField
          displayField
        }
      }
    }
  }
}

This is a Simfinity introspection extension, not a field in standard GraphQL introspection. It does not expose arbitrary extensions such as validator functions. Avoid schema-cloning middleware; use Envelop plugins and in-place resolver wrapping.

Initialization supports GraphQL fields that have already been materialized and schemas that already exist. Repeated evaluation of Simfinity with the same GraphQL peer preserves the existing extension field and metadata type identities, with one exception: when the first Simfinity schema of the process has an application type named FieldExtensionsType or RelationType, Simfinity replaces the metadata types once with renamed ones, described below, and every evaluation follows the shared extension field to them. Your application field's extensions metadata remains intact.

The extension is global to that GraphQL peer, including unrelated schemas. Simfinity type and middleware registries belong to each runtime instance. Schemas constructed after import register FieldExtensionsType and RelationType in their type maps. Schemas constructed before import keep their original type maps: ordinary operations and direct selections such as the query above work, but named fragments on these metadata types require a schema constructed after import.

When the first Simfinity schema of a process has an application type named FieldExtensionsType or RelationType, Simfinity names its metadata types SimfinityFieldExtensionsType and SimfinityRelationType instead, with a number appended when those names are taken too, in every schema the process constructs from then on, Simfinity's or not, and logs a Configuration issue warning. Selections by field name, such as the query above, are unaffected; named fragments and permission maps use the new names. The first Simfinity schema of the process fixes the names, so a later schema with an application type of a current name fails with RESERVED_TYPE_NAME. A schema constructed before the rename keeps the old types, like a schema constructed before import. See what gets generated.

In a process that has imported Simfinity, passing a post-import schema's introspection result to buildClientSchema() can still fail with duplicate FieldExtensionsType names, or duplicates of whichever metadata names are current. This is the same retained global-type limitation that affects schema-cloning middleware; import-order safety does not make schema cloning supported.

Open source. Released under the Apache 2.0 License.