Gå til hovedinnhold
Mål

Når du er ferdig med dette kapitlet har du lært hvordan du legger til nye data i grafen, og hvilke vurderinger du må gjøre underveis. Du vil lære hvordan du finner ut hvilke tabeller og referanser mellom dem som er tilgjengelige i FS-plattformen, og hvordan du mapper til disse fra GraphQL-skjemaet.

Løse lesebehov i GraphQL

Lesebehov skal løses på en måte som gir et mest mulig brukervennlig API for klientene. Hva dette innebærer i praksis er noe vi fortsatt høster erfaringer med, og dette dokumentet vil dermed oppdateres etterhvert som vi lærer. Kapitlet kan leses som en sjekkliste, og for hvert punkt har vi konkrete eksempler som viser hvordan behovet kan løses på en god måte.

Måtehold er en dyd

Når du legger til nye data i grafen, er det fristende å legge til så mange felter, noder og kanter som mulig, slik at APIet blir mest mulig "komplett". Det er imidlertid gode grunner til å være litt tilbakeholden. Det er mye billigere å legge til felter i grafen enn å fjerne eller endre senere. Vi anbefaler derfor at du holder deg til data som er nødvendige for den arbeidsprosessen du jobber med, og heller venter med å legge til ytterligere felter, noder og kanter til behovet dukker opp i kontekst av andre arbeidsprosesser.

Kjerne-APIet viser hva som finnes i FS

Alle data vi tilgjengeliggjør via GraphQL hentes fra Kjerne-API-et. Les mer om Kjerne-APIet her.

1 Redigere GraphQL-skjemaet

For legge til data i GraphQL-APIet, må vi kunne redigere GraphQL-skjemaet. GraphQL-skjemaet ligger i FS-plattform-repoet under /fs-graphql-spec/src/main/resources/schema/. Du kan redigere skjemaet direkte på gitlab.sikt.no, eller via et utviklerverktøy som IntelliJ eller Microsoft Visual Studio Code.

Direktiver styrer kodegenereringen

For de fleste lesebehov kan implementasjonen genereres ut fra skjemaet. Vi bruker skjemadirektiver for å fortelle kodegeneratoren hvor dataene ligger i FS, og hvordan de er koblet sammen. Vi viser eksempler på bruk av direktivene under. Utfyllende beskrivelse av hvert direktiv finner du også øverst i skjemafila.

2 Utforsk: Finnes dataene i grafen fra før?

FS GraphQL API er allerede et rikt API som tilgjengeliggjør store mengder data. Før du går i gang med å utvide grafen, sjekk i Voyager om dataene allerede ligger der:

Hvis du finner dataene du vil ha i grafen, bruk gjerne GraphiQL for å sjekke at du får det utvalget du trenger (se Introduksjon til FS GraphQL API for konsumenter). Hvis du fikk tak i det utvalget du ville ha, gjenstår det bare å dokumentere i prosesskatalogen hvilke API-kall klienten skal gjøre, så er du i mål.

3 Legge til nye data i grafen

Hvis dataene ikke finnes i grafen fra før, må vi legge dem til. Dette gjør vi ved å legge til noder, subtyper og løvfelter i grafen.

Sett at vi skal lage en løsning for å registrere at semesteravgift er betalt ved en annen institusjon. Vi har gjort en analyse av denne arbeidsprosessen, og kommet fram til at klienten vår trenger tilgang til følgende data for en gitt termin:

  • Hvorvidt studenten har betalt semesteravgift
  • Hvorvidt studenten er semesterregistrert
  • Dato for betaling av semesteravgift
  • Hvilken institusjon semesteravgiften er betalt ved I tillegg trenger vi informasjon om hvilke institusjoner som er godkjent for å motta semesteravgift.
Navngiving av felter og typer

Vi har etablert noen prinsipper for navngiving av felter og typer i FS-plattformen. Disse finner du i Oppslagsboka: Navngiving av felter og objekter

Obligatoriske felter markeres med !

I eksemplene under vil du se at vi bruker utropstegn for å markere felter som alltid vil returnere data. Dette gir et enklere grensesnitt for klienten, fordi de slipper å implementere en null-sjekk i koden. Dette bruker vi typisk for felter som ikke kan ha nullverdi i databasen ("gule felter" i FS-klienten).

3.1 Legge til en ny node

For at klienten vår skal få tilgang til de relevante dataene, lager vi to nye typer. Først lager vi en type som vi velger å kalle Semesterregistrering, og som skal inneholde data om semesterregistrering og betaling av semesteravgift:

type Semesterregistrering {
}

Vi ønsker å gjøre Semesterregistrering til en node i grafen. Dette gjør vi ved å legge på implements Node:

type Semesterregistrering implements Node {
}

Alle noder i grafen må ha et ID-felt, og dette skal markert som obligatorisk:

type Semesterregistrering implements Node {
id: ID!
}

Deretter lager vi en node for institusjon:

type Institusjon implements Node {
id: ID!
}

Nodene peker ikke på tabeller i kjerne-APIet før vi setter på @table-direktivet. Det trengs for at implementasjonen skal kunne genereres automatisk. Hvis vi ønsker å bruke et annet navn enn det typen heter, kan vi peke kodegeneratoren i riktig retning ved å legge på argumentet name@table-direktivet. Her er et eksempel på en slik node:

type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
id: ID!
}

3.2 Legge til løvfelter

Nå kan vi legge til felter for dataene våre i de nye nodene:

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean!
betaltSemesteravgift: Boolean!
betaltSemesteravgiftDato: Date
}

type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean
aktiv: Boolean!
}

Datatyper

Her bruker vi datatypene Boolean og Date, i tillegg til ID. Du finner veiledning for å finne riktige datatyper i oppslagsboka: Datatyper i FS GraphQL API

Dersom løvfeltene har andre navn enn tilsvarende kolonner i kjerneapiet, bruker vi @field- direktiver for å peke kodegeneratoren i riktig retning:

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
}

type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

Hente inn løvfelter fra en annen tabell

Det er lov å legge til løvfelter fra en annen tabell en den som er definert for noden som helhet. Det krever imidlertid at det finnes en fremmednøkkel i kjerne-APIet fra nodens tabell til tabellen du jobber med. Her er et konkret eksempel:

type Fakturadetalj implements Node @table(name: "FAKTURARESKONTRODETALJ") {
id: ID!
navnAlleSprak: FakturadetaljnavnAlleSprak!
}

type FakturadetaljnavnAlleSprak {
nob: String! @field(name: "FAKTURADETALJTYPENAVN") @reference(table: "FAKTURADETALJTYPE", key: "FAKTURARESKONTRODETALJ__HAR__FAKTURADETALJTYPE__FK")
nno: String @field(name: "FAKTURADETALJTYPENAVN_NYNORSK") @reference(table: "FAKTURADETALJTYPE", key: "FAKTURARESKONTRODETALJ__HAR__FAKTURADETALJTYPE__FK")
eng: String @field(name: "FAKTURADETALJTYPENAVN_ENGELSK") @reference(table: "FAKTURADETALJTYPE", key: "FAKTURARESKONTRODETALJ__HAR__FAKTURADETALJTYPE__FK")
}

Som du ser, må vi i tillegg bruke @reference-direktivet med et table-argument og et key-argument for å få koblingen til å fungere.

Argumenter på løvfelter

GraphQL har mulighet for å legge til argumenter på løvfelter, for eksempel for å be om å få dataene tilbake på et bestemt format. Dette er foreløpig ikke i bruk i FS, men her er noen eksempler på mulige bruksområder:

  • Transformere en tekststreng i databasen til STORE BOKSTAVER
  • Runde av et tall til et gitt antall desimaler
  • Returnere telefonnummer med mellomrom mellom annethvert siffer

Se også GraphQL.org: Schemas and types – Arguments for flere relevante eksempler.

Teknisk oppgave

Siden vi ikke har brukt argumenter på denne måten før, er det ikke støtte i kodegeneratoren for slik funksjonalitet. Dersom du innfører argumenter på et løvfelt, må implementasjonen inntil videre gjøres manuelt.

3.3 Representere standardiserte datasett med enumerations

Dersom et felt har et begrenset antall mulige verdier, og lista over mulige verdier er stabil over tid, kan man legge til dataene som en enum. Dette innebærer at alle gyldige verdier blir listet opp i skjemaet:

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
}

enum Betalingsform {
BANK @field(name: "GIRO")
KORT @field(name: "KORT")
KONTANT @field(name: "KONTANT")
}

Dette er imidlertid bare én av flere strategier for slike tilfeller. Se oppslagsboka Kodetabeller i FS for mer informasjon.

3.4 Vi grupperer informasjon som hører sammen, i subtyper

Klienten trenger også navn på institusjonen. FS har institusjonsnavn på bokmål, nynorsk, engelsk og samisk. Siden disse navnene henger sammen grupperer vi dem ved å opprette en ny subtype koblet til noden Institusjon:

type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
navnAlleSprak: InstitusjonsnavnAlleSprak
}

type InstitusjonsnavnAlleSprak {
eng: String @field(name: "INSTITUSJONSNAVN_ENGELSK")
nob: String! @field(name: "INSTITUSJONSNAVN_BOKMAL")
nno: String @field(name: "INSTITUSJONSNAVN_NYNORSK")
sme: String @field(name: "INSTITUSJONSNAVN_SAMISK")
}
Flerspråklighet i FS-plattformen

Vi har inntil videre valgt denne modellen for håndtering av flerspråklighet i FS-plattformen. For mer informasjon, se oppslagsboka: Flerspråklighet.

Vi bruker @field-direktivet for å mappe kolonner i subtyper her på samme måte som for løvfelter (se over). På samme måte kan du bruke @table-direktivet for å mappe en subtype til en ny tabell, eventuelt sammen med @reference-direktivet. Se under punkt 4.2 under for mer informasjon om når og hvordan du bruker @reference-direktivet.

4 Nye veier gjennom grafen: Få tak i de riktige dataene

I eksempelet vårt har vi data i grafen, men det er ikke mulig å få tak i dem ennå. Vi må derfor lage en vei inn i grafen for å hente dataene. Vi har også data i casen vår som er tilgjengelig i grafen, men ikke i den konteksten vi trenger dem. Nedenfor skal vi se på hvordan vi legger opp veiene vi trenger.

4.1 Alle spørringer begynner i Query-noden

For å få tak i dataene i grafen, er det nyttig å gå tilbake til Voyager-grensesnittet og gjøre litt utforsking igjen. For at data skal være tilgjengelig i grafen, må det finnes en vei fra Query-noden og fram til der dataene bor.

For å få tilgang til noden "Semesterregistrering", må vi lage en slik vei. Vi begynner med å spørre oss selv i hvilken kontekst vi ønsker oss semesterregistreringer. I dette tilfellet finner vi at Query-noden allerede har et felt som heter "studenter", og som peker på noden "StudentVedInstitusjon".

4.2 Legge til kanter i grafen

Løsningen vår blir derfor å lage en kant fra "StudentVedInstitusjon" til "Semesterregistrering":

type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
id: ID!
semesterregistreringer: [Semesterregistrering!]!
}

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
}

enum Betalingsform {
BANK @field(name: "GIRO")
KORT @field(name: "KORT")
KONTANT @field(name: "KONTANT")
}

Hver student har mer enn én semesterregistrering. Derfor bruker vi [ og ] for å angi at det skal være en liste. Vi erfarer at det alltid er bedre å returnere en tom liste fremfor at listen ikke eksisterer (er null), og at det sjeldent gir mening at listen kan inneholde tomme (null) elementer. Dette håndhever vi ved å legge på to utropstegn: ett inne i klammeparantesene og ett bak.

Vi ser i kjerne-APIet at det bare finnes én fremmednøkkel mellom STUDENT og SEMESTERREGISTRERING. Derfor trenger vi ikke å angi noen eksplisitt referanse her. Vi imidlertid oppgi direktivet @splitQuery, for at at kodegeneratoren ikke skal bli forvirret. Dette er fordi fremmednøkkelen "SEMESTERREGISTRERING__HAR__STUDENT__FK" går fra SEMESTERREGISTRERING til STUDENT, altså motsatt vei av grafens retning.

Ikke bruk @splitQuery-direktivet i utide!

@splitQuery-direktivet er et verktøy for implementasjon, ikke for design. Vi skal derfor aldri sette dette som en del av en API-design-prosess. Det eneste unntaket er tilfeller der referansen mellom tabellene går "feil vei", slik som her.

Nå kan vi også koble sammen Semesterregistrering og Institusjon. Semesterregistrering har flere koblinger til institusjon, og vi må derfor oppgi hvilken fremmednøkkel kodegeneratoren skal følge. Dette gjør vi ved å legge til et @reference-direktiv på feltet betaltSemesteravgiftVedInstitusjon:

type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
id: ID!
semesterregistreringer: [Semesterregistrering!]! @splitQuery
}

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
betaltSemesteravgiftVedInstitusjon: Institusjon @reference(key: "SEMESTERREGISTRERING__BETAL_STED__INSTITUSJON__FK")
}

enum Betalingsform {
BANK @field(name: "GIRO")
KORT @field(name: "KORT")
KONTANT @field(name: "KONTANT")
}

type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
navnAlleSprak: InstitusjonsnavnAlleSprak
}

type InstitusjonsnavnAlleSprak {
eng: String @field(name: "INSTITUSJONSNAVN_ENGELSK")
nob: String! @field(name: "INSTITUSJONSNAVN_BOKMAL")
nno: String @field(name: "INSTITUSJONSNAVN_NYNORSK")
sme: String @field(name: "INSTITUSJONSNAVN_SAMISK")
}

Lage "snarveier" i grafen med via-argumentet i @reference-direktivet

Krever innsikt i FS

Denne teknikken krever litt utforskning av tilgjengelige nøkler i kjerne-APIet. Det er en fordel om du er godt kjent med hvordan tabeller i FS-databasen er koblet sammen.

I eksempelet vårt ønsker vi at noden Semesterregistrering også skal inneholde informasjon om hvilken termin dataene gjelder for. Dette krever en mer avansert bruk av @reference-direktivet, fordi det ikke finnes noen fremmednøkkel til TERMIN i SEMESTERREGISTRERING i kjerne-APIet. Vi finner imidlertid en fremmednøkkel TIL SEMESTERREGISTRERINGSTERMIN, som igjen har en fremmednøkkel til TERMIN. Det går altså en sti fra SEMESTERREGISTRERING til TERMIN via SEMESTERREGISTRERINGSTERMIN. Dette er tilstrekkelig til at kodegeneratoren kan generere riktig implementasjon.

Reference-direktivet tar inn et argument via. Verdiene for argumentet skal være en liste over objekter, der hvert objekt kan inneholde argumentene, table, key og condition.

type Semesterregistrering implements Node @table {
id: ID!
termin: Termin! @reference(via: [{table: "SEMESTERREGISTRERINGSTERMIN"}])
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
betaltSemesteravgiftVedInstitusjon: Institusjon
}

I dette tilfellet går relasjonen via bare én annen tabell. Det går også an å definere relasjoner som går via flere tabeller. Et eksempel er emnet for en vurderingsmelding. VURDERINGSMELDING har ingen fremmednøkkel til EMNE, men den har en fremmednøkkel til VURDERINGSENHET, som igjen har en fremmednøkkel til VURDERINGSKOMBINASJON, og sistnevnte har en fremmednøkkel til EMNE. Da kan vi gjøre det slik:

type Vurderingsmelding implements Node @table {
id: ID!
emne: Emne! @splitQuery @reference(via: [{table: "VURDERINGSENHET"}, {table: "VURDERINGSKOMBINASJON"}])
}

Hvis relasjonen ikke er entydig, det vil si at det finnes mer enn én fremmednøkkel mellom to tabeller i kjeden, bruker du key-argumentet for å angi hvilken fremmednøkkel kodegeneratoren skal følge. Ett eksempel er dersom vi ønsker å vise hvilken termin en gitt vurderingsenhet skal avvikles i. VURDERINGSENHET har ingen kobling til TERMIN, men den har to fremmednøkler til VURDERINGSTID, og denne har igjen en fremmednøkkel til TERMIN. Da må vi oppgi hvilken fremmednøkkel vi skal følge til vurderingstid:

type Vurderingsenhet implements Node @table {
id: ID!
avviklesITermin: Termin! @reference(via: [{table: "VURDERINGSTID" key: "VURDERINGSENHET__REELL__VURDERINGSTID__FK"}])
}

:::Alert Denne teknikken fungerer foreløpig bare for tilfeller der fremmednøklene går "riktig vei" I eksemplene over inneholder tabellen vi starter i, fremmednøkkelen som viser oss veien videre. Dersom kanten representerer en relasjon der fremmednøkkelen finnes i måltabellen, må du inntil videre bruke en condition i stedet for en fremmednøkkel. Støtte for fremmednøkler som går "feil vei" vil komme etterhvert. :::

Kanter som ikke følger fremmednøklene i database, filtrerte kanter

I noen tilfeller ønsker vi å lage kanter som ikke følger fremmednøklene i databasen. I disse tilfellene må vi innføre en condition i kjerne-APIet som definerer hvordan joinen skal gjøres. For eksempel kunne vi se for oss at vi ønsket å returnere faktura for semesteravgift i semesterregistreringstypen vår. Det finnes ingen kobling mellom SEMESTERREGISTRERING og FAKTURA, men FAKTURA har en kobling til TERMIN, så det er mulig å lage en join som henter fakturaer for samme student og termin som semesterregistreringen. I FS kan man opprette fakturaer også for andre formål enn semesteravgiften. Dersom vi ønsker det, kan vi skrive en condition som returnerer kun fakturaer som inneholder semesteravgift.

Se Legge til conditions i kjerneapiet for mer informasjon om hvordan du skriver selve conditionen, og gjør den tilgjengelig for Graphitron.

I skjemaet bruker vi @reference-direktivet for å referere til conditionen. Vi legger inn navnet vi har definert for condition-klassen vår i konfigurasjonen til Graphitron-maven-plugin, samt fullt kvalifisert navn på metoden vi skal bruke her:

type Semesterregistrering implements Node @table {
id: ID!
termin: Termin! @reference(via: [{table: "SEMESTERREGISTRERINGSTERMIN"}])
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
betaltSemesteravgiftVedInstitusjon: Institusjon
fakturaer: [Faktura!]! @reference(condition: {name: "CONDITION_SEMESTERREGISTRERING", method: "no.fellesstudentsystem.kjerneapi.conditions.semesterregistreringFakturaJoin"})
}

Conditions kan også inngå ved bruk av via-argumentet @reference-direktivet.

Paginerte kanter

I eksempelet vårt trenger klienten å hente en liste over institusjoner som kan motta semesteravgift. Dette fikser vi ved å legge til en kant på query-noden. Kanter på Query-noden skal alltid ha paginering. Kodegeneratoren tar seg av det praktiske her, så lenge vi gjør følgende:

  • Legge til argumentet "first: Int". På kanter på Query-noden bruker vi gjerne standardverdi 100, som betyr at de 100 første treffene blir returnert, med mindre klienten ber om noe annet
  • Legge til argumentet "after: String" Kanten skal gå mot en connection-type, som blir automatisk generert når vi setter på @connection-direktivet. Connection-typen navngir vi slik: [Navn på noden vi kommer fra][Navn på feltet vi kommer fra]Connection.
  • Legge til et @connection-direktiv med argumentet "for: "[Navn på noden vi skal til]"
type Query {
institusjoner(
first: Int = 100
after: String
): QueryInstitusjonerConnection @connection(for: "Institusjon")
}

type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

Det er mulig å legge paginering på lister hvor som helst i grafen, men foreløpig har vi bare lagt til denne funksjonaliteten på kanter på query-noden.

4.3 Legge til et argument på en kant

Vi har nå lagt til mulighet for å hente en liste av alle institusjoner. Til eksempelet vårt vil vi kun ha institusjoner som kan motta semesteravgift, og vi vil kun ha institusjoner som er aktive. Dette løser vi ved å legge til to nye argumenter på kanten:

type Query {
institusjoner(
first: Int = 100
after: String
kanMottaSemesteravgift: Boolean
aktiv: Boolean
): QueryInstitusjonConnection @connection(for: "Institusjon")
}
type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

Klienten kan nå spesifisere true for begge disse argumentene, og dermed få med seg bare aktive institusjoner som er godkjent for å motta semesteravgift. Når argumentene tilsvarer et felt som også finnes i noden vi spør etter, er det lurt å bruke samme feltnavn.

Vi bruker @field-direktivet for koble argumentene til riktig kolonne i kjerne-APIet:

type Query {
institusjoner(
first: Int = 100
after: String
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean @field(name: "STATUS_AKTIV")
): QueryInstitusjonConnection @connection(for: "Institusjon")
}
type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

Obligatoriske argumenter tvinger brukeren til å gjøre et valg

Det går an å gjøre argumenter obligatoriske, ved å markere feltet med et utropstegn:

type Query {
studenter(
eierInstitusjon: String!
)
}

Dette skal vi gjøre med forsiktighet, siden det da ikke vil være mulig å utføre spørringen uten å angi en verdi for argumentet.

Jobb bakoverkompatibelt

Å legge til et påkrevd filter, eller å gjøre et ikke-påkrevd filter påkrevd, er en bakover- inkompatibel endring, fordi spørringer som tidligere fungerte, ikke lenger vil fungere. Å gjøre et tidligere påkrevd filter ikke-påkrevd, er imidlertid bakoverkompatibelt, fordi alle eksisterende spørringer fortatt vil fungere.

Ikke glem eierInstitusjonsnummer

For kanter fra Query-noden som går mot en "VPD-tabell" i FS, må vi alltid legge til argumentet "eierInstitusjonsnummer: Int!". Med VPD-tabell mener vi en tabell der hver institusjon bare skal kunne se sine egne data. Hvis tabellen inneholder kolonnen INSTITUSJONSNR_EIER i kjerne-APIet, skal den ha dette argumentet.

Argumenter kan ha standardverdier

Det er også mulig å sette standardverdier for argumenter, som vil gjelde dersom brukeren ikke har gjort et valg.

I de fleste tilfeller gir det ikke mening å returnere inaktive institusjoner. Vi ønsker derfor å sette opp søket vårt slik at inaktive institusjoner blir returnert bare hvis klienten eksplisitt ber om det. Det kan vi løse ved å legge til en standardverdi på argumentet "aktiv":

type Query {
institusjoner(
first: Int = 100
after: String
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean = true @field(name: "STATUS_AKTIV")
): QueryInstitusjonConnection @connection(for: "Institusjon")
}
type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}
Standardverdier skal bare settes på argumenter som ikke er obligatoriske

Dersom vi markerer et argument som obligatorisk, gir det ikke mening å gi det en standardverdi. Standardverdier får bare effekt hvis klienten ikke sender noen verdi for argumentet. Hvis argumentet er obligatorisk, får ikke klienten sendt kallet sitt uten å sette en eksplisitt verdi.

Slik eksempelet ser ut nå, har klienten to alternativer:

  • aktiv: true, eller ingen verdi for "aktiv": Returnerer kun aktive institusjoner
  • aktiv: false: Returnerer kun inaktive institusjoner

En kant kan ta imot lister av argumenter

Hvis klienten fortsatt skal ha mulighet for å hente hele lista med aktive og inaktive institusjoner, må vi gjøre et litt mer avansert design. GraphQL støtter også lister av argumenter:

type Query {
institusjoner(
first: Int = 100
after: String
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: [Boolean] = true @field(name: "STATUS_AKTIV")
): QueryInstitusjonConnection @connection(for: "Institusjon")
}
type Institusjon implements Node @table {
id: ID!
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

Standardverdien gjør fortsatt at inaktive institusjoner skjules med mindre annet er spesifisert, men klienten har nå mulighet for å hente hele lista ved å spesifisere aktiv: [true, false], for å få hele settet med aktive og inaktive institusjoner.

Alle kanter kan ha argumenter

Det er ikke bare kanter på query-noden som kan ha argumenter. I eksempelet vårt ønsker vi oss kanskje mulighet til å hente kun semesterregistreringer der semesteravgift er registrert betalt. Da kan vi legge et argument på kanten fra StudentVedInstitusjon til Semesterregistrering:

type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
id: ID!
semesterregistreringer(
betaltSemesteravgift: Boolean @field(name: "STATUS_BET_OK")
): [Semesterregistrering!]! @splitQuery
}

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
betaltSemesteravgiftVedInstitusjon: Institusjon
}

Avansert filtrering: Enumerations og input-typer

I noen tilfeller trenger vi å filtrere på mer enn ett felt av gangen. I eksempelet vårt er vi mest interessert i semesterregistreringer fra inneværende termin, men vi er også åpne for at vi skal gjøre noen etterregistreringer på tidligere terminer.

En termin består av et årstall og en termintype. for å gruppere disse sammen, lager vi en input-type:

input SemesterregistreringsterminInput {
ar: Int!
termintype: Termintype!
}

Merk at Termintype er en enumeration:

input SemesterregistreringsterminInput {
ar: Int! @field(name: "ARSTALL")
termintype: Termintype!
}

enum Termintype {
VAR
SOM
HOST
}

Dette tvinger brukeren til å velge én av disse verdiene.

Vi mapper verdiene i inputtypen og Enum-verdiene med @field-direktiver på samme måte som for vanlige typer som brukes til å returnere data:

input SemesterregistreringsterminInput {
ar: Int! @field(name: "ARSTALL")
termintype: Termintype! @field(name: "TERMINKODE")
}

enum Termintype {
VAR @field(name: "VÅR")
SOM @field(name: "SOM")
HOST @field(name: "HØST")
}

Nå kan vi legge til denne input-typen som et argument på kanten:

type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
id: ID!
semesterregistreringer(
betaltSemesteravgift: Boolean @field(name: "STATUS_BET_OK")
terminer: [SemesterregistreringsterminInput!]
): [Semesterregistrering!]! @splitQuery
}

type Semesterregistrering implements Node @table {
id: ID!
registrert: Boolean! @field(name: "STATUS_REG_OK")
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
betalingsform: Betalingsform
betaltSemesteravgiftVedInstitusjon: Institusjon
}

Klienten kan nå velge å hente semesterregistreringer bare for et gitt sett av terminer. Vi gjør ikke argumetet obligatorisk, fordi vi tror det finnes andre mulige lesebehov der klienten har behov for hele settet med semesterregistreringer for en student. Av samme grunn setter vi heller ingen standardverdi for dette argumentet.

5 Spør mot flere noder samtidig: Union types og interfaces

GraphQL har mulighet for å legge til rette for spørringer som går mot flere typer i samme liste. Her finnes det to varianter. Union types gir en spørring som returnerer data fra helt forskjellige typer.

Interfaces gir mulighet for å returnere data fra forskjellige typer i samme liste, men til forskjell fra Union types, kan vi angi et sett med felter som alle typene i settet må implementere.

Vi har foreløpig ikke gode eksempler på bruk av disse mulighetene for å løse lesebehov i FS GraphQL API, så vi går ikke inn på dem her.

https://productionreadygraphql.com/blog/2020-05-05-all-about-graphql-abstract-types

6 Dokumentasjon i grafen

Du er ikke ferdig før du har dokumentert. Husk at hovedmålgruppen for APIet vårt er utviklere som ikke kan FS, og ikke kan studieadministrasjon. Alt må derfor beskrives på en utfyllende måte. Heldigvis har GraphQL god støtte for å legge inn beskrivelser av alle deler av skjemaet, slik at disse beskrivelsene blir synlig i dokumentasjonen som vises i mellom annet GraphiQL og Voyager.

type Query {
"""Returnerer en liste med institusjoner"""
institusjoner(
first: Int = 100
after: String
"""Filtrer på hvorvidt institusjonen er en gyldig mottaker av semesteravgift"""
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
"""Filtrer på hvrovidt institusjonen er aktiv. Standard er at kun aktive institusjoner returneres"""
aktiv: [Boolean] = true @field(name: "STATUS_AKTIV")
): QueryInstitusjonConnection @connection(for: "Institusjon")
}

"""Institusjon er det høyeste nivået i organisasjonsenhetshierarkiet i FS"""
type Institusjon implements Node @table {
id: ID!
"""Hvorvidt institusjonen er en gyldig betalingsmottaker for semesteravgift."""
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
"""Hvorvidt institusjonen er aktiv"""
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

"""Data som er tilknyttet en person i kraft av at hun er student ved en gitt institusjon"""
type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
"""Unik identifikator for studenten ved denne institusjonen"""
id: ID!
"""Studentens semesterregistreringer"""
semesterregistreringer(
"""Angi om du kun ønsker semesterregistreringer der semesteravgift er registrert betalt (eller der det er registrert fritak for betaling), eller om du også vil ha med semesterregistreringer uten registrert betaling (standardverdi er kun betalte)"""
betaltSemesteravgift: Boolean
"""Angi hvilke terminer du ønsker å se semesterregistreringer for"""
terminer: [SemesterregistreringsterminInput!]
): [Semesterregistrering!]! @splitQuery
}

"""Filtrering av semesterregistreringer"""
input SemesterregistreringsterminInput {
"""Hvilket årstall har terminen du ønsker du å filtrere på?"""
ar: Int! @field(name: "ARSTALL")
"""Hvilken termintype har terminen du ønsker å filtrere på?"""
termintype: Termintype! @field(name: "TERMINKODE")
}

"""Gyldige termintyper"""
enum Termintype {
"""Vår"""
VAR @field(name: "VÅR")
"""Sommer"""
SOM @field(name: "SOM")
"""Høst"""
HOST @field(name: "HØST")
}

"""En students semesterregistrering for en gitt termin. For mer informasjon, se dokumentasjon for [registerkort](/docs/begreper/registerkort)"""
type Semesterregistrering implements Node @table {
"""Unik identifikator for semesterregistreringen"""
id: ID!
"""Om studenten har fullført semesterregistreringen"""
registrert: Boolean! @field(name: "STATUS_REG_OK")
"""Om studenten har betalt semesteravgift"""
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
"""Dato for når semesteravgiften ble betalt"""
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
"""Hvordan studenten betalte semesteravgiften"""
betalingsform: Betalingsform
"""Institusjon semesteravgiften ble betalt ved"""
betaltSemesteravgiftVedInstitusjon: Institusjon
}

Domenebegreper bør også forklares mer i detalj begreps- og prosesskatalogen. Vi lenker til denne fra API-dokumentasjon når begrepet brukes. I dette eksempelet bruker vi begrepet semesteravgift og har lagt inn de nødvendige lenkene:

type Query {
"""Returnerer en liste med institusjoner"""
institusjoner(
first: Int = 100
after: String
"""Filtrer på hvorvidt institusjonen er en gyldig mottaker av [semesteravgift](../../../begreper/semesteravgift.md)"""
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
"""Filtrer på hvrovidt institusjonen er aktiv. Standard er at kun aktive institusjoner returneres"""
aktiv: [Boolean] = true @field(name: "STATUS_AKTIV")
): QueryInstitusjonConnection @connection(for: "Institusjon")
}

"""Institusjon er det høyeste nivået i organisasjonsenhetshierarkiet i FS"""
type Institusjon implements Node @table {
id: ID!
"""Hvorvidt institusjonen er en gyldig betalingsmottaker for [semesteravgift](../../../begreper/semesteravgift.md)."""
kanMottaSemesteravgift: Boolean @field(name: "STATUS_GODKJENT_BETSTED")
"""Hvorvidt institusjonen er aktiv"""
aktiv: Boolean! @field(name: "STATUS_AKTIV")
}

"""Data som er tilknyttet en person i kraft av at hun er student ved en gitt institusjon"""
type StudentVedInstitusjon implements Node @table(name: "STUDENT") {
"""Unik identifikator for studenten ved denne institusjonen"""
id: ID!
"""Studentens semesterregistreringer"""
semesterregistreringer(
"""Angi om du kun ønsker semesterregistreringer der [semesteravgift](../../../begreper/semesteravgift.md) er registrert betalt (eller der det er registrert fritak for betaling), eller om du også vil ha med semesterregistreringer uten registrert betaling (standardverdi er kun betalte)"""
betaltSemesteravgift: Boolean
"""Angi hvilke terminer du ønsker å se semesterregistreringer for"""
terminer: [SemesterregistreringsterminInput!]
): [Semesterregistrering!]! @splitQuery
}

"""Filtrering av semesterregistreringer"""
input SemesterregistreringsterminInput {
"""Hvilket årstall har terminen du ønsker du å filtrere på?"""
ar: Int! @field(name: "ARSTALL")
"""Hvilken termintype har terminen du ønsker å filtrere på?"""
termintype: Termintype! @field(name: "TERMINKODE")
}

"""Gyldige termintyper"""
enum Termintype {
"""Vår"""
VAR @field(name: "VÅR")
"""Sommer"""
SOM @field(name: "SOM")
"""Høst"""
HOST @field(name: "HØST")
}

"""En students semesterregistrering for en gitt termin. For mer informasjon, se dokumentasjon for [registerkort](/docs/begreper/registerkort)"""
type Semesterregistrering implements Node @table {
"""Unik identifikator for semesterregistreringen"""
id: ID!
"""Om studenten har fullført semesterregistreringen"""
registrert: Boolean! @field(name: "STATUS_REG_OK")
"""Om studenten har betalt [semesteravgift](../../../begreper/semesteravgift.md)"""
betaltSemesteravgift: Boolean! @field(name: "STATUS_BET_OK")
"""Dato for når [semesteravgiften](../../../begreper/semesteravgift.md) ble betalt"""
betaltSemesteravgiftDato: Date @field(name: "DATO_BETALING")
"""Hvordan studenten betalte [semesteravgiften](../../../begreper/semesteravgift.md)"""
betalingsform: Betalingsform
"""Institusjon [semesteravgiften](../../../begreper/semesteravgift.md) ble betalt ved"""
betaltSemesteravgiftVedInstitusjon: Institusjon
}