GraphQL API¶
Mosaic can serve an autogenerated GraphQL API alongside the REST API. The GraphQL schema — object types, enums, relationship fields, queries, and mutations — is generated at startup from your deployment's LinkML schema, so it always matches your data model. No per-entity GraphQL code is ever written.
Like the REST API, the GraphQL transport is a thin wrapper: every resolver
delegates to the same MosaicClient the SDK, REST, and TUI use, against the same
storage.
Installation¶
GraphQL support ships in the optional graphql extra:
(It is also included in datahelix-mosaic[all].)
Serving¶
mosaic serve --graphql # auto-detect config in the cwd
mosaic serve --graphql --config mosaic.yaml
mosaic serve --graphql --graphql-max-depth 6 # tighten the query-depth limit
The GraphQL endpoint mounts at http://host:port/graphql on the same server as
the REST routes. Opening it in a browser (GET) serves the GraphiQL IDE;
executing operations (POST) requires the same Authorization: Bearer <token>
header as the REST API.
The deployment must be schema-backed: your config's schema_path (or
--config) provides the LinkML schema the GraphQL surface is generated from.
Without one, --graphql fails at startup with a configuration error.
Embedding¶
from mosaic.core.factory import create_client_from_config, load_config_autodetect
from mosaic.serve import create_default_app
config = load_config_autodetect("mosaic.yaml")
client = create_client_from_config(config)
app = create_default_app(client, graphql=True, graphql_max_query_depth=10)
What gets generated¶
For each concrete entity class in your schema (say Sample), Mosaic generates:
| Surface | Example |
|---|---|
| Object type | type Sample with every slot as a typed field |
| Page type | type SamplePage { items, total, limit, offset } |
| Input types | input SampleCreateInput, input SampleUpdateInput |
| Queries | sample(id), samples(filters, filterMode, limit, offset), searchSamples(q, limit, offset) |
| Mutations | createSample, updateSample, setSampleAvailability, setSampleAvailabilityBulk, supersedeSample |
LinkML enums become GraphQL enums. Field names are camelCased
(volume_ml → volumeMl); write payloads are mapped back to slot names before
they reach the SDK.
A list query's filters argument accepts either spelling for its field —
the LinkML slot name (volume_ml, as listed by hippoSchema) or the camelCased
field name the type exposes (volumeMl). A field that matches neither is an
error (extensions.code: UNKNOWN_FILTER_FIELD) rather than an empty page, as
are the two kinds of name that look filterable but cannot be: read-time
computed fields (createdAt, updatedAt, supersededBy — for temporal
queries use asOf) and multivalued references, which are stored as
relationship edges rather than columns (use relatedTo). Both report
extensions.code: UNFILTERABLE_FIELD.
The exposed class set is decided by Mosaic's shared type model
(mosaic.core.schema_typing) — the same model behind the typed Python SDK — so
the two surfaces never drift. Framework classes (Entity, ProvenanceRecord,
Process, Validator, ReferenceLoader) are never exposed; ExternalID is.
Relationships traverse the graph¶
A class-ranged slot generates two fields: the raw stored UUID and a resolved field that walks the relationship.
{
samples {
items {
name
donorId # raw stored UUID
donor { # resolved Donor — one batched query, no N+1
name
sex
}
}
}
}
Resolved fields load through a per-request DataLoader: resolving the same target type N times in one request issues a single batched query.
System and temporal fields¶
Every entity type carries id and isAvailable (stored), plus the read-only
computed fields version, createdAt, updatedAt, schemaVersion,
createdBy, updatedBy, and supersededBy — derived from the provenance log
at read time, never stored on entity rows.
Lifecycle, not deletion¶
There are no delete mutations. Mosaic never hard-deletes; use availability transitions:
mutation {
setSampleAvailability(id: "…", isAvailable: false, reason: "depleted") {
entityId
isAvailable
}
}
mutation {
setSampleAvailabilityBulk(ids: ["…", "…"], isAvailable: false, reason: "audit") {
total succeeded failed
failures { entityId error }
}
}
The bulk form mirrors the REST bulk-availability endpoint: per-record error isolation — one bad id never rolls back its siblings.
Supersession works the same way as in the SDK, with a read-side query for the chain:
mutation { supersedeSample(id: "old", replacementId: "new", reason: "remeasured") { entityId supersededBy } }
query { supersededBy(id: "old") { entityId supersededBy chain } }
Full-text search¶
Slots annotated hippo_search in your schema are searchable per type:
Schema introspection (the Mosaic kind)¶
Beyond GraphQL's own __schema, the API exposes Mosaic's domain type model —
useful for generic clients that need the LinkML classification:
{
hippoSchema { name accessorName }
hippoEntityType(name: "Sample") {
fields { name kind range role required multivalued targetEntityType enumValues }
relationships { field targetEntityType }
}
}
Errors¶
SDK errors surface as structured GraphQL errors with a machine-readable
extensions.code: VALIDATION_FAILED (with the tier-tagged failure envelope
when available), NOT_FOUND, ALREADY_SUPERSEDED,
AVAILABILITY_CHANGE_FAILED, or INTERNAL_ERROR.
Hardening¶
Queries nesting deeper than the configured depth limit (default 10) are
rejected before execution — relationship fields make unbounded traversals
expressible, so the cap is the transport's recursion guard. Introspection is
exempt, so GraphiQL works regardless. Configure with
--graphql-max-depth / max_query_depth.
Known limitations¶
- Offset pagination only — cursor pagination follows the REST roadmap.
- No schema versioning — the GraphQL schema regenerates from your LinkML schema at startup; breaking LinkML changes break the GraphQL contract the same way they break the typed SDK.
- Equality filters only (
filters+ AND/OR composition), mirroringMosaicClient.query. - Updates cannot null-out a field — omitted and
nullinput fields are both dropped from the patch.