Support Power Platform - Apps - Automate - SharePoint - Copilot

[Dev] Import d’un fichier Excel dans une liste SharePoint

Stack : Node.js + PnPjs v4 + xlsx + certificat Azure AD

Prérequis

  • Projet SPFx existant avec PnPjs v4 configuré
  • Certificat Azure AD en place (private.key, thumbprint)
  • Liste SharePoint cible déjà créée avec les colonnes correspondantes
  • Fichier .env complété (voir guide de création de liste)

Phase 1 — Préparer le fichier Excel

Étape 1 — Respecter la structure attendue

La première ligne du fichier Excel doit contenir les noms exacts des colonnes de la liste SharePoint cible.

RègleDétail
Nom de colonneIdentique au nom interne SharePoint (sensible à la casse)
DatesFormat YYYY-MM-DD (ex. 2026-03-30)
NombresValeurs numériques sans unité ni espace
Colonnes videsLaisser la cellule vide — ne pas mettre N/A ou -
Colonne choixLa valeur doit correspondre exactement à l’un des choix définis

Étape 2 — Placer le fichier dans le projet

mon-projet/
└── sessions.xlsx    ← à la racine du projet

Phase 2 — Installer la dépendance

Étape 3 — Installer la bibliothèque de lecture Excel

npm install xlsx

Phase 3 — Créer le script d’import

Étape 4 — Créer scripts/importList.ts

import "dotenv/config";
import { spfi } from "@pnp/sp";
import { SPDefault } from "@pnp/nodejs";
import * as fs from "fs";
import * as path from "path";
import * as XLSX from "xlsx";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";


// ─── Configuration ─────────────────────────────────────────────────
const TENANT_ID    = process.env.TENANT_ID!;
const CLIENT_ID    = process.env.CLIENT_ID!;
const THUMBPRINT   = process.env.THUMBPRINT!;
const KEY_PATH     = process.env.KEY_PATH!;
const SITE_URL     = process.env.SITE_URL!;
const SP_ROOT_URL  = process.env.SP_ROOT_URL!;


const LIST_NAME    = "NomDeLaListe";   // ← à adapter
const EXCEL_PATH   = path.resolve(__dirname, "../fichier.xlsx"); // ← à adapter
// ──────────────────────────────────────────────────────────────────


async function importData(): Promise<void> {


  // Lecture du fichier Excel
  const workbook = XLSX.readFile(EXCEL_PATH);
  const sheet    = workbook.Sheets[workbook.SheetNames[0]];
  const rows: Record<string, any>[] = XLSX.utils.sheet_to_json(sheet);


  console.log(`${rows.length} ligne(s) trouvée(s).`);


  // Connexion SharePoint
  const privateKey = fs.readFileSync(KEY_PATH, "utf-8").trim();


  const sp = spfi(SITE_URL).using(
    SPDefault({
      baseUrl: SITE_URL,
      msal: {
        config: {
          auth: {
            clientId:  CLIENT_ID,
            authority: `https://login.microsoftonline.com/${TENANT_ID}`,
            clientCertificate: {
              thumbprint: THUMBPRINT,
              privateKey: privateKey
            }
          }
        },
        scopes: [`${SP_ROOT_URL}/.default`]
      }
    })
  );


  const list = sp.web.lists.getByTitle(LIST_NAME);


  // Import ligne par ligne
  for (const [index, row] of rows.entries()) {
    try {


      // Construire l'objet à envoyer — adapter les champs selon la liste
      // ?? "" : envoyer une chaîne vide si la cellule est vide (colonnes texte)
      // ?? null : envoyer null si la cellule est vide (colonnes date)
      // Number() : convertir en nombre (colonnes numériques)


      // Conversion de date JJ/MM/AAAA → AAAA-MM-JJ (format attendu par SharePoint)
      // Exemple : "30/03/2026" → "2026-03-30"
      const toIsoDate = (val: string | undefined): string | null => {
        if (!val) return null;
        const parts = val.split("/");
        if (parts.length !== 3) return val; // déjà au bon format ou valeur inattendue
        return `${parts[2]}-${parts[1]}-${parts[0]}`;
      };


      const item = {
        Title:             row["Title"]             ?? "",   // texte → chaîne vide par défaut
        CodeFormation:     row["CodeFormation"]     ?? "",   // texte → chaîne vide par défaut
        DateSession:       toIsoDate(row["DateSession"]),    // JJ/MM/AAAA → AAAA-MM-JJ
        NbConfirmes:       Number(row["NbConfirmes"])  || 0, // nombre → 0 si vide ou NaN
        NbOption:          Number(row["NbOption"])     || 0, // nombre → 0 si vide ou NaN
        // ↑ Ajouter ici tous les autres champs de la liste en suivant le même modèle
      };


      await list.items.add(item);
      console.log(`  [${index + 1}/${rows.length}] Importé : ${row["Title"] ?? row[Object.keys(row)[0]]}`);


    } catch (err: any) {
      console.error(`  [${index + 1}] Erreur : ${err.message}`);
    }
  }


  console.log("Import terminé.");
}


importData()
  .then(() => process.exit(0))
  .catch((err) => {
    console.error("Erreur fatale :", err.message ?? err);
    process.exit(1);
  });

Étape 5 — Adapter les types de colonnes

Dans la boucle d’import, identifier les colonnes nécessitant une conversion :

Type SharePointTraitement dans le script
Ligne de texteString(val) ou val ?? ""
NombreNumber(val) || 0
DateLaisser en string YYYY-MM-DD — SharePoint accepte ce format
ChoixLa valeur doit correspondre exactement à un choix existant
Oui/NonBoolean(val) ou val === "Oui"
PersonneRequiert le loginName ou l’id utilisateur — ne pas passer en import direct

Exemple concret de mapping avec les 3 types les plus courants

Lors de la construction de l’objet item à envoyer à SharePoint, chaque champ suit une logique différente selon son type :

// Colonne texte — si la cellule est vide, envoyer une chaîne vide plutôt que undefined
Title:         row["Title"]         ?? "",


// Colonne texte — même logique, le nom de la clé doit correspondre exactement
// au nom interne de la colonne SharePoint
CodeFormation: row["CodeFormation"] ?? "",


// Colonne date — si la cellule est vide, envoyer null plutôt qu'une chaîne vide
// (SharePoint rejetterait "" pour un champ de type date)
DateSession:   row["DateSession"]   ?? null,

L’opérateur ?? (nullish coalescing) renvoie la valeur de droite uniquement si la valeur de gauche est null ou undefined — il n’écrase pas 0 ou false, contrairement à ||.

Phase 4 — Configurer et lancer

Étape 6 — Ajouter le script dans package.json

"scripts": {
  "import-list": "ts-node --project tsconfig.scripts.json scripts/importList.ts"
}

Étape 7 — Vérifier la structure du projet

mon-projet/
├── fichier.xlsx             ← fichier Excel source
├── scripts/
│   └── importList.ts        ← script d'import
├── .env                     ← variables d'environnement
├── tsconfig.scripts.json
└── package.json

Étape 8 — Lancer l’import

npm run import-list

Résultat attendu :

N ligne(s) trouvée(s).
  [1/N] Importé : ...
  [2/N] Importé : ...
  ...
Import terminé.

Gestion des erreurs courantes

ErreurCause probableCorrection
401 UnauthorizedCertificat ou permissions incorrectsVérifier Azure AD et le thumbprint
Unsupported app only tokenPermission SharePoint de type Application manquanteAjouter Sites.FullControl.All Application dans Azure AD
The field X does not existNom de colonne Excel différent du nom interne SPVérifier la casse et le nom exact de la colonne
Invalid choice valueValeur non définie dans la colonne ChoixVérifier les valeurs autorisées dans la liste SP
Cannot convert to numberCellule vide dans une colonne numériqueAjouter `

Bonnes pratiques

  • Tester l’import avec une seule ligne avant de lancer les données complètes
  • Conserver le fichier Excel source dans Git (sans données sensibles)
  • Ne jamais commiter .env, private.key et appcert.pfx
  • En cas d’import partiel (erreur à mi-chemin), supprimer les éléments déjà créés avant de relancer pour éviter les doublons
  • Pour les gros volumes (> 100 lignes), envisager un import par batch avec Promise.all() par groupes de 10