Lire et écrire des données avec GraphQL dans Fabric Apps

Fabric Apps fournit un client GraphQL de type sécurisé qui vous permet d’effectuer des opérations de création, de lecture, de mise à jour et de suppression sans écrire de requêtes brutes. Le client génère automatiquement GraphQL à partir de vos appels de méthode et retourne des entités typées en fonction de vos définitions de modèle de données.

Prerequisites

  • Projet Fabric Apps avec des modèles de données définis. Consultez Définir des modèles de données.
  • Les services back-end s’exécutant localement ou déployés sur Fabric.

Initialiser le client

Initialisez RayfinClient avec l’URL de votre backend, votre clé publique et le type de schéma :

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,
});

L’argument de type générique permet à TypeScript de fournir la saisie semi-automatique et la vérification de type pour toutes les opérations de données.

Ces exemples supposent que vous avez activé les données et que vous avez enregistré Note et Notebook dans le contrat de schéma partagé et le packages/data/src/index.ts de l’application universelle. Dans un modèle plat, importez plutôt votre type de schéma depuis rayfin/data/schema.ts. Consultez Définir des modèles de données.

Lire les données

Accéder aux collections d’entités via client.data.<EntityName>. L’API Fluent fournit des méthodes pour interroger, filtrer, trier et paginer.

Va chercher une page d’enregistrements

execute() ne renvoie qu’une seule page, avec 100 enregistrements par défaut. Il ne renvoie pas les métadonnées du curseur ni ne récupère les pages suivantes. Utilisez la pagination suivante pour les listes qui peuvent dépasser une page.

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

Récupérer un enregistrement unique par clé primaire

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

findById() renvoie null si aucun dossier n’existe. L’implémentation publiée ne sélectionne que la clé primaire, donc ne supposez pas que d’autres champs d’entités sont remplis. Sélectionnez explicitement les champs lorsque vous avez besoin de leurs valeurs :

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;

Filtrer les enregistrements

Utilisez la where() méthode pour filtrer les résultats :

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

Opérateurs de filtre

Operator Description Example
eq Égale { status: { eq: 'active' } }
neq N’est pas égal à { status: { neq: 'archived' } }
gt Supérieur à { age: { gt: 18 } }
gte Supérieur ou égal à { age: { gte: 21 } }
lt Inférieur à { price: { lt: 100 } }
lte Inférieur ou égal à { price: { lte: 50 } }
contains Contient la sous-chaîne { title: { contains: 'draft' } }

Trier les résultats

Permet orderBy() de trier les résultats de la requête :

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

Trier par plusieurs colonnes :

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

Lorsque vous définissez des relations avec les décorateurs @one() et @many(), vous pouvez inclure des champs de l’entité associée dans la même requête :

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

Chaque note inclut les données de son carnet associé sans nécessiter de requête distincte.

Paginer les jeux de résultats volumineux

Utilisez la pagination basée sur le curseur pour les grandes listes :

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);

Récupérez la page suivante à l’aide du curseur :

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();
}

Note

Le backend ne renseigne pas la propriété totalCount du type PagedResult. La page.items.length valeur ne compte que la page actuelle, pas tous les enregistrements correspondants.

Pour traiter chaque enregistrement correspondant, continuez jusqu’à ce que hasNextPage soit false. Gardez les sélections, filtres et ordre de tri inchangés d’une requête à l’autre.

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);

La pagination ne fournit pas d’instantané entre les requêtes. Des modifications simultanées peuvent affecter les enregistrements que vous lisez et comptez.

Agréger et regrouper les données

À utiliser aggregate() pour les totaux côté serveur et groupBy() pour les résumés par groupe. La méthode autonome de Fluent count() n’est pas disponible, mais count est prise en charge dans une spécification d’agrégation.

Les cinq opérations agrégées (sum, avg, min, max, et count) nécessitent un champ numérique. Ne comptez pas un UUID id ni un champ de texte. Par exemple, la requête suivante suppose une entité enregistrée Product avec un champ numérique stockQuantity requis :

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 compte les valeurs non nulles du champ numérique spécifié. Utilisez un champ numérique requis pour compter les lignes correspondantes. Si votre entité n’a pas de champ approprié, comptez les identifiants sur toutes les pages comme indiqué précédemment, pas seulement sur une page.

Pour les totaux regroupés :

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);
}

Les résultats d’agrégation sont des tableaux composés d’objets fields et aggregations. Un grand total comporte une ligne avec un objet vide fields . count retourne un number; les autres opérations retournent number | null pour des entrées vides ou tout-nulles.

Appliquez where() avant l’agrégation. Ne combinez pas l’agrégation avec la sélection des lignes, l’ordre ou la pagination via select(), orderBy(), first() ou after().

Créer des enregistrements

Utilisez la create() méthode pour insérer de nouveaux enregistrements :

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',
});

La méthode renvoie l’enregistrement créé, y compris son identifiant auto-généré id. Utilisez une sélection explicite lorsque vous avez besoin de champs supplémentaires stockés.

Créer des enregistrements associés

Lors de la création d’entités qui ont des relations, transmettez l’objet associé complet ou un objet avec uniquement la clé primaire :

// 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(),
});

Les deux formulaires produisent le même résultat. Utilisez le premier formulaire lorsque vous connaissez déjà l’ID de l’entité associée et souhaitez éviter une extraction supplémentaire.

Le client utilise la clé primaire de l’objet associé pour définir la relation. Passer un objet ne crée ni ne met à jour le carnet associé.

Mettre à jour les enregistrements

Utilisez la update() méthode pour modifier les enregistrements existants. Transmettez un objet de filtre et un objet contenant les champs à mettre à jour :

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

Mettre à jour les relations

Pour modifier une relation, transmettez la nouvelle entité associée ou simplement son ID :

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

Supprimer les enregistrements

Utilisez la delete() méthode pour supprimer les enregistrements correspondant à un filtre :

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

La méthode se résout lorsque le back-end confirme la suppression. Si aucun enregistrement ne correspond au filtre, la méthode réussit toujours.

Créer ou mettre à jour un enregistrement

Utilisez upsert(where, create, update) pour créer un enregistrement manquant ou mettre à jour un enregistrement existant. Fournissez le même ID dans la recherche et l’entrée de création :

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 },
);

Cette fonction utilitaire effectue une lecture suivie d’une création ou d’une mise à jour séparée. Ce n’est pas atomique, et les appels simultanés peuvent entrer en concurrence. Ne l’utilisez pas comme garantie de transaction ou de concurrence.

Gérer l’authentification

Connectez-vous avant d’effectuer des opérations sur les données protégées. Pour un backend déployé, utilisez le flux de connexion Microsoft Entra configuré par l'application. L’exemple suivant d’email et de mot de passe concerne uniquement un backend local :

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

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

Le client attache automatiquement la session d’authentification à tous les appels d’API de données. Vous n’avez pas besoin de passer manuellement des jetons.

Bonnes pratiques

  • Sélectionnez uniquement les champs nécessaires : récupérez uniquement les champs que vous utilisez pour réduire la taille de charge utile et améliorer les performances.
  • Utilisez la pagination pour les grandes listes : évitez d’extraire des milliers d’enregistrements à la fois à l’aide first() et executePaginated().
  • Requêtes de relation batch : incluez des champs d’entité connexes dans la même requête plutôt que d’effectuer des requêtes distinctes.
  • Mettre en cache les données fréquemment sollicitées : stockez des données de référence statiques en mémoire pour réduire les appels d’API.

Limitations actuelles

  • Aucune méthode autonome count() n’est disponible. Utilisez une agrégation de champ numérique, ou comptez les enregistrements sur l’ensemble des pages. La longueur d’un tableau ne compte que les enregistrements retournés sur cette page.
  • Les relations plusieurs-à-plusieurs ne sont pas prises en charge. Utilisez une entité de jointure explicite avec deux @one() décorateurs de navigation.
  • La propriété totalCount de PagedResult n’est pas renseignée par le backend.