GraphQL Schema
This page documents the conventions every SwissMentor GraphQL endpoint follows. Once you understand them you can navigate the full schema without further guidance.
The complete, type-by-type listing — every query, mutation, object, input, enum and scalar — is in the Schema Reference.
Getting the Schema
Abschnitt betitelt „Getting the Schema“The live schema is always available through introspection from any GraphQL IDE (Apollo Sandbox, Altair, GraphiQL) pointed at your SwissMentor endpoint. Introspection always reflects the current server, so it is the authoritative source for the exact fields available to you.
Core Entity Model
Abschnitt betitelt „Core Entity Model“Every domain entity (Person, Course, Document, Registration, …) extends a base SMObject. This base carries the identity, type, status, validity window and ACL bindings.
type SMObject { "Stable UID — use this everywhere in the API" id: ID!
"Short name shown in lists and pickers" name: String!
"Object type code, e.g. 'PERSON', 'COURSE', 'DOCUMENT'" objectType: String!
"System classification; coarser than objectType" sysType: SysType!
"Status (active / inactive / archived / …)" statusID: Int!
"Validity window — object is hidden outside these dates" validFrom: Date validTo: Date
"Owner — typically a person responsible for the record" owner: Person
remarks: String customFields: [CustomField!]}
enum SysType { Person Group Course CourseParticipant CourseRegistration Document Company Folder # ... several dozen more}A Person, Course or CourseParticipant is always reachable through its .object field on the entity, and every SMObject can be narrowed back to its specific entity via .person, .course, .courseParticipant, …
IDs: only UID
Abschnitt betitelt „IDs: only UID“The public API exclusively exposes the UID scalar (a GUID string). Internal integer IDs (personID, courseID, objectID) are never returned through GraphQL, even though they exist in the database.
Consequences:
- Client code stores and compares
UIDstrings. parentIDand other cross-entity references also take aUID.- Some resolvers accept a few special UID forms, e.g.
special://PERSONFLDROOTto target the default person folder.
Queries
Abschnitt betitelt „Queries“Single-item query
Abschnitt betitelt „Single-item query“Singular, named after the entity, takes id: ID!.
query { person(id: "8f3a1a8e-...-1c") { id lastName preName }}List query
Abschnitt betitelt „List query“Plural, takes one options argument of type <Entity>ListOptions. All list-options types share the same base interface:
input <Entity>ListOptions { id: UID # restrict to a single object ids: [UID!] # restrict to a set parentID: UID # children of this parent recursiveLevel: Int # walk N folder levels deep searchTerm: String # full-text search (name, code, …) sysType: SysType sysTypes: [SysType!] onlyActive: Boolean statusAll: Boolean statusIncludes: [Int!] statusExcludes: [Int!] orderBy: String orderDirection: OrderDirection # ASC / DESC limit: Int offset: Int}List queries return a result wrapper:
type <Entity>Results { totalCount: Int! nodes: [<Entity>!]!}Example:
query { persons(options: { searchTerm: "Müller", onlyActive: true, limit: 25 }) { totalCount nodes { id lastName preName } }}Mutations
Abschnitt betitelt „Mutations“CRUD mutations
Abschnitt betitelt „CRUD mutations“createBaseResolver emits a standard set of mutations for every base object:
type Mutation { createPerson(person: PersonCreateInput!, parentID: ID, objectType: String!): CreatePersonResult! updatePerson(id: ID!, person: PersonUpdateInput!): UpdatePersonResult! updatePersonMultiple(ids: [ID!]!, person: PersonUpdateInput!): [UpdatePersonResult!]! deletePerson(id: ID!): DeleteResult!}parentIDsubordinates the new object under an existing one; ACL is checked against it.objectTypefalls back to the entity default when omitted.
Result wrapper
Abschnitt betitelt „Result wrapper“Every mutation returns a structured result — not the entity directly:
type CreatePersonResult { status: OperationResultStatus! # SUCCESS, WARNING, ERROR, DENIED message: String id: ID person: Person errorType: String # programmatic error code (e.g. 'PersonNumberDuplicateError') errorData: JSON # structured data for the error}Always branch on status first, then show message to the user. Use errorType / errorData for programmatic handling (retry, linking to the conflicting record, …).
Input inheritance
Abschnitt betitelt „Input inheritance“<Entity>CreateInput extends <Entity>UpdateInput, which extends BaseObjectInput. That means every create input inherits:
parentID,objectType,remarks,code,importIDownerID,validFrom,validTocustomFields
plus all entity-specific fields.
Custom Fields
Abschnitt betitelt „Custom Fields“Entities that declare customFieldsEnabled = true accept arbitrary customer-defined fields through the shared CustomFieldInput:
input CustomFieldInput { customFieldID: UID! value: String}On the read side, custom fields appear under entity.customFields with their configured types already parsed (date, number, select values, …).
Subscriptions
Abschnitt betitelt „Subscriptions“Real-time updates are available for high-signal events. They are push-based and use the same auth as queries.
type Subscription { objectChanged(objectID: ID!): ObjectChangeEvent! registrationCreated(courseID: ID!): CourseRegistration!}Subscriptions are optional — polling a list query at a sensible interval is often simpler for integrations.
Error Handling
Abschnitt betitelt „Error Handling“Two layers of errors:
- Transport-level (HTTP 4xx/5xx, malformed GraphQL): returned in the top-level
errorsarray. - Domain-level (validation failures, ACL denials, conflicts): returned inside the mutation’s result wrapper via
status,message,errorType.
{ "data": { "createPerson": { "status": "ERROR", "message": "Die Personennummer \"1234\" ist bereits vergeben.", "errorType": "PersonNumberDuplicateError", "errorData": { "personNumber": "1234", "existingPersonID": "8f3a..." } } }}Authorization
Abschnitt betitelt „Authorization“- Every resolver is
@Authorizedby default — requests without a valid JWT return HTTP 401. - A small, curated set is annotated
@Authorized(auth('Public'))and accepts anonymous calls:login,systemInfo,publicEnrollToCourse, a handful of OTP/password-reset endpoints. - ACL is enforced inside the repository layer, not just the resolver.
status: "DENIED"is the standard response for insufficient rights.
Conventions Cheat Sheet
Abschnitt betitelt „Conventions Cheat Sheet“| Concept | Pattern |
|---|---|
| Public ID type | UID scalar (GUID) |
| List query | <entity>s(options: <Entity>ListOptions) |
| Single query | <entity>(id: ID!) |
| Create mutation | create<Entity>(<entity>: <Entity>CreateInput!, ...) |
| Update mutation | update<Entity>(id: ID!, <entity>: <Entity>UpdateInput!) |
| Delete mutation | delete<Entity>(id: ID!) |
| Mutation result | { status, message, id, <entity>, errorType, errorData } |
| Auth default | @Authorized() — JWT required |
| Public auth | @Authorized(auth('Public')) |
When in doubt, open Apollo Sandbox against your endpoint or browse the Schema Reference — both always reflect the current schema.