Serverpod : développement des endpoints

Dans lʼarticle précédent, je vous ai montré comment implémenter et déployer un modèle de données avec le package Serverpod. Poursuivons donc notre applicationfil rouge là où nous lʼavions laissée et continuons à explorer cet ORM en développant et déployant nos endpoints.

IV. Développement des endpoints

1. Introduction à la syntaxe des opérations CRUDs

Maintenant que nos tables et classes sont générées et que nos modèles ont été définis dans leurs fichiers YAML respectifs, nous avons accès à un large paneldʼopérations CRUDs pour travailler avec notre base de données.

Pour ce faire, il suffit de faire appel à une méthode comme suit :

await YourGeneratedModel.db.crudOperation(session)

Les méthodes CRUDs que nous utiliserons sont les sont les suivantes :

// Récupération d'un objet par id
await YourGeneratedModel.db.findById(session, transaction)
// Récupération d'une liste d'objets avec un filtre
await YourGeneratedModel.db.find(session, where, transaction)
// Insertion d'un objet en BDD
await YourGeneratedModel.db.insertRow(session, row, transaction)
// Insertion d'une liste d'objets en BDD
await YourGeneratedModel.db.insert(session, rows, transaction)
// Update d'un objet en BDD
await YourGeneratedModel.db.updateRow(session, row, transaction)
// Update d'une liste d'objet en BDD
await YourGeneratedModel.db.update(session, rows, transaction)
// Effectuer une jonction entre deux objets ayant une relation
await YourGeneratedModel.db.attachRow(session, row, row, transaction) 
// Casser une jonction entre deux objets ayant une relation
await YourGeneratedModel.db.detachRow(session, row, row, transaction)

Toutes ces méthodes nécessitent un paramètre Session, qui fournit lʼaccès à la session (par exemple, les informations sur lʼutilisateur connecté). Elles acceptent également un paramètre optionnel Transaction? pour la gestion des transactions SQL avec Serverpod. Pour plus de détails, vous pouvez consulter la documentation sur les opérations CRUD.

Ceci présenté, commençons à développer.

2. Corrélation entre notre authentification et notre base de données

La première opération à implémenter est la mise en relation entre un nouvel utilisateur créant son compte et notre table bounty_hunter_raw qui doit contenir ses UserInfo.

Pour ce faire, dans votre projet serveur, placez-vous dans src/server.dart, et ajoutez lʼattribut onUserCreated dans notre objet AuthConfig comme suit :

auth.AuthConfig.set(auth.AuthConfig( // ...
  // other attributes
  // ...
  onUserCreated: (session, userInfo) async {
    if (userInfo.id != null) {
      final BountyHunterRaw bountyHunterRaw = BountyHunterRaw(
        userInfoId: userInfo.id!,  
        userInfo: userInfo,  
        hunterHuntedList: <HunterHunted> [],  
      );
      await BountyHunterRaw.db.insertRow(session, bountyHunterRaw);  
    }  
  },  
));  

Ainsi, dès quʼun utilisateur est créé, un objet BountyHunterRaw est déclaré pour transmettre les informations de ce nouvel utilisateur et lʼinscrire en BDD avec BountyHunterRaw.db.insertRow(session, bountyHunterRaw).

Cela garantit quʼun BountyHunterRaw représente un utilisateur. Nous devons maintenant développer les endpoints pour récupérer et manipuler les données de ce dernier.

3. Implémentations de nos endpoints

Deux endpoints sont à décrire, accompagnés de deux fichiers utils (afin de simplifier certaines opérations récurrentes) et de deux mappers pour notre application dʼexemple :

  • bounty_hunter.mapper.dart
  • force_user.mapper.dart
  • bounty_hunter_raw.utils.dart
  • force_user_raw.utils.dart
  • bounty_hunter.endpoint.dart
  • force_user.endpoint.dart

Débutons par les mappers. Jʼai localisé mes fichiers au sein du projet serveur dans lib/src/mappers/, et sont implémentés comme suit :

// mapper.dart  
abstract class Mapper<R, M> {  
  M toModel({  
    required R rawModel  
  });  
}

// bounty_hunter.mapper.dart  
class BountyHunterMapper extends Mapper<BountyHunterRaw, BountyHunter> {  
  @override  
  BountyHunter toModel({  
    required BountyHunterRaw rawModel  
  }) => BountyHunter(  
    id: rawModel.id,  
    userInfo: rawModel.userInfo!,  
    forceUsers: _getForceUsers(rawModel.hunterHuntedList),  
  );  
  
  List<ForceUser> _getForceUsers(List<HunterHunted>? hunterHuntedList) {  
    if (hunterHuntedList == null) return <ForceUser>[];  
    List<ForceUser> forceUsersList = List<ForceUser>.empty(growable: true);  
    for (HunterHunted hunterHunted in hunterHuntedList) {  
      if (hunterHunted.forceUser != null) {  
        forceUsersList.add(  
          ForceUserMapper().toModel(rawModel: hunterHunted.forceUser!),  
        );  
      }  
    }  
    return forceUsersList;  
  }  
}

// force_user.mapper.dart  
class ForceUserMapper extends Mapper<ForceUserRaw, ForceUser> {  
  @override  
  ForceUser toModel({  
    required ForceUserRaw rawModel  
  }) => ForceUser(  
    id: rawModel.id,  
    type: rawModel.type,  
    name: rawModel.name,  
    laserSaber: rawModel.laserSaber ?? LaserSaber(  
      name: 'DEFAULT',  
      color: 'DEFAULT',  
    ),  
    master: rawModel.master == null ? null : toModel(rawModel: rawModel.master!),  
    apprentices: _getApprentices(rawModel.apprentices),  
    droids: rawModel.droids ?? <Droid>[],  
  );  

  List<ForceUser> _getApprentices(List<ForceUserRaw>? rawApprentices) {  
    if (rawApprentices == null) return <ForceUser>[];  
    List<ForceUser> apprentices = List<ForceUser>.empty(growable: true);  
    for (ForceUserRaw forceUserRaw in rawApprentices) {  
      apprentices.add(toModel(rawModel: forceUserRaw));  
    }  
    return apprentices;  
  }  
}

Ces mappers permettent simplement de caster respectivement un BountyHunterRaw ou ForceUserRaw en BountyHunter ou ForceUser.Passons à lʼimplémentation de nos fichiers utils :

// bounty_hunter_raw.utils.dart  
class BountyHunterRawUtils {  
  const BountyHunterRawUtils._();  
  static BountyHunterRawInclude allInclude = BountyHunterRaw.include(  
    userInfo: UserInfo.include(),  
    hunterHuntedList: HunterHunted.includeList(  
      include: HunterHunted.include(  
        bountyHunter: BountyHunterRaw.include(),  
        forceUser: ForceUserRawUtils.allInclude,  
      ),  
    ),  
  );  
  static BountyHunterRawIncludeList allIncludeList = BountyHunterRaw.includeList(  
    include: allInclude,  
  );  
  static Future<BountyHunterRaw> getFirstWhere({  
    required Session session,  
    Expression<dynamic> Function(BountyHunterRawTable)? where,  
    Transaction? transaction,  
  }) async {  
    final BountyHunterRaw? res = await BountyHunterRaw.db.findFirstRow(  
      session,  
      where: where,  
      transaction: transaction,  
      include: allInclude,  
    );  
    if (res == null) throw Exception();  
    return res;  
  }  
}

// force_user_raw.utils.dart  
class ForceUserRawUtils {  
  const ForceUserRawUtils._();  
  static ForceUserRawInclude allInclude = ForceUserRaw.include(  
    laserSaber: LaserSaber.include(),  
    droids: Droid.includeList(  
      include: Droid.include(),  
    ),  
    apprentices: ForceUserRaw.includeList(  
      include: ForceUserRaw.include(  
        laserSaber: LaserSaber.include(),  
        droids: Droid.includeList(  
          include: Droid.include(),  
        ),  
      ),  
    ),  
    master: ForceUserRaw.include(  
      laserSaber: LaserSaber.include(),  
      droids: Droid.includeList(  
        include: Droid.include(),  
      ),  
    ),  
  );  
  static ForceUserRawIncludeList allIncludeList = ForceUserRaw.includeList(  
    include: allInclude,  
  );  
  static Future<ForceUserRaw> get({  
    required Session session,  
    required int forceUserId,  
    Transaction? transaction,  
  }) async {  
    final ForceUserRaw? res = await ForceUserRaw.db.findById(  
      session,  
      forceUserId,  
      include: allInclude,  
      transaction: transaction,  
    );  
    if (res == null) throw Error();  
    return res;  
  }  
  static Future<List<ForceUserRaw>> getWhere({  
    required Session session,  
    Expression<dynamic> Function(ForceUserRawTable)? where,  
    Transaction? transaction,  
  }) async {  
    final List<ForceUserRaw> res = await ForceUserRaw.db.find(  
      session,  
      where: where,  
      include: allInclude,  
      transaction: transaction,  
    );  
    return res;  
  }  
}

Ces deux fichiers utils nous permettent notamment de déclarer les constantes dʼ include . Ces dernières, dans Serverpod, consistent à indiquer la portée de lʼobjet récupéré sʼil contient des relations avec dʼautres objets. Par défaut, si aucun include nʼest défini, lʼobjet renvoyé par notre base de données SQL ne contiendra aucun des objets liées par une relation. Dans notre exemple, nous incluons donc la totalité des objets liés à BountyHunterRaw et ForceUserRaw avec les variables allInclude et allIncludeList dans chacun des fichiers utils. Si vous souhaitez plus dʼinformations sur ce concept, je vous conseille la documentation Serverpod sur ce sujet.

Vous remarquerez également la présence de plusieurs méthodes effectuant des opérations CRUDs dans ces fichiers utils :

  • bounty_hunter.utils.dart
    • getFirstWhere(), nous permettant de récupérer le premier BountyHunterRaw validant un filtre en intégrant toutes ses includes.
  • force_user.utils.dart
    • get(), nous permettant de récupérer un ForceUserRaw par son id en vérifiant que lʼobjet de retour ne soit pas null en intégrant toutes ses includes.
    • getWhere(), nous permettant de récupérer une liste de ForceUserRaw validant un filtre en intégrant toutes les includes.

En définitif, ces fichiers utils ne sont pas forcément nécessaires, mais nous permettent de définir des includes ou des opérations CRUDs que nous pourrions répéter dans nos endpoints, ce qui pourrait dégrader la lisibilité de notre code sur une application avec de nombreuses opérations. Je souhaitais tout de même lʼaborder, carceci peut porter à réflexion sur un type dʼarchitecture qui pourrait être initié sur la partie serveur dʼun projet Serverpod.

Recentrons-nous maintenant dʼavantage sur nos endpoints, à commencer avec bounty_hunter.endpoint.dart.


Débutez par créer votre classe en lʼétendant avec Endpoint (tous nos endpoints devrons étendre cette classe apportée par Serverpod) :

class BountyHunterEndpoint extends Endpoint { @override
bool get requireLogin => true; }

Vous remarquerez que nous avons override le getter requireLogin afin certifier que lʼutilisateur qui jouera des méthodes de ce endpoint est correctement connecté. Sivous développer un endpoint dans le future qui ne nécessite aucune authentification, il ne sera pas utile dʼoverride cette méthode.

Maintenant, implémentons la récupération des données de notre utilisateur ( BountyHunter ), méthode qui sera jouée à sa connexion à notre application :

Future<BountyHunter> me(Session session) async {  
  // Récupération de la version Raw de notre BountyHunter authentifié  
  final BountyHunterRaw res = await _meRaw(session);  
  // Traduction de la version Raw en Modèle  
  return BountyHunterMapper().toModel(rawModel: res);  
}

Future<BountyHunterRaw> _meRaw(Session session) async {  
  // Récupération de l'id de notre utilisateur authentifié  
  final int? userId = (await session.authenticated)?.userId;  
  if (userId == null) throw Error();  
  final BountyHunterRaw res = await BountyHunterRawUtils.getFirstWhere(  
    session: session,  
    // Récupération du BountyHunter ayant un attribut userInfoId égal à  
    // l'id de notre utilisateur authentifié  
    where: (bH) => bH.userInfoId.equals(userId),  
  );  
  return res;  
}

Vous comprendrez avec le code ci-dessus que nous récupérons lʼid de lʼutilisateur connecté grace à lʼobjet Session qui doit être passé en paramètre de chaque méthode appartenant à une classe étendant Endpoint. De plus, nous récupérons ensuite la première et unique instance validant le filtre que nous apposons à notreméthode construite précédemment, BountyHunterRawUtils.getFirstWhere(). Si vous souhaitez en apprendre dʼavantage sur les filtres disponibles avec Serverpod, consultez cette documentation.

Pour finaliser ce BountyHunterEndpoint, il ne nous reste plus quʼà mettre en place la gestion des jointures many-to-many avec ForceUserRaw comme suit :

Future<BountyHunter> addForceUserToPreysList(Session session, int forceUserId) async {  
  final BountyHunterRaw loggedBountyHunter = await _meRaw(session);  
  late ForceUserRaw? forceUserRaw;  
  late HunterHunted hunterHunted;  
  await session.db.transaction((transaction) async {  
    forceUserRaw = await ForceUserRaw.db.findFirstRow(  
      session,  
      where: (fU) => fU.id.equals(forceUserId),  
      transaction: transaction,  
    );  
    if (forceUserRaw == null) throw Error();  
    hunterHunted = await HunterHunted.db.insertRow(  
      session,  
      HunterHunted(  
        bountyHunterId: loggedBountyHunter.id!,  
        forceUserId: forceUserId,  
      ),  
    );  
  });  
  await BountyHunterRaw.db.attachRow.hunterHuntedList(  
    session,  
    loggedBountyHunter,  
    hunterHunted,  
  );  
  await ForceUserRaw.db.attachRow.hunterHuntedList(  
    session, forceUserRaw!, hunterHunted,  
  );  
  return await me(session);  
}

Future<BountyHunter> withdrawForceUserFromPreysList(Session session, {  
  required int forceUserId,  
}) async {  
  final ForceUserRaw? forceUserRaw = await ForceUserRaw.db.findById(  
    session,  
    forceUserId,  
  );  
  if (forceUserRaw == null) throw Exception();  
  final BountyHunterRaw meRawRes = await _meRaw(session);  
  final HunterHunted? hunterHuntedRes = await HunterHunted.db.findFirstRow(  
    session,  
    where: (hH) =>  
      hH.forceUserId.equals(forceUserRaw.id) &&  
      hH.bountyHunterId.equals(meRawRes.id),  
  );  
  if (hunterHuntedRes == null) throw Exception();  
  HunterHunted.db.deleteRow(session, hunterHuntedRes);  
  return await me(session);  
}

Pour ce faire, nous avons développé deux méthodes addForceUserToPreysList() permettant de créer une table de jointure avec les données de notre couple BountyHunterRaw / ForceUserRaw, et withdrawForceUserFromPreysList() rompant cette jointure en la supprimant (surpression de la colonne correspondant sur la table de jointure hunter_hunted). Les actions effectuées par ces deux fonctions sont donc les suivantes :

  • addForceUserToPreysList() : mise en place dʼune transaction (grâce à notre objet Session) afin dʼeffectuer deux actions successives en BDD (récupération du ForceUserRaw à liée puis création de notre objet de jointure HunterHunted), pour finaliser avec les opérations de jointure sur les deux objets liés en many-to-many avec db.attachRow.hunterHuntedList().
  • withdrawForceUserFromPreysList() : récupération du couple ForceUserRaw / BountyHunterRaw afin de supprimer la table de jointure correspondante avec db.deleteRow().

Ceci étant déployé, nous allons maintenant passer à lʼintégration de notre second endpoint, ForceUserEndpoint. Vous pouvez dʼores et déjà étendre cette classe avec Endpoint et override requireLogin afin de le passer à true par défaut.

Dorénavant, nous aurons besoin dʼimplémenter six méthodes différentes comme suit :

  • get() : récupération dʼun ForceUserRaw avec tous ces objets relationnels par son id.
Future<ForceUser> get(Session session, {  
  required int id,  
}) async {  
  final ForceUserRaw res = await ForceUserRawUtils.get(  
    session: session,  
    forceUserId: id,  
  );  
  final ForceUser model = ForceUserMapper().toModel(rawModel: res);  
  return model;  
}

  • create() : création dʼun ForceUserRaw et de tous ces objets relationnels en BDD
Future<ForceUser> create(Session session, ForceUser forceUser) async {  
  late ForceUserRaw forceUserRawRes;  
  late LaserSaber laserSaberRes;  
  await session.db.transaction((transaction) async {  
    laserSaberRes = await LaserSaber.db.insertRow(  
      session,  
      forceUser.laserSaber,  
      transaction: transaction,  
    );  
    forceUserRawRes = await ForceUserRaw.db.insertRow(  
      session,  
      ForceUserRaw(  
        type: forceUser.type,  
        name: forceUser.name,  
      ),  
      transaction: transaction,  
    );  
    if (forceUserRawRes.id == null) throw Error();  
    final List<Droid> droidsToInsert = forceUser.droids.map(  
      (droid) => droid.copyWith(forceUserMasterId: forceUserRawRes.id!)  
    ).toList();  
    await Droid.db.insert(  
      session,  
      droidsToInsert,  
      transaction: transaction,  
    );  
  });  
  await ForceUserRaw.db.attachRow.laserSaber(  
    session,  
    forceUserRawRes,  
    laserSaberRes,  
  );  
  final ForceUserRaw res = await ForceUserRawUtils.get(  
    session: session,  
    forceUserId: forceUserRawRes.id!,  
  );  
  return ForceUserMapper().toModel(rawModel: res);  
}

Pour cette méthode, ne pas oublier de joindre les Droid en surchargeant leur clé étrangère (attribut forceUserMasterId) et le LaserSaber avec db.attach respectivement durant et après leur création ( db.insert ou db.insertRow ).

💡 Les jonctions ne sont pas à effectuer dans une transaction avec Serverpod, si vous effectué ceci, elle ne seront pas prises en compte (bug dupackage

  • update() : mise à jour des informations dʼun ForcesUserRaw et des ses objets en relation
Future<ForceUser> update(Session session, {  
  required ForceUser forceUser,  
}) async {  
  late ForceUserRaw forceUserRawRes;  
  late LaserSaber laserSaberRes;  
  List<Droid> droidsRes = List<Droid>.empty(growable: true);  
  await session.db.transaction((transaction) async {  
    laserSaberRes = await LaserSaber.db.updateRow(  
      session,  
      forceUser.laserSaber,  
      transaction: transaction,  
    );  
    for (Droid droid in forceUser.droids) {  
      if (droid.id == null) {  
        final Droid res = await Droid.db.insertRow(  
          session,  
          droid,  
          transaction: transaction,  
        );  
        droidsRes.add(res);  
      } else {  
        final Droid res = await Droid.db.updateRow(  
          session,  
          droid,  
          transaction: transaction,  
        );  
        droidsRes.add(res);  
      }  
    }  
    forceUserRawRes = await ForceUserRaw.db.updateRow(  
      session,  
      ForceUserRaw(  
        id: forceUser.id,  
        type: forceUser.type,  
        name: forceUser.name,  
        laserSaberId: forceUser.laserSaber.id,  
        masterId: forceUser.master?.id,  
      ),  
    );  
  });  
  final ForceUserRaw res = await ForceUserRawUtils.get(  
    session: session,  
    forceUserId: forceUserRawRes.id!,  
  );  
  return ForceUserMapper().toModel(rawModel: res);  
}

💡 Nʼoubliez pas dʼeffectuer toutes les opérations CRUDs au sein dʼune transaction, car nous effectuons des opérations sur plusieurs objets ( ForceUserRawet ses relations)

  • delete() : suppression dʼun ForceUserRaw et de tous ces objets liés
Future<int> delete(  
  Session session, {  
    required int forceUserId,  
  }) async =>  
  await session.db.transaction<int>((transaction) async {  
    final ForceUserRaw forceUserRawRes = await ForceUserRawUtils.get(  
      session: session,  
      forceUserId: forceUserId,  
      transaction: transaction,  
    );  
    final List<HunterHunted> hunterHuntedListRes = await HunterHunted.db.find(  
      session,  
      where: (hH) => hH.forceUserId.equals(forceUserId),  
      transaction: transaction,  
    );  
    await HunterHunted.db.delete(  
      session,  
      hunterHuntedListRes,  
      transaction: transaction,  
    );  
    if (forceUserRawRes.droids != null) {  
      await Droid.db.delete(  
        session,  
        forceUserRawRes.droids!,  
        transaction: transaction,  
      );  
    }  
    final ForceUserRaw deletedForceUserRes = await ForceUserRaw.db.deleteRow(  
      session,  
      forceUserRawRes,  
      transaction: transaction,  
    );  
    if (deletedForceUserRes.id == null) throw Error();  
    if (forceUserRawRes.laserSaber != null) {  
      await LaserSaber.db.deleteRow(  
        session,  
        forceUserRawRes.laserSaber!,  
        transaction: transaction,  
      );  
    }  
    return Future<int>.value(deletedForceUserRes.id);  
  });

💡Prenez garde à lʼordre de suppression de vos objets, ceux détenants une clé étrangère sont prioritaires ( HunterHunted puis Droid puis ForceUserRaw puisLaserSaber ), sinon un erreur sera levée côté serveur.

  • withoutMasterApprentices() : liste tous les apprentis sans maîtres
Future<List<ForceUser>> withoutMasterApprentices(  
  Session session, {  
    required ForceUserType forceType,  
  }) async {  
    if (forceType == ForceUserType.jediMaster ||  
        forceType == ForceUserType.sithMaster) {  
      throw Error();  
    }  
    final List<ForceUserRaw> res = await ForceUserRawUtils.getWhere(  
      session: session,  
      where: (fU) => fU.type.equals(forceType) && fU.masterId.equals(null),  
    );  
    return res  
        .map((apprentice) => ForceUserMapper().toModel(rawModel: apprentice))  
        .toList();  
  }

  • masters() : liste tous les maîtres
Future<List<ForceUser>> masters(  
  Session session, {  
    required ForceUserType forceType,  
  }) async {  
    if (forceType == ForceUserType.jediApprentice ||  
        forceType == ForceUserType.sithApprentice) {  
      throw Error();  
    }  
    final List<ForceUserRaw> res = await ForceUserRawUtils.getWhere(  
      session: session,  
      where: (fU) => fU.type.equals(forceType),  
    );  
    return res  
      .map(  
        (master) => ForceUserMapper().toModel(rawModel: master),  
      )  
      .toList();  
  }  

  • addApprenticeToMaster() : permet la jonction dʼun apprenti à un maître
Future<ForceUser> addApprenticeToMaster(  
    Session session, {  
      required int masterId,  
      required int apprenticeId,  
    }) async {  
    final ForceUserRaw masterRes = await ForceUserRawUtils.get(  
      session: session,  
      forceUserId: masterId,  
    );  
    final ForceUserRaw apprenticeRes = await ForceUserRawUtils.get(  
      session: session,  
      forceUserId: apprenticeId,  
    );  
    if (masterRes.type != ForceUserType.jediMaster &&  
      masterRes.type != ForceUserType.sithMaster) {  
      throw Error();  
    }  
    if (apprenticeRes.type != ForceUserType.jediApprentice &&  
      apprenticeRes.type != ForceUserType.sithApprentice) {  
      throw Error();  
    }  
    if ((masterRes.type == ForceUserType.sithMaster &&  
        apprenticeRes.type == ForceUserType.jediApprentice) ||  
      (masterRes.type == ForceUserType.jediMaster &&  
        apprenticeRes.type == ForceUserType.sithApprentice)) {  
      throw Error();  
    }  
    await ForceUserRaw.db.attachRow.apprentices(  
      session,  
      masterRes,  
      apprenticeRes,  
    );  
    return ForceUserMapper().toModel(  
      rawModel: await ForceUserRawUtils.get(  
        session: session,  
        forceUserId: masterId,  
      ),  
    );  
}

  • addMasterToApprentice() : permet la jonction dʼun maître à un apprenti
Future<ForceUser> addMasterToApprentice(  
  Session session, {  
    required int apprenticeId,  
    required int masterId,  
  }) async {  
  final ForceUserRaw apprenticeRes = await ForceUserRawUtils.get(  
    session: session,  
    forceUserId: apprenticeId,  
  );  
  final ForceUserRaw masterRes = await ForceUserRawUtils.get(  
    session: session,  
    forceUserId: masterId,  
  );  
  if (masterRes.type != ForceUserType.jediMaster &&  
    masterRes.type != ForceUserType.sithMaster) {  
    throw Error();  
  }  
  if (apprenticeRes.type != ForceUserType.jediApprentice &&  
    apprenticeRes.type != ForceUserType.sithApprentice) {  
    throw Error();  
  }  
  if ((masterRes.type == ForceUserType.sithMaster &&  
      apprenticeRes.type == ForceUserType.jediApprentice) ||  
    (masterRes.type == ForceUserType.jediMaster &&  
      apprenticeRes.type == ForceUserType.sithApprentice)) {  
    throw Error();  
  }  
  await ForceUserRaw.db.attachRow.master(  
    session,  
    apprenticeRes,  
    masterRes,  
  );  
  return ForceUserMapper().toModel(  
    rawModel: await ForceUserRawUtils.get(  
      session: session,  
      forceUserId: apprenticeId,  
    ),  
  );  
}

  • allNotHunted() : liste tous les ForceUser qui ne sont pas reliés à lʼutilisateur authentifié
Future<List<ForceUser>> allNotHunted(  
  Session session,  
) async {  
  final BountyHunter meRes = await bountyHunterEndpoint.me(session);  
  final Set<int> meHuntedForceUsersId = meRes.forceUsers  
    .map(  
      (fU) => fU.id!,  
    )  
    .toSet();  
  final List<ForceUserRaw> notHuntedForceUsersRes =  
    await ForceUserRaw.db.find(  
      session,  
      include: ForceUserRaw.include(  
        laserSaber: LaserSaber.include(),  
        droids: Droid.includeList(  
          include: Droid.include(),  
        ),  
      ),  
      where: (fU) => fU.id.notInSet(meHuntedForceUsersId),  
    );  
  return notHuntedForceUsersRes  
    .map((fURaw) => ForceUserMapper().toModel(rawModel: fURaw))  
    .toList();  
}

Cela développé, vous pouvez maintenant générer le code concernant nos endpoints toujours avec la ligne de commande serverpod generate en racine de votre projetserveur.

Ainsi, la mise en place de ForceUserEndpoint et la génération conclue donc la configuration de notre server ! Pour les plus intéressés, nous allons explorer une partiebonus ayant pour sujet le test de nos endpoints tout juste développés avec Postman.

Pour les autres, rendez-vous au chapitre suivant afin dʼeffectuer la liaison de notre backend avec notre application.

4. Test de de nos endpoint avec Postman (bonus)

Pour illustrer le test dʼun endpoint avec Postman, nous allons prendre lʼexemple de la création dʼun ForceUserRaw en base de données à lʼaide de la méthode create() de lʼendpoint ForceUserEndpoint . Pour réaliser ceci, entrez ces paramètres au sein dʼune requête Postman :

Vous remarquerez donc les points suivants :

  • LʼURL de la requête http reprend le nom de la classe de notre endpoint, ici http://localhost:8080/forceUser.
  • Comme lʼauthentification est requise pour cet endpoint, vous devez inclure les informations dʼauthentification dans le corps JSON de la requête sous lʼattribut “auth”, au format “$keyId:$key” . Ces informations sont obtenues après une connexion, comme détaillé au chapitre II.4.
  • Spécifiez le nom de la méthode à appeler dans votre endpoint, ici “method”: “create” .
  • Incluez les arguments nécessaires pour la méthode spécifiée, ici “forceUser”: “...” .

Spécifiez le nom de la méthode à appeler dans votre endpoint, ici “method”: “create” .


Comme jʼai pu lʼécrire dans le chapitre bonus sur le test de lʼauthentification, vous ne trouverez pas tellement dʼinformations sur ce genre de pratique car nʼétant pas poussé par les développeurs de Serverpod. De plus, la communauté de ce package restant encore restreinte peu de personnes ont mit ce genre de procédé en avant. Néanmoins, en tant que développeur, ce processus de test est une bonne pratique à implémenter pour gagner en temps de développement et en maintenabilité. Cʼest pourquoi il me semblait important dʼintégrer ces parties “bonusˮ au sein de cet article.

Cette troisième partie de notre exploration de Serverpod touche à sa fin. Il nous reste désormais un ultime chapitre, où nous verrons comment relier ce backend,développé tout au long de nos articles, à notre application Flutter. Je vous invite à nous retrouver pour cette dernière étape !

Quentin Lebreton - Ingénieur Etudes et Développement Junior