Office Graph API: Neues Dokument auf Basis einer Vorlage

In dem Artikel "Neues Dokument auf Basis einer Vorlage" erkläre ich die notwendigen Schritte um ein Word Dokument auf Basis eines Word Template zu erstellen.

Office Graph API: Neues Dokument auf Basis einer Vorlage

Letztens wollte ich über die Office Graph API ein Word Dokument auf Basis einer Vorlage erstellen und ein Paar Metadaten von dem zugehörigen SharePoint ListItem setzen. Es stellte sich heraus, dass es kein "straight-forward" Job ist, was letztendlich zu diesem Artikel führte.

Problem in a nutshell

Ich habe eine Bibliothek, in dieser Bibliothek sollen Dokumente basierend auf einen ContentType AkDocumentDoc angelegt werden können. Der Besagte ContentType ist vom Typ Document und hat ein Paar Spalten zusätzlich.

die Bibliothek

Der AkDocumentDoc ContentType hat noch ein Word Template hinterlegt, wodurch beim Erstellen eines Elements auf Basis von AkDocumentDoc zugleich ein Word Dokument mit dieser Vorlage angelegt wird. Die Vorlage nennt sich Prüf.dotx - mir ist nichts besseres eingefallen in der Regel verwende ich meistens bla oder blabla, es muss ja immer schnell gehen :)

Dokumentenvorlage: Prüf.dotx

Nun soll eine Logik - egal wie getriggert und egal wo - basierend auf diesem ContentType neue Elemente in dieser Bibliothek anlegen und die Metadaten von dem zugehörigen SharePoint ListItem setzen.

Alle Request in diesem Artikel können entweder über den Graph Explorer oder z.B. Postman abgesetzt werden. Für letzteres muss der OAuth2 Token zuerst beschafft werden und bei jeder Query hinzugefügt werden.

Die Umgebung in a nutshell

Für dieses Beispiel verwende ich eine fiktive O365 Subscription, für die jeweiligen Graph API Zugriffe benötigen wir verschiedene Informationen.

Die Umgebung(svariablen)

Es geht hier um die kirmizi Enterprises, die neuerdings in der M365 Welt zu Hause ist. Die besagte Dokumentenbibliothek befindet sich auf einer Sitecollection für die HR Abteilung, also /sites/halklailiskiler ... ich weiß ein Zungenbrecher. Die Bibliothek - in der die Dokumente angelegt werden sollen - beinhaltet alle Profildokumente von allen Mitarbeitern und hat den Titel Profillerimiz.

Das wären mal die Informationen, und die sollten uns zum sammeln der weiteren Informationen auch reichen, denn folgendes brauchen wir:

Name Wert Beschreibung
hostname kirmizient.sharepoint.com SharePoint Tenant
siteUrl https://kirmizient.sharepoint.com/sites/halkailiskiler Besagte SiteCollection
siteCollectionId abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a die ID der SiteCollection
siteId 953edbdd-3289-4efd-bfdd-18cb1e63e0fd die ID der Root Site
listId ab026011-fa6a-411a-ba00-1ff9d6185849 die ID der Bibliothek

Der hostname und siteUrl Parameter sollte uns vorliegen, genauso wie der Titel der Liste.

Die Site Id

Nun können wir über den Graph API Explorer unsere Site Selektieren, am einfachsten geht das mit der Search API.

https://graph.microsoft.com/v1.0/sites?search=halklailiskiler
die Query

Als Ergebnis sollte sowas wie hier rauskommen.

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites",
    "value": [
        {
            "createdDateTime": "2019-06-13T13:05:16Z",
            "id": "kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd",
            "lastModifiedDateTime": "2019-06-13T13:06:34Z",
            "name": "halklailiskiler",
            "webUrl": "https://kirmizient.sharepoint.com/sites/halklailiskiler",
            "displayName": "halklailiskiler",
            "root": {},
            "siteCollection": {
                "hostname": "kirmizient.sharepoint.com"
            }
        }
    ]
}
das Ergebnis

Wichtig ist hier der "id" key - kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd - für unsere weiteren Operationen über die Graph API werden wir diese benötigen. Ich nehme diesen Wert als "graph siteId" in die Tabelle mit auf.

Name Wert Beschreibung
hostname kirmizient.sharepoint.com SharePoint Tenant
siteUrl https://kirmizient.sharepoint.com/sites/halklailiskiler Besagte SiteCollection
siteCollectionId abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a die ID der SiteCollection
siteId 953edbdd-3289-4efd-bfdd-18cb1e63e0fd die ID der Root Site
listId ab026011-fa6a-411a-ba00-1ff9d6185849 die ID der Bibliothek
graph siteId kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd = <hostname>,<siteCollectionId>,<siteId>

Wie in der Tabelle ersichtlich besteht die graph siteId aus dem hostname, der siteCollectionId und der siteId.

Drive Id

Jetzt können wir die drive id von unserer Bibliothek herausfinden, die unterscheidet sich von der ListId aus SharePoint. An diese Information kommen wir wieder mit der Graph API ran.

https://graph.microsoft.com/v1.0/sites/<graph siteId>/drives
get drives request

Mit den Variablen aus der Tabelle würde der Request wie folgt aussehen.

https://graph.microsoft.com/v1.0/sites/kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd/drives

Von dem Ergebnis stellt der id Parameter die benötigte Information bereit.

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives",
    "value": [
        {
            "createdDateTime": "2020-05-12T09:35:06Z",
            "description": "",
            "id": "b!EWACgmr6GkF7BC_51hhYSXyTTYegpN1Gp-JagqufRnrd2z6VKzL9Tr_dGMseY-D9",
            "lastModifiedDateTime": "2020-05-15T09:58:43Z",
            "name": "Profillerimiz",
            "webUrl": "kirmizient.sharepoint.com/sites/halklailiskiler",
            "driveType": "documentLibrary",
            "createdBy": {
                "user": {
                    "email": "sevgi.böcügü@kirmizient.onmicrosoft.com",
                    "id": "cb7ab853-3ce0-985a-8499-bba138343cca",
                    "displayName": "Sevgi Böcügü"
                }
            },
            ....

Die drive id ist für unsere Library wäre b!EWACgmr6GkF7BC_51hhYSXyTTYegpN1Gp-JagqufRnrd2z6VKzL9Tr_dGMseY-D9. Nachdem diese Information auch in die Tabelle aufgenommen wurde, sind wir komplett.

Name Wert Beschreibung
hostname kirmizient.sharepoint.com SharePoint Tenant
siteUrl https://kirmizient.sharepoint.com/sites/halklailiskiler Besagte SiteCollection
siteCollectionId abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a die ID der SiteCollection
siteId 953edbdd-3289-4efd-bfdd-18cb1e63e0fd die ID der Root Site
listId ab026011-fa6a-411a-ba00-1ff9d6185849 die ID der Bibliothek
graph siteId kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd = <hostname>,<siteCollectionId>,<siteId>
graph drive id b!EWACgmr6GkF7BC_51hhYSXyTTYegpN1Gp-JagqufRnrd2z6VKzL9Tr_dGMseY-D9 Die drive id

Das war es auch schon, an die ListId kommt man entweder in dem man die Einstellungen der Liste/Bibliothek aufruft oder per Graph Explorer und folgender Query.

https://graph.microsoft.com/v1.0/sites/<graph siteId>/lists?$filter=displayname eq '<List title>'
query list by title

Hier wenden wir den filter Operator (OData) auf den List-Titel an. Mit den Informationen aus der Liste, würde die Query folgendermaßen aussehen.

https://graph.microsoft.com/v1.0/sites/kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd/lists?$filter=displayname eq 'Profillerimiz'

Nun weiter mit der Thematik.

Solution in a nutshell

Theoretisch ist dies mit CSOM, SharePoint REST oder per Hilfe von PnP (Core) ein sehr einfacher Task. Bei der Graph API ist der Weg nicht so trivial. Hier wird das Dokument über die OneDrive API angelegt, das befüllen der Metadaten erfolgt über die SharePoint API.

Sofern alles notwendige - Word Vorlage, ContentType, Bibliothek, die Tabelle mit den Variablen, etc. - vorhanden ist, kann über die Graph API das Vorhaben realisiert werden.

Für unser Vorhaben werden Graph API Berechtigungen für SharePoint - ich habe hier Sites.ReadWrite.All verwendet - und OneDirve - hier habe ich auch eine hohe Berechtigung File.ReadWrite.All verwendet - benötigt. Diese können im Graph Explorer gesetzt werden, sofern eine Anwendung im Hintergrund agieren soll, dann sollten diese in der App Registrierung im Azure AD gesetzt werden.

Die Beispiel Variablen werden aus dieser Tabelle hergenommen.

Name Wert Beschreibung
hostname kirmizient.sharepoint.com SharePoint Tenant
siteUrl https://kirmizient.sharepoint.com/sites/halklailiskiler Besagte SiteCollection
siteCollectionId abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a die ID der SiteCollection
siteId 953edbdd-3289-4efd-bfdd-18cb1e63e0fd die ID der Root Site
listId ab026011-fa6a-411a-ba00-1ff9d6185849 die ID der Bibliothek
graph siteId kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd = <hostname>,<siteCollectionId>,<siteId>
graph drive id b!EWACgmr6GkF7BC_51hhYSXyTTYegpN1Gp-JagqufRnrd2z6VKzL9Tr_dGMseY-D9 Die drive id

Dokument erstellen (OneDrive API)

Dokumente werden über einen PUT Request gegenüber die OneDrive API realisiert.

https://graph.microsoft.com/v1.0/sites/<graph siteId>/drives/<graph drive id>/<folder>:/<filename>.<file extension>:/content
create document PUT request

Da meine Dokumente direkt auf root Ebene innerhalb der Bibliothek abgelegt werden, muss ich beim <folder> Platzhalter root angeben. Sofern eine Hierarchie vorhanden sein sollte, muss der Pfad an dieser Stelle angegeben werden.

Mein Dokument soll akblogtest123 heißen und vom typ docx sein. Mit den Beispielen aus der Tabelle und den beiden zusätzlichen Informationen, würde die Query wie folgt aussehen.

https://graph.microsoft.com/v1.0/sites/kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd/drives/b!EWACgmr6GkF7BC_51hhYSXyTTYegpN1Gp-JagqufRnrd2z6VKzL9Tr_dGMseY-D9/root:/akblogtest123.docx:/content

Nun können wir um Graph Explorer die Query reinkopieren und als Protokoll PUT auswählen. Als Request Header sollte noch Content-Type: text/plain gesetzt werden.

Bei Erfolg wird dann ein langes JSON inkl. Download Link zurückgeliefert. Ausserdem sollte das Dokument in der Dokumentenbibliothek aufgezeigt werden.

neues Dokument

Dokument herausfinden (SharePoint API)

Um die Metadaten von unserem Dokument erfolgreich setzen zu können, müssen wir an unser Dokument über die SharePoint API kommen. Leider bietet die Anwort von der OneDrive API bei/nach der Erstellung keinen Wert, den wir mit der SharePoint API weiter verwenden können.

An diese Information kommen wir mit folgendem GET Request. Hier muss darauf geachtet werden im Header Prefer: HonorNonIndexedQueriesWarningMayFailRandomly mitzugeben. Das FileLeafRef Feld ist nicht indiziert => Ressourcenhungrigere Abfrage, diese muss mit Prefer bestätigt werden.

https://graph.microsoft.com/v1.0/sites/<graph siteId>/lists/<listId>/items?select=id&filter=fields/FileLeafRef eq '<filename>.<file extension>'
get document GET request

Mit den Vorgaben und Parametern die wir definiert/gefunden haben, würde die Query wei folgt aussehen.

https://graph.microsoft.com/v1.0/sites/kirmizient.sharepoint.com,abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a,953edbdd-3289-4efd-bfdd-18cb1e63e0fd/lists/ab026011-fa6a-411a-ba00-1ff9d6185849/items?select=id&filter=fields/FileLeafRef eq 'akblogtest123.docx'

Die Antwort auf diese Abfrage ist relativ klein, diese besteht aus der SharePoint ListItem ID.

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('kirmizient.sharepoint.com%2C8abcdd937c-a4a0-46dd-a7e2-5a82ab9f467a%2C8953edbdd-3289-4efd-bfdd-18cb1e63e0fd')/lists('ab026011-fa6a-411a-ba00-1ff9d6185849')/items",
    "value": [
        {
            "@odata.etag": "\"75c3eb58-4ccc-4abe-8ef2-573eb5053cce,1\"",
            "id": "11"
        }
    ]
}

Unser neues Dokument hat die ID 11, diese ID brauchen wir bei unserem nächsten Task.

Dokument aktualisieren ( SharePoint API)

Nun können wir die Metadaten von unserem Dokument aktualisieren, hierfür werden wir das PATCH Verfahren verwenden. Zusätzlich müssen wir Änderungen im Request Header vorgeben und ein JSON als Body mitgeben. Der Request an sich würde wie folgt aussehen.

https://graph.microsoft.com/v1.0/sites/<graph siteId>/lists/<listId>/items/items/<new item id>
update document/item PATCH request
Content-Type: application/json
{
	"fields":
	{
		"Title":"Test-Blog-AK",
		"FileLeafRef":"akblogtest123-update.docx",
		"ContentType":"AkDocumentDoc",
		"Test1":true,
		"Test2":false
	}
}
Request Body

So wir setzen den Titel auf "Test-Blog-AK", ändern den Dateinamen zu "akblogtest123-update.docx", geben den ContentType mit "AkDocumentDoc" und setzen zusätzlich die boolischen Spalten Test1 und Test2.

Der finale Query würde wie folgt aussehen.

https://graph.microsoft.com/v1.0/sites/kirmizi.sharepoint.com,82026011-fa6a-411a-ba00-1ff9d6185849,874d937c-a4a0-46dd-a7e2-5a82ab9f467a/lists/ab026011-fa6a-411a-ba00-1ff9d6185849/items/items/11

Das Ergebnis

Wir bekommen eine Antwort - JSON - in der auch unsere aktuellen Änderungen enthalten sind. In der Bibliothek sieht das Bild folgendermaßen aus.

Update erfolgreich!

Fertig in a nutshell (nicht)!

Das war es dann auch. Was man mit dieser Information hier genau macht ist jedem selbst überlassen. Das Schreiben dauert wieder einmal länger als das Herausfinden/Umsetzen. In Zukunft werde ich das tatsächlich in a nutshell halten... oder auch nicht ;-)