Lesen und Schreiben von Daten mit GraphQL in Fabric Apps

Fabric Apps bietet einen typsicheren GraphQL-Client, mit dem Sie Erstellungs-, Lese-, Aktualisierungs- und Löschvorgänge ausführen können, ohne unformatierte Abfragen zu schreiben. Der Client generiert GraphQL automatisch aus Ihren Methodenaufrufen und gibt typierte Entitäten basierend auf Ihren Datenmodelldefinitionen zurück.

Voraussetzungen

  • Ein Fabric-Apps-Projekt mit definierten Datenmodellen. Siehe Definieren von Datenmodellen.
  • Die Back-End-Dienste, die lokal ausgeführt oder für Fabric bereitgestellt werden.

Initialisieren des Clients

Instanziieren Sie RayfinClient mit Ihrer Backend-URL, dem veröffentlichbaren Schlüssel und dem Schematyp:

import { RayfinClient } from '@microsoft/rayfin-client';
import type { UniversalAppSchema } from '@rayfin-app/shared';

const client = new RayfinClient<UniversalAppSchema>({
  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});

Das generische Typargument ermöglicht TypeScript die Bereitstellung von AutoVervollständigen und Typüberprüfung für alle Datenvorgänge.

Diese Beispiele gehen davon aus, dass du Data aktiviert sowie Note und Notebook in der packages/data/src/index.ts der Universal App und im freigegebenen Schemavertrag registriert hast. In einer flachen Vorlage importiere stattdessen deinen Schema-Typ von rayfin/data/schema.ts. Siehe Definieren von Datenmodellen.

Daten lesen

Zugreifen auf Entitätssammlungen über client.data.<EntityName>. Die Fluent-API stellt Methoden zum Abfragen, Filtern, Sortieren und Paginieren bereit.

Hol eine Seite mit Datensätzen

execute() gibt standardmäßig nur eine Seite mit 100 Datensätzen zurück. Es gibt keine Cursor-Metadaten zurück und ruft keine weiteren Seiten ab. Verwenden Sie Vorwärtsnavigation bei der Seitennummerierung für Listen, die über eine Seite hinausgehen können.

const notes = await client.data.Note.select([
  'id',
  'title',
  'content',
  'createdAt',
  'isPinned',
]).execute();

Einen einzelnen Datensatz anhand des Primärschlüssels abrufen

const note = await client.data.Note.findById('00000000-0000-0000-0000-000000000000');

findById() gibt null zurück, wenn kein Eintrag existiert. Die veröffentlichte Implementierung wählt nur den Primärschlüssel aus, also sollte man nicht davon ausgehen, dass andere Entitätsfelder ausgefüllt sind. Wähle Felder explizit aus, wenn du ihre Werte brauchst:

const notes = await client.data.Note.select(['id', 'title', 'content'])
  .where({ id: { eq: '00000000-0000-0000-0000-000000000000' } })
  .first(1)
  .execute();

const note = notes[0] ?? null;

Datensätze filtern

Verwenden Sie die Methode where(), um Ergebnisse zu filtern:

const pinnedNotes = await client.data.Note.select([
  'id',
  'title',
  'isPinned',
])
  .where({ isPinned: { eq: true } })
  .execute();

Filteroperatoren

Operator Description Example
eq Gleich { status: { eq: 'active' } }
neq Ungleich { status: { neq: 'archived' } }
gt Größer als { age: { gt: 18 } }
gte Größer als oder gleich { age: { gte: 21 } }
lt Kleiner als { price: { lt: 100 } }
lte Kleiner oder gleich { price: { lte: 50 } }
contains Enthält Teilzeichenfolge { title: { contains: 'draft' } }

Ergebnisse sortieren

Verwenden Sie orderBy(), um Abfrageergebnisse zu sortieren:

const notes = await client.data.Note.select([
  'id',
  'title',
  'createdAt',
])
  .orderBy({ createdAt: 'desc' })
  .execute();

Sortieren nach mehreren Spalten:

const notes = await client.data.Note.select([
  'id',
  'title',
  'isPinned',
  'createdAt',
])
  .orderBy({ isPinned: 'desc' })
  .orderBy({ createdAt: 'desc' })
  .execute();

Wenn Sie Beziehungen mit den Dekoratoren @one() und @many() definieren, können Sie Felder verknüpfter Entitäten in dieselbe Abfrage einbeziehen:

const notes = await client.data.Note.select([
  'id',
  'title',
  'content',
  'notebook.id',
  'notebook.name',
])
  .execute();

Jede Notiz enthält die zugehörigen Notizbuchdaten, ohne dass eine separate Abfrage erforderlich ist.

Paginierung großer Ergebnismengen

Verwenden Sie cursorbasierte Paginierung für große Listen:

const page = await client.data.Note.select([
  'id',
  'title',
  'createdAt',
])
  .orderBy({ createdAt: 'desc' })
  .orderBy({ id: 'asc' })
  .first(25)
  .executePaginated();

console.log('Items:', page.items);
console.log('Has next page:', page.hasNextPage);
console.log('End cursor:', page.endCursor);

Rufen Sie die nächste Seite mit dem Cursor ab:

if (page.hasNextPage) {
  if (!page.endCursor) {
    throw new Error('The next page is missing its cursor.');
  }
  const nextPage = await client.data.Note.select([
    'id',
    'title',
    'createdAt',
  ])
    .orderBy({ createdAt: 'desc' })
    .orderBy({ id: 'asc' })
    .first(25)
    .after(page.endCursor)
    .executePaginated();
}

Hinweis

Das Backend füllt die totalCount Eigenschaft auf dem Typ PagedResult nicht aus. Der Wert page.items.length zählt nur die aktuelle Seite, nicht alle übereinstimmenden Datensätze.

Um jeden passenden Datensatz zu verarbeiten, fahren Sie fort, bis hasNextPagefalse ist. Behalten Sie Auswahlen, Filter und Sortierreihenfolgen über mehrere Anfragen hinweg unverändert.

let cursor: string | undefined;
let matchingRecords = 0;

while (true) {
  let query = client.data.Note.select(['id'])
    .where({ isArchived: { eq: false } })
    .orderBy({ id: 'asc' })
    .first(100);

  if (cursor !== undefined) {
    query = query.after(cursor);
  }

  const page = await query.executePaginated();
  matchingRecords += page.items.length;

  if (!page.hasNextPage) {
    break;
  }
  if (!page.endCursor) {
    throw new Error('The next page is missing its cursor.');
  }
  cursor = page.endCursor;
}

console.log('Records read across all pages:', matchingRecords);

Das Paging liefert keinen Snapshot über mehrere Anfragen hinweg. Gleichzeitige Änderungen können die Datensätze beeinflussen, die du liest und zählst.

Daten aggregieren und gruppieren

Verwenden Sie aggregate() für serverseitige Gesamtsummen und groupBy() für Zusammenfassungen pro Gruppe. Die eigenständige Fluent-Methode count() ist nicht verfügbar, aber count wird in einer Aggregationsspezifikation unterstützt.

Alle fünf aggregierten Operationen (sum, avg, min, max und count) erfordern ein numerisches Feld. Zähle weder eine UUID id noch ein Textfeld dazu. Zum Beispiel nimmt die folgende Abfrage eine registrierte Product Entität mit einem erforderlichen Zahlenfeld stockQuantity an:

const totals = await client.data.Product.where({ isAvailable: { eq: true } })
  .aggregate({
    matchingProducts: { count: 'stockQuantity' },
    totalStock: { sum: 'stockQuantity' },
    averageStock: { avg: 'stockQuantity' },
    lowestStock: { min: 'stockQuantity' },
    highestStock: { max: 'stockQuantity' },
  })
  .execute();

console.log(totals[0].aggregations.matchingProducts);

count zählt nicht-null Werte des angegebenen numerischen Feldes. Verwenden Sie ein erforderliches Zahlenfeld, um passende Zeilen zu zählen. Wenn Ihre Entität kein geeignetes Feld hat, zählen Sie die Identifikatoren auf allen Seiten wie zuvor gezeigt, nicht nur auf einer Seite.

Für gruppierte Gesamtwerte:

const groups = await client.data.Product.groupBy(['status'])
  .aggregate({
    matchingProducts: { count: 'stockQuantity' },
    totalStock: { sum: 'stockQuantity' },
  })
  .execute();

for (const row of groups) {
  console.log(row.fields.status, row.aggregations.totalStock);
}

Aggregationsergebnisse sind Arrays mit fields und aggregations Objekten. Ein Gesamtergebnis hat eine Zeile mit einem leeren fields Objekt. count gibt number zurück; die anderen Operationen geben für leere oder komplett null Eingaben number | null zurück.

Wenden Sie where() vor der Aggregation an. Kombinieren Sie keine Aggregation mit Zeilenauswahl, Sortierung oder Paginierung über select(), orderBy(), first() oder after().

Datensätze erstellen

Verwenden Sie die create() Methode, um neue Datensätze einzufügen:

const newNote = await client.data.Note.create({
  title: 'Meeting notes',
  content: 'Discussion points from the team sync',
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
  user_id: 'user-123',
});

Die Methode liefert den erstellten Datensatz zurück, einschließlich seiner automatisch generierten id. Verwenden Sie einen explizit ausgewählten Lesevorgang, wenn Sie zusätzliche gespeicherte Felder benötigen.

Erstellen von Datensätzen mit Beziehungen

Übergeben Sie beim Erstellen von Entitäten mit Beziehungen entweder das vollständige verknüpfte Objekt oder ein Objekt mit nur dem Primärschlüssel:

// Option 1: Pass just the ID
const note = await client.data.Note.create({
  title: 'Weekly summary',
  content: 'Summary of this week',
  notebook: { id: '22222222-3333-4444-5555-666666666666' },
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
});

// Option 2: Select the related fields you need before creating the note.
const notebooks = await client.data.Notebook.select(['id', 'name'])
  .where({ id: { eq: '22222222-3333-4444-5555-666666666666' } })
  .first(1)
  .execute();
const notebook = notebooks[0];
if (!notebook) {
  throw new Error('Notebook not found.');
}

const noteWithNotebook = await client.data.Note.create({
  title: 'Weekly summary',
  content: 'Summary of this week',
  notebook,
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
});

Beide Formen erzeugen dasselbe Ergebnis. Verwenden Sie das erste Formular, wenn Sie bereits die ID der zugehörigen Entität kennen und einen zusätzlichen Abruf vermeiden möchten.

Der Client verwendet den primären Schlüssel des zugehörigen Objekts, um die Beziehung festzulegen. Das Weitergeben eines Objekts erzeugt oder aktualisiert das zugehörige Notizbuch nicht.

Datensätze aktualisieren

Verwenden Sie die update() Methode, um vorhandene Datensätze zu ändern. Übergeben Sie ein Filterobjekt und ein Objekt, das die zu aktualisierenden Felder enthält:

await client.data.Note.update(
  { id: 'note-123' },
  {
    title: 'Updated title',
    updatedAt: new Date(),
  }
);

Aktualisieren von Beziehungen

Um eine Beziehung zu ändern, übergeben Sie die neue verknüpfte Entität oder nur ihre ID:

// Move a note to a different notebook
await client.data.Note.update(
  { id: 'note-123' },
  { notebook: { id: 'new-notebook-789' } }
);

Datensätze löschen

Verwenden Sie die delete() Methode, um Datensätze zu entfernen, die mit einem Filter übereinstimmen:

await client.data.Note.delete({ id: 'note-123' });

Die Methode wird aufgelöst, wenn das Back-End das Löschen bestätigt. Wenn keine Datensätze mit dem Filter übereinstimmen, ist die Methode weiterhin erfolgreich.

Erstellen oder aktualisieren Sie einen Datensatz

Nutze upsert(where, create, update), um einen fehlenden Datensatz zu erstellen oder einen bestehenden zu aktualisieren. Gib dieselbe ID im Lookup und in der Eingabe zum Erstellen an:

const id = '11111111-2222-3333-4444-555555555555';
const now = new Date();

await client.data.Note.upsert(
  { id },
  {
    id,
    title: 'Daily summary',
    content: 'Summary of today',
    isPinned: false,
    isArchived: false,
    createdAt: now,
    updatedAt: now,
    user_id: 'user-123',
  },
  { title: 'Daily summary', updatedAt: now },
);

Dieser Helfer führt einen Lesevorgang durch, gefolgt von einer separaten Erstellung oder Aktualisierung. Es ist nicht atomar, und gleichzeitige Aufrufe können zu Race Conditions führen. Verwende es nicht als Transaktions- oder Nebenläufigkeitsgarantie.

Behandeln der Authentifizierung

Melden Sie sich an, bevor Sie geschützte Datenoperationen durchführen. Für ein bereitgestelltes Backend verwenden Sie den konfigurierten Microsoft Entra-Anmeldefluss der App. Das folgende E-Mail-und-Passwort-Beispiel gilt nur für ein lokales Backend:

await client.auth.signIn({ email, password });

// All subsequent data calls include authentication context
const notes = await client.data.Note.select(['id', 'title']).execute();

Der Client fügt die Authentifizierungssitzung automatisch an alle Daten-API-Aufrufe an. Sie müssen Token nicht manuell übergeben.

Bewährte Methoden

  • Wählen Sie nur erforderliche Felder aus – Rufen Sie nur die Felder ab, die Sie verwenden, um die Nutzlastgröße zu verringern und die Leistung zu verbessern.
  • Verwenden Sie die Paginierung für große Listen – Vermeiden Sie das Gleichzeitige Abrufen von Tausenden von Datensätzen mithilfe first() und executePaginated().
  • Batchbeziehungsabfragen – Fügen Sie verwandte Entitätsfelder in dieselbe Abfrage ein, anstatt separate Anforderungen auszuführen.
  • Häufig verwendete Daten zwischenspeichern – Speichern Sie statische Referenzdaten im Arbeitsspeicher, um API-Aufrufe zu reduzieren.

Aktuelle Einschränkungen

  • Eine eigenständige count() Methode ist nicht verfügbar. Verwenden Sie eine numerische Feldaggregation oder zählen Sie Datensätze über alle Seiten hinweg. Die Länge eines Arrays zählt nur die auf der jeweiligen Seite angegebenen Datensätze.
  • M:n-Beziehungen werden nicht unterstützt. Verwenden Sie eine explizite Verknüpfungsentität mit zwei @one() Navigationsdekortoren.
  • Die Eigenschaft totalCount bei PagedResult wird nicht vom Backend gesetzt.