Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Cette bibliothèque démontre comment :
Utilisation des données de configuration transmises à la bibliothèque
Gestion des erreurs retournées par l’API Web Dataverse
Aider à maintenir le code DRY et encourager la réutilisation.
Modèle de réutilisation du code par :
- Toutes les opérations passent par une méthode Send commune qui accepte une seule instance de classe Request et ajoute des en-têtes courants, notamment
Authorization. - Fourniture d’une méthode Batch qui accepte les classes Request et retourne les classes Response .
- Chaque méthode fournie représente un exemple montrant comment construire une
Requestinstance qui peut être utilisée avec laBatchméthode.
- Toutes les opérations passent par une méthode Send commune qui accepte une seule instance de classe Request et ajoute des en-têtes courants, notamment
Note
Cet exemple de bibliothèque est un helper qui est utilisé par tous les exemples d’API web côté client JavaScript Dataverse, mais ce n’est pas un SDK. Il est testé uniquement pour confirmer que les exemples qui l’utilisent s’exécutent correctement. Cet exemple de code est fourni « tel quel » sans garantie de réutilisation.
Cette bibliothèque ne gèrent pas les actions suivantes :
- Gérer l’authentification. Cela dépend d’une fonction passée depuis une application qui fournit le jeton à utiliser.
- Fournir toutes les capacités de génération de code. Toutes les méthodes utilisées dans les exemples sont écrites manuellement. Toutes les données d’entité métier utilisent des objets JavaScript plutôt qu’une classe représentant le type d’entité.
- Fournir un modèle d’objet pour composer des requêtes OData. Toutes les requêtes affichent la syntaxe de requête OData en tant que paramètres de requête.
Cette bibliothèque contient des définitions des classes suivantes :
| classe | Descriptif |
|---|---|
Client |
Représente le client d’API web Dataverse. Il fournit des méthodes permettant d’interagir avec l’API Web Dataverse. |
ChangeSet |
Représente un ensemble de modifications utilisées avec le traitement par lots. Toutes les demandes au sein de l’ensemble de modifications doivent réussir ou échouer en tant que groupe. |
Vous trouverez le code de cette bibliothèque dans DataverseWebAPI.js exemple de code de bibliothèque et également sur GitHub sur PowerApps-Samples/blob/master/dataverse/webapi/JS/SPASample/src/scripts/DataverseWebAPI.js.
Classe Client
Représente le client d’API web Dataverse. Il fournit des méthodes permettant d’interagir avec l’API Web Dataverse.
Constructeur client
constructor(baseUrl, getTokenFunc, version = "9.2")
Crée une instance du Client.
-
Paramètres :
-
baseUrl(chaîne) : URL de base de l’API Dataverse. -
getTokenFunc(fonction) : fonction qui retourne un jeton d’accès. -
version(chaîne, facultatif) : chaîne pour remplacer la version par défaut. La valeur par défaut est"9.2".
-
Méthodes publiques
Note
Toutes les méthodes publiques sont asynchrones.
Send(request)
Envoie une requête HTTP à l’aide de fetch avec les en-têtes standard requis, y compris ceux de Authorization. Toutes les autres méthodes publiques utilisent cette méthode pour envoyer la requête.
Paramètres :
-
request(Demande) : La demande à envoyer.
-
Renvoie :
Promise<Response|Error>: réponse de l’appel fetch ou une erreur si la requête échoue.
WhoAmI()
Récupère des informations sur l’utilisateur actuel en appelant la fonction WhoAmI.
-
Renvoie :
Promise<Object|Error>: une promesse qui est résolue dans les informations de l’utilisateur au format JSON, ou une erreur si la requête échoue.
Create(entitySetName, data)
Crée un enregistrement dans le jeu d’entités spécifié, comme décrit dans Créer une ligne de table à l’aide de l’API Web.
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités où la nouvelle entité sera créée. -
data(Objet) : données de la nouvelle entité.
-
Renvoie :
Promise<Object|Error>: promesse qui se résout en objet contenant l’ID de l’entité créée, ou une erreur en cas d’échec de la requête.
Retrieve(entitySetName, id, query = null, includeAnnotations = true)
Récupère un enregistrement à partir de l’ensemble d’entités spécifié par ID, avec des options de requête facultatives, comme décrit dans Récupérer une ligne de table à l’aide de l’API Web.
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités à partir duquel récupérer l’entité. -
id(chaîne) : ID de l’entité à récupérer. -
query(chaîne, facultatif) : options de requête OData à appliquer. -
includeAnnotations(booléen, facultatif) : indique si les annotations OData sont retournées dans la réponse. La valeur par défaut esttrue.
-
Renvoie :
Promise<Object|Error>: une promesse qui est résolue dans l’entité récupérée au format JSON, ou une erreur si la requête échoue.
Refresh(record, primarykeyName)
Actualise l’enregistrement donné en récupérant les données les plus récentes du serveur à l’aide de la récupération conditionnelle.
Paramètres :
-
record(Objet) : enregistrement à actualiser. Doit contenir les propriétés@odata.etaget@odata.context. -
primarykeyName(chaîne) : nom de la propriété de clé primaire dans l’enregistrement.
-
Renvoie :
Promise<Object>: enregistrement actualisé.
CreateRetrieve(entitySetName, data, query, includeAnnotations = true)
Crée et récupère un enregistrement à partir du jeu d’entités spécifié, comme décrit dans la création avec les données retournées.
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités. -
data(Objet) : données à envoyer dans le corps de la requête. -
query(chaîne, facultatif) : chaîne de requête à ajouter à l’URL du jeu d’entités. -
includeAnnotations(booléen, facultatif) : indique s’il faut inclure des annotations OData dans la réponse. La valeur par défaut esttrue.
-
Renvoie :
Promise<Object>: données de réponse en tant qu’objet JSON.
RetrieveMultiple(collectionResource, query, maxPageSize = 100, includeAnnotations = true)
Récupère plusieurs enregistrements d’une collection d’ensembles d’entités spécifiée avec des paramètres de requête facultatifs, comme décrit dans l’utilisation d’OData pour interroger des données.
Paramètres :
-
collectionResource(chaîne) : nom de l’ensemble d’entités ou d’une expression de collection filtrée à partir de laquelle récupérer des enregistrements. -
query(chaîne) : options de requête OData à appliquer. -
maxPageSize(nombre, facultatif) : nombre maximal d’enregistrements à récupérer par page. La valeur par défaut est100. -
includeAnnotations(booléen, facultatif) : indique s’il faut inclure des annotations OData dans la réponse. La valeur par défaut esttrue.
-
Renvoie :
Promise<object>: réponse du serveur contenant les entités récupérées.
GetNextLink(nextLink, maxPageSize = 100, includeAnnotations = true)
Récupère la page suivante d’enregistrements d’une collection d’ensembles d’entités spécifiée à l’aide de la @odata.nextLink valeur décrite dans les résultats de la page.
Paramètres :
-
nextLink(chaîne) :@odata.nextLinkvaleur de la réponse précédente. -
maxPageSize(nombre, facultatif) : nombre maximal d’enregistrements à récupérer par page. La valeur par défaut est100. -
includeAnnotations(booléen, facultatif) : indique s’il faut inclure des annotations OData dans la réponse. La valeur par défaut esttrue.
-
Renvoie :
Promise<object>: réponse du serveur contenant les entités récupérées.
FetchXml(entitySetName, fetchXml)
Récupère de façon asynchrone des données à partir d’un jeu d’entités spécifié à l’aide de FetchXML, comme décrit dans l’utilisation de FetchXml pour récupérer des données.
Paramètres :
-
entitySetName(chaîne) : nom de l’entité définie à interroger. -
fetchXml(chaîne) : La chaîne de requête FetchXML.
-
Renvoie :
Promise<Object>: réponse JSON du serveur.
GetCollectionCount(collectionResource)
Récupère de façon asynchrone le nombre d’éléments d’une collection spécifiée, comme décrit dans les lignes de comptage.
Paramètres :
-
collectionResource(chaîne) : URL de ressource de la collection.
-
Renvoie :
Promise<number>: nombre d’éléments dans la collection, jusqu’à5000.
Update(entitySetName, id, data, etag = null)
Met à jour un enregistrement dans le jeu d’entités spécifié par ID avec les données fournies, comme décrit dans la mise à jour de base.
Paramètres :
-
entitySetName(chaîne) : nom du jeu d’entités où existe l’enregistrement. -
id(chaîne) : ID de l’enregistrement à mettre à jour. -
data(Objet) : les données pour mettre à jour l’enregistrement. -
etag(chaîne, facultatif) : spécifiez la valeur etag pour empêcher la mise à jour lorsqu’un enregistrement plus récent existe.
-
Renvoie :
Promise<Response|Error>: une promesse qui est résolue dans la réponse de l’opération de mise à jour, ou une erreur si la requête échoue.
Delete(entitySetName, id, etag = null)
Supprime une entité du jeu d’entités spécifié par ID, comme décrit dans la mise à jour de base
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités à partir duquel supprimer l’entité. -
id(chaîne) : ID de l’entité à supprimer. -
etag(chaîne, facultatif) : spécifiez la valeur etag pour empêcher la suppression lorsqu’un enregistrement plus récent existe.
-
Renvoie :
Promise<Response|Error>: une promesse qui est résolue dans la réponse de l’opération de suppression, ou une erreur si la requête échoue.
SetValue(entitySetName, id, columnName, value)
Définit la valeur d’une colonne spécifiée pour un enregistrement donné, comme décrit dans la mise à jour d’une valeur de propriété unique.
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités. -
id(chaîne) : ID de l’enregistrement. -
columnName(chaîne) : nom logique de la colonne pour lequel définir la valeur. -
value(*) : valeur à définir pour la colonne spécifiée.
-
Renvoie :
Object: réponse du serveur.
GetValue(entitySetName, id, columnName)
Récupère la valeur d’une colonne spécifiée pour un enregistrement donné, comme décrit dans la récupération d’une valeur de propriété unique
Paramètres :
-
entitySetName(chaîne) : nom de l’ensemble d’entités. -
id(chaîne) : ID de l’enregistrement. -
columnName(chaîne) : nom de la colonne à partir duquel récupérer la valeur.
-
Renvoie :
Object: réponse du serveur.
Associate(targetSetName, targetId, navigationProperty, relatedSetName, relatedId)
Associe des enregistrements en créant des données dans la relation pour les lier, comme décrit dans l’ajout d’un enregistrement à une collection.
Paramètres :
-
targetSetName(chaîne) : nom de l’ensemble d’entités cibles. -
targetId(string|number) : ID de l’enregistrement cible. -
navigationProperty(chaîne) : propriété de navigation qui définit la relation. -
relatedSetName(chaîne) : nom de l’ensemble d’entités associé. -
relatedId(string|number) : ID de l’enregistrement à associer à la cible.
-
Renvoie :
Promise<object>: réponse du serveur après avoir créé l’association.
Disassociate(targetSetName, targetId, navigationProperty, relatedId)
Dissocie un enregistrement d’un autre enregistrement en supprimant des données dans la relation pour les lier, comme décrit dans la suppression d’un enregistrement d’une collection.
Paramètres :
-
targetSetName(chaîne) : nom de l’ensemble d’entités cibles. -
targetId(string|guid) : ID de l’enregistrement cible. -
navigationProperty(chaîne) : propriété de navigation qui définit la relation. -
relatedId(string|guid) : ID de l’enregistrement associé.
-
Renvoie :
Promise<object>: réponse du serveur après avoir supprimé l’association.
getBatchBody(request, id, inChangeSet = false)
Réservé exclusivement à un usage interne. Cette méthode est publique, car elle est utilisée par la classe ChangeSet. Il n’existe aucun scénario dans lequel vous devez utiliser cette méthode lors de l’utilisation de cette bibliothèque.
Batch(requests, continueOnError = false)
Envoie une demande de traitement par lots contenant plusieurs (éléments Request ou ChangeSet , comme décrit dans Exécuter des opérations de traitement par lots à l’aide de l’API web.
Paramètres :
Renvoie :
Promise<Array<Response>>: réponse analysée de la requête par lots.
Classe ChangeSet
Représente un ensemble de modifications utilisées avec le traitement par lots. Toutes les demandes au sein de l’ensemble de modifications doivent réussir ou échouer en tant que groupe.
Constructeur ChangeSet
constructor(requests)
Crée une instance de ChangeSet.
-
Paramètres :
-
requests(Tableau<de Requêtes>): Un tableau d’objets Requête.
-
Propriétés
-
requests(Tableau<Requête>) : tableau d’objets Request dans l’ensemble de modifications.
Méthodes
-
getChangeSetText(batchId): pour une utilisation interne uniquement. Obtient le texte de l’ensemble de modifications dans l’opération$batch. Cette méthode est publique, car elle est utilisée par la méthode Batch de la classe cliente. Il n’existe aucun scénario dans lequel vous devez utiliser cette méthode directement avec cette bibliothèque.
DataverseWebAPI.js exemple de code de bibliothèque
Voici le code de l’exemple de bibliothèque DataverseWebAPI.js
/**
* @class Client
* @classdesc This class represents the Dataverse Web API Client.
*/
class Client {
// The base URL for the Dataverse Web API
// Something like: https://your-org.api.crm.dynamics.com/api/data/v9.2/
#apiEndpoint;
// The function to get an access token
#getTokenFunc;
/**
* Creates an instance of Client.
*
* @constructor
* @param {string} baseUrl - The base URL for the Dataverse API.
* @param {function} getTokenFunc - A function that returns an access token.
* @param {string} version - A string to override the default version (9.2)
*/
constructor(baseUrl, getTokenFunc, version = "9.2") {
const path = `/api/data/v${version}/`;
this.#apiEndpoint = baseUrl + path;
this.#getTokenFunc = getTokenFunc;
}
//#region Validation Functions
/**
* Checks if a given string is a valid GUID.
*
* @param {string} str - The string to be tested.
* @returns {boolean} - Returns true if the string is a valid GUID, otherwise false.
*/
#isGUID(str) {
const guidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
return guidRegex.test(str);
}
/**
* Validates if a given variable is an object that can be used with JSON.stringify.
*
* @param {any} obj - The variable to be tested.
* @returns {boolean} - Returns true if the variable is a valid object for JSON.stringify, otherwise false.
*/
#isValidObjectForJSON(obj) {
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
}
/**
* Validates if a given string is a valid XML document.
*
* @param {string} xmlString - The XML string to be tested.
* @returns {boolean} - Returns true if the string is a valid XML document, otherwise false.
*/
#isValidXML(xmlString) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
const parserError = xmlDoc.getElementsByTagName("parsererror");
return parserError.length === 0;
}
/**
* Validates if a given string is a weak ETag.
*
* @param {string} etag - The ETag string to be tested.
* @returns {boolean} - Returns true if the string is a valid weak ETag, otherwise false.
*/
#isValidWeakETag(etag) {
const weakEtagRegex = /^W\/"([0-9a-fA-F]+)"$/;
const result = weakEtagRegex.test(etag);
if (!result) {
console.error(`The etag ${etag} is not a valid weak ETag.`);
}
return result;
}
//#endregion Validation Functions
/**
* Gets the API endpoint.
*
* @returns {string} The API endpoint.
*/
get apiEndpoint() {
return this.#apiEndpoint;
}
/**
* Sends a request to the Dataverse Web API.
*
* @async
* @function Send
* @param {Request} request - The request to be sent.
* @returns {Promise<Response>} The response from the server.
* @throws {Error} Throws an error if the request is not a valid Request instance or if the request fails.
*/
async Send(request) {
if (!request instanceof Request) {
throw new Error(`request parameter value '${request}' is not a Request.`);
}
const token = await this.#getTokenFunc();
const baseHeaders = new Headers({
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json",
"OData-Version": "4.0",
"OData-MaxVersion": "4.0",
});
const combinedHeaders = new Headers(baseHeaders);
request.headers.forEach((value, key) => {
combinedHeaders.set(key, value);
});
const requestOptions = {
method: request.method,
headers: combinedHeaders,
};
if (request.body) {
requestOptions.body = await request.text();
}
const requestCopy = new Request(new URL(request.url), requestOptions);
try {
const response = await fetch(requestCopy);
if (!response.ok) {
// Handle 304 Not Modified for GET requests
if (response.status === 304 && request.method === "GET") {
return response;
}
const error = await response.json();
console.error(error);
throw new Error(error.error.message);
}
return response;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Retrieves information about the current user by calling the "WhoAmI" message.
*
* @async
* @function WhoAmI
* @returns {Promise<Object|Error>} A promise that resolves to the user information in JSON format, or an error if the request fails.
* @throws {Error} If the request fails.
*/
async WhoAmI() {
try {
const request = new Request(new URL("WhoAmI", this.apiEndpoint), {
method: "GET",
});
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Creates a new record in the specified entity set.
*
* @async
* @function Create
* @param {string} entitySetName - The name of the entity set where the new entity will be created.
* @param {Object} data - The data for the new entity.
* @returns {Promise<Object|Error>} A promise that resolves to an object containing the ID of the created entity, or an error if the request fails.
* @throws {Error} If the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/create-entity-web-api
*/
async Create(entitySetName, data) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isValidObjectForJSON(data)) {
throw new Error(
"data parameter value is not a valid object to convert to JSON."
);
}
//#endregion Parameter Validation
const request = new Request(new URL(entitySetName, this.apiEndpoint), {
method: "POST",
body: JSON.stringify(data),
});
request.headers.set("Content-Type", "application/json");
try {
const response = await this.Send(request);
const url = response.headers.get("OData-EntityId");
// Extract the ID from the OData-EntityId header value
const id = url.substring(url.lastIndexOf("(") + 1, url.lastIndexOf(")"));
return id;
} catch (error) {
throw error;
}
}
/**
* Retrieves a record from the specified entity set by ID, with optional query options.
*
* @async
* @function Retrieve
* @param {string} entitySetName - The name of the entity set from which to retrieve the entity.
* @param {string} id - The ID of the entity to retrieve.
* @param {string} [query] - The odata query options to apply
* @param {boolean} [includeAnnotations] - Whether OData annotations are returned in the response. Default value is true.
* @returns {Promise<Object|Error>} A promise that resolves to the retrieved entity in JSON format, or an error if the request fails.
* @throws {Error} If the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/retrieve-entity-using-web-api
*/
async Retrieve(entitySetName, id, query = null, includeAnnotations = true) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isGUID(id)) {
throw new Error(`The id ${id} is not a valid GUID.`);
}
if (includeAnnotations && typeof includeAnnotations !== "boolean") {
throw new Error("includeAnnotations parameter value is not a boolean.");
}
//#endregion Parameter Validation
let resource = `${entitySetName}(${id})`;
if (query) {
resource += query.startsWith("?") ? query : `?${query}`;
}
const request = new Request(new URL(resource, this.apiEndpoint), {
method: "GET",
});
if (includeAnnotations) {
request.headers.set("Prefer", 'odata.include-annotations="*"');
}
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Refreshes the given record by fetching the latest data from the server.
*
* @async
* @param {Object} record - The record to refresh. Must contain @odata.etag and @odata.context properties.
* @param {string} primarykeyName - The name of the primary key property in the record.
* @throws {Error} If the record does not have the required @odata.etag and @odata.context properties.
* @throws {Error} If the entity set name and columns cannot be extracted from the record.
* @throws {Error} If the id cannot be extracted from the record using the primary key name.
* @returns {Promise<Object>} The refreshed record.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/perform-conditional-operations-using-web-api#conditional-retrievals
*/
async Refresh(record, primarykeyName) {
//#region Parameter Validation
const errorMessage =
"record parameter value doesn't have required @odata.etag and @odata.context properties.";
const etag = record["@odata.etag"];
const context = record["@odata.context"];
if (!etag || !context) {
throw new Error(errorMessage);
}
if (!this.#isValidWeakETag(etag)) {
throw new Error("extracted etag value is not a valid ETag value.");
}
const columnsMatch = context.match(/\(([^)]+)\)/);
const entitySetNameMatch = context.match(/#(\w+)\(/);
if (!entitySetNameMatch || !columnsMatch) {
throw new Error(
"Cannot extract entity set name and columns from record."
);
}
if (primarykeyName && typeof primarykeyName !== "string") {
throw new Error("primarykeyName parameter value is not a string.");
}
const id = record[primarykeyName];
if (!id) {
throw new Error(`Can't extract id from record using ${primarykeyName}.`);
}
if (!this.#isGUID(id)) {
throw new Error(
`The ${primarykeyName} value '${id}' is not a valid GUID.`
);
}
//#endregion Parameter Validation
// This operation can't use the Prefer: odata.include-annotations="*" header
// because it will never return 304 Not Modified.
const columns = columnsMatch[1].split(",");
// This operation can't use any $expand query options
// because it will never return 304 Not Modified.
const query = `$select=${columns}`;
const entitySetName = entitySetNameMatch[1];
let resource = `${entitySetName}(${id})?${query}`;
const request = new Request(new URL(resource, this.apiEndpoint), {
method: "GET",
});
// This operation can't use the Prefer: odata.include-annotations="*" header
// because it could never return 304 Not Modified when that header is set.
request.headers.set("If-None-Match", etag);
try {
const response = await this.Send(request);
if (!response.ok) {
if (response.status === 304) {
console.log("The record was NOT modified on the server.");
return record;
} else {
const error = await response.json();
console.error(error);
throw new Error(error.error.message);
}
}
console.log("The record WAS modified on the server.");
return response.json();
} catch (error) {
throw error;
}
}
/**
* Creates and retrieves a record from the specified entity set.
*
* @async
* @param {string} entitySetName - The name of the entity set.
* @param {Object} data - The data to be sent in the request body.
* @param {string} [query] - The query string to be appended to the entity set URL.
* @param {boolean} [includeAnnotations=true] - Whether to include OData annotations in the response.
* @returns {Promise<Object>} The response data as a JSON object.
* @throws {Error} If the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/create-entity-web-api#create-with-data-returned
*/
async CreateRetrieve(entitySetName, data, query, includeAnnotations = true) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isValidObjectForJSON(data)) {
throw new Error(
"data parameter value is not a valid object to convert to JSON."
);
}
if (includeAnnotations && typeof includeAnnotations !== "boolean") {
throw new Error(
"includeAnnotations parameter value is not a boolean value."
);
}
//#endregion Parameter Validation
let resource = entitySetName;
if (query) {
resource += query.startsWith("?") ? query : `?${query}`;
}
const request = new Request(new URL(resource, this.apiEndpoint), {
method: "POST",
body: JSON.stringify(data),
});
request.headers.set("Content-Type", "application/json");
let preferHeaders = ["return=representation"];
if (includeAnnotations) {
preferHeaders.push('odata.include-annotations="*"');
}
request.headers.set("Prefer", preferHeaders.join(","));
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Retrieves multiple records from a specified entity set collection with optional query parameters.
*
* @async
* @function RetrieveMultiple
* @param {string} collectionResource - The name of the entity set or a filtered collection expression to retrieve records from.
* @param {string} query - The OData query options to apply.
* @param {number} maxPageSize - The maximum number of records to retrieve per page defaults to 100.
* @param {boolean} [includeAnnotations = true] - Whether to include OData annotations in the response.
* @returns {Promise<object>} The response from the server containing the retrieved entities.
* @throws {Error} Throws an error if the retrieval fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/query/overview
*/
async RetrieveMultiple(
collectionResource,
query,
maxPageSize = 100,
includeAnnotations = true
) {
//#region Parameter Validation
if (typeof collectionResource !== "string") {
throw new Error("collectionResource parameter value is not a string.");
}
if (maxPageSize && typeof maxPageSize !== "number") {
throw new Error("maxPageSize parameter value is not a number value.");
}
if (includeAnnotations && typeof includeAnnotations !== "boolean") {
throw new Error(
"includeAnnotations parameter value is not a boolean value."
);
}
//#endregion Parameter Validation
if (query) {
collectionResource += query.startsWith("?") ? query : `?${query}`;
}
const request = new Request(new URL(collectionResource, this.apiEndpoint), {
method: "GET",
});
let preferHeaders = [`odata.maxpagesize=${maxPageSize}`];
if (includeAnnotations) {
preferHeaders.push('odata.include-annotations="*"');
}
request.headers.set("Prefer", preferHeaders.join(","));
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Retrieves the next page of records from a specified entity set collection using the @odata.nextLink value.
*
* @async
* @function GetNextLink
* @param {string} nextLink the @odata.nextLink value from the previous response
* @param {number} maxPageSize - The maximum number of records to retrieve per page defaults to 100.
* @param {boolean} [includeAnnotations=true] - Whether to include OData annotations in the response.
* @returns {Promise<object>} The response from the server containing the retrieved entities.
* @throws {Error} Throws an error if the retrieval fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/query/page-results
*/
async GetNextLink(nextLink, maxPageSize = 100, includeAnnotations = true) {
//#region Parameter Validation
if (typeof nextLink !== "string") {
throw new Error("nextLink parameter value is not a string.");
}
if (maxPageSize && typeof maxPageSize !== "number") {
throw new Error("maxPageSize parameter value is not a number value.");
}
if (includeAnnotations && typeof includeAnnotations !== "boolean") {
throw new Error(
"includeAnnotations parameter value is not a boolean value."
);
}
//#endregion Parameter Validation
const request = new Request(new URL(nextLink), {
method: "GET",
});
let preferHeaders = [`odata.maxpagesize=${maxPageSize}`];
if (includeAnnotations) {
preferHeaders.push('odata.include-annotations="*"');
}
request.headers.set("Prefer", preferHeaders.join(","));
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Asynchronously fetches data from a specified entity set using FetchXML.
*
* @async
* @function FetchXml
* @param {string} entitySetName - The name of the entity set to query.
* @param {string} fetchXml - The FetchXML query string.
* @returns {Promise<Object>} The JSON response from the server.
* @throws Will throw an error if the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/fetchxml/retrieve-data?tabs=webapi
*/
async FetchXml(entitySetName, fetchXml) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isValidXML(fetchXml)) {
throw new Error(
"fetchXml parameter string value doesn't represent a valid XML document."
);
}
//#endregion Parameter Validation
const encodedFetchXml = encodeURIComponent(fetchXml).replace(/%20/g, "+");
const request = new Request(
new URL(`${entitySetName}?fetchXml=${encodedFetchXml}`, this.apiEndpoint),
{
method: "GET",
}
);
request.headers.set("Prefer", 'odata.include-annotations="*"');
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Asynchronously retrieves the count of items in a specified collection.
*
* @async
* @function GetCollectionCount
* @param {string} collectionResource - The resource URL of the collection.
* @returns {Promise<number>} The count of items in the collection, up to 5000
* @throws Will throw an error if the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/query/count-rows
*/
async GetCollectionCount(collectionResource) {
if (typeof collectionResource !== "string") {
throw new Error(
"collectionResource parameter value is not a valid string."
);
}
const request = new Request(
new URL(`${collectionResource}/$count`, this.apiEndpoint),
{
method: "GET",
}
);
try {
const response = await this.Send(request);
return response.json();
} catch (error) {
throw error;
}
}
/**
* Updates a record in the specified entity set by ID with the provided data.
*
* @async
* @function Update
* @param {string} entitySetName - The name of the entity set where the record exists.
* @param {string} id - The ID of the record to update.
* @param {Object} data - The data to update the record with.
* @param {string} etag - Specify the etag value to prevent update when newer record exists. (optional)
* @returns {Promise<Response|Error>} A promise that resolves to the response of the update operation, or an error if the request fails.
* @throws {Error} If the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/update-delete-entities-using-web-api#basic-update
*/
async Update(entitySetName, id, data, etag = null) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a valid string.");
}
if (!this.#isGUID(id)) {
throw new Error(`The id ${id} is not a valid GUID.`);
}
if (!this.#isValidObjectForJSON(data)) {
throw new Error(
"data parameter value is not a valid object to convert to JSON."
);
}
if (etag && !this.#isValidWeakETag(etag)) {
throw new Error("etag parameter value is not a valid ETag value.");
}
//#endregion Parameter Validation
// Prevents create operation
let ifMatchHeaderValue = "*";
if (etag) {
// Prevents update when newer record exists
ifMatchHeaderValue = etag;
}
const request = new Request(
new URL(`${entitySetName}(${id})`, this.apiEndpoint),
{
method: "PATCH",
body: JSON.stringify(data),
}
);
request.headers.set("Content-Type", "application/json");
request.headers.set("If-Match", ifMatchHeaderValue);
try {
const response = await this.Send(request);
return response;
} catch (error) {
throw error;
}
}
/**
* Deletes an entity from the specified entity set by ID.
*
* @async
* @function Delete
* @param {string} entitySetName - The name of the entity set from which to delete the entity.
* @param {string} id - The ID of the entity to delete.
* @param {string} etag - Specify the etag value to prevent delete when newer record exists. (optional)
* @returns {Promise<Response|Error>} A promise that resolves to the response of the delete operation, or an error if the request fails.
* @throws {Error} If the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/update-delete-entities-using-web-api#basic-delete
*/
async Delete(entitySetName, id, etag = null) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isGUID(id)) {
throw new Error(`The id ${id} is not a valid GUID.`);
}
if (etag && typeof etag !== "string") {
throw new Error("etag parameter value is not a string.");
}
if (etag && !this.#isValidWeakETag(etag)) {
throw new Error("etag parameter value is not a valid ETag value.");
}
//#endregion Parameter Validation
const request = new Request(
new URL(`${entitySetName}(${id})`, this.apiEndpoint),
{
method: "DELETE",
}
);
if (etag) {
request.headers.set("If-Match", etag);
}
try {
const response = await this.Send(request);
return response;
} catch (error) {
throw error;
}
}
/**
* Sets the value of a specified column for a given record.
* @async
* @function SetValue
* @param {string} entitySetName - The name of the entity set.
* @param {string} id - The ID of the record.
* @param {string} columnName - The logical name of the column to set the value for.
* @param {*} value - The value to set for the specified column.
* @returns {Object} The response from the server.
* @throws Will throw an error if the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/update-delete-entities-using-web-api#update-a-single-property-value
*/
async SetValue(entitySetName, id, columnName, value) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isGUID(id)) {
throw new Error(`The id ${id} is not a valid GUID.`);
}
if (typeof columnName !== "string") {
throw new Error("columnName parameter value is not a string.");
}
if (value === undefined) {
throw new Error("value parameter value is undefined.");
}
//#endregion Parameter Validation
const request = new Request(
new URL(`${entitySetName}(${id})/${columnName}`, this.apiEndpoint),
{
method: "PUT",
body: JSON.stringify({ value: value }),
}
);
request.headers.set("Content-Type", "application/json");
request.headers.set("If-None-Match", null);
try {
const response = await this.Send(request);
return response;
} catch (error) {
throw error;
}
}
/**
* Retrieves the value of a specified column for a given record.
* @async
* @function GetValue
* @param {string} entitySetName - The name of the entity set.
* @param {string} id - The ID of the record.
* @param {string} columnName - The name of the column to retrieve the value from.
* @returns {Object} The response from the server.
* @throws Will throw an error if the request fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/retrieve-entity-using-web-api#retrieve-a-single-property-value
*/
async GetValue(entitySetName, id, columnName) {
//#region Parameter Validation
if (typeof entitySetName !== "string") {
throw new Error("entitySetName parameter value is not a string.");
}
if (!this.#isGUID(id)) {
throw new Error(`The id ${id} is not a valid GUID.`);
}
if (typeof columnName !== "string") {
throw new Error("columnName parameter value is not a string.");
}
//#endregion Parameter Validation
const request = new Request(
new URL(`${entitySetName}(${id})/${columnName}`, this.apiEndpoint),
{
method: "GET",
}
);
request.headers.set("If-None-Match", null);
try {
const response = await this.Send(request);
const data = await response.json();
return data.value;
} catch (error) {
throw error;
}
}
/**
* Associates records by creating data in the relationship to link them.
*
* @async
* @function Associate
* @param {string} targetSetName - The name of the target entity set.
* @param {string|number} targetId - The ID of the target record.
* @param {string} navigationProperty - The navigation property that defines the relationship.
* @param {string} relatedSetName - The name of the related entity set.
* @param {string|number} relatedId - The ID of the record to associate with the target.
* @returns {Promise<object>} The response from the server after creating the association.
* @throws {Error} Throws an error if the association fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/associate-disassociate-entities-using-web-api#add-a-record-to-a-collection
*/
async Associate(
targetSetName,
targetId,
navigationProperty,
relatedSetName,
relatedId
) {
//#region Parameter Validation
if (typeof targetSetName !== "string") {
throw new Error("targetSetName parameter value is not a string.");
}
if (!this.#isGUID(targetId)) {
throw new Error(`The targetId ${targetId} is not a valid GUID.`);
}
if (typeof navigationProperty !== "string") {
throw new Error("navigationProperty parameter value is not a string.");
}
if (typeof relatedSetName !== "string") {
throw new Error("relatedSetName parameter value is not a string.");
}
if (!this.#isGUID(relatedId)) {
throw new Error(`The relatedId ${relatedId} is not a valid GUID.`);
}
//#endregion Parameter Validation
const request = new Request(
new URL(
`${this.apiEndpoint}${targetSetName}(${targetId})/${navigationProperty}/$ref`,
this.apiEndpoint
),
{
method: "POST",
body: JSON.stringify({
"@odata.id": `${this.apiEndpoint}/${relatedSetName}(${relatedId})`,
}),
}
);
request.headers.set("Content-Type", "application/json");
try {
const response = await this.Send(request);
return response;
} catch (error) {
throw error;
}
}
/**
* Disassociates an record from another record by deleting data in the relationship to link them.
*
* @async
* @function Disassociate
* @param {string} targetSetName - The name of the target entity set.
* @param {string|guid} targetId - The ID of the target record.
* @param {string} navigationProperty - The navigation property that defines the relationship.
* @param {string|guid} relatedId - The ID of the related record.
* @returns {Promise<object>} The response from the server after deleting the association.
* @throws {Error} Throws an error if the disassociation fails.
* https://learn.microsoft.com/power-apps/developer/data-platform/webapi/associate-disassociate-entities-using-web-api#remove-a-record-from-a-collection
*/
async Disassociate(targetSetName, targetId, navigationProperty, relatedId) {
//#region Parameter Validation
if (typeof targetSetName !== "string") {
throw new Error("targetSetName parameter value is not a string.");
}
if (!this.#isGUID(targetId)) {
throw new Error(`The targetId ${targetId} is not a valid GUID.`);
}
if (typeof navigationProperty !== "string") {
throw new Error("navigationProperty parameter value is not a string.");
}
if (typeof relatedId !== "string") {
throw new Error("relatedId parameter value is not a string.");
}
if (!this.#isGUID(relatedId)) {
throw new Error(`The relatedId ${relatedId} is not a valid GUID.`);
}
//#endregion Parameter Validation
const request = new Request(
new URL(
`${this.apiEndpoint}${targetSetName}(${targetId})/${navigationProperty}(${relatedId})/$ref`,
this.apiEndpoint
),
{
method: "DELETE",
}
);
try {
const response = await this.Send(request);
return response;
} catch (error) {
throw error;
}
}
//#region Private Methods to support Batch
// Parses batch responses for the Batch method
#parseBatchText(batchResponse) {
const batchBoundary = batchResponse.match(/--batchresponse_[\w-]+/)[0];
const batchParts = batchResponse
.split(batchBoundary)
.filter((part) => part.trim() !== "--" && part.trim() !== "");
const parts = [];
for (const part of batchParts) {
if (
part.startsWith(
"\r\nContent-Type: multipart/mixed; boundary=changesetresponse_"
)
) {
const changeSetBoundary = part.match(/--changesetresponse_[\w-]+/)[0];
const changeSetParts = part
.split(changeSetBoundary)
.filter((part) => part.trim() !== "--" && part.trim() !== "");
for (const changeSetPart of changeSetParts) {
if (
changeSetPart.trim() !== "" &&
!changeSetPart.startsWith(
"\r\nContent-Type: multipart/mixed; boundary=changesetresponse_"
)
) {
parts.push(changeSetPart);
}
}
} else {
parts.push(part);
}
}
function parseHttpStatus(httpStatusString) {
const regex = /^HTTP\/1\.1 (\d{3}) (.+)$/;
const match = httpStatusString.match(regex);
if (match) {
const status = match[1];
const statusText = match[2];
return { status: parseInt(status, 10), statusText: statusText };
} else {
throw new Error("Invalid HTTP status string format");
}
}
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
let responses = [];
for (const part of parts) {
const subParts = part.split("\r\n");
let body = null;
let status = null;
let statusText = null;
let headers = new Headers();
for (let i = 0; i < subParts.length; i++) {
if (subParts[i] === "") {
continue;
}
if (isValidJSON(subParts[i])) {
body = subParts[i];
continue;
}
if (subParts[i].startsWith("HTTP/1.1")) {
const parsedStatus = parseHttpStatus(subParts[i]);
status = parsedStatus.status;
statusText = parsedStatus.statusText;
}
if (subParts[i].includes(":")) {
const [key, value] = subParts[i].split(": ");
headers.set(key.trim(), value.trim());
}
}
const response = new Response(body, {
status: status,
statusText: statusText,
headers: headers,
});
responses.push(response);
}
return responses;
}
//#endregion Private Methods to support Batch
//#region Public Methods to support Batch
// getBatchBody method is publicly accessible because it is used by the ChangeSet class.
/**
* Generates the batch request body for the OData service.
*
* @param {Request} request - The Request object to be used for generating the batch request body.
* @param {string} id - The unique identifier for the batch request or change set
* @param {bool} inChangeSet - Whether the request is in a change set.
* @returns {string} The formatted batch request body.
* @throws {Error} Throws an error if the request parameter is not a valid Request object.
*/
async getBatchBody(request, id, inChangeSet = false) {
if (!(request instanceof Request)) {
throw new Error(
`response parameter value '${request}' is not a Request.`
);
}
const batchBody = [
inChangeSet ? `--changeset_${id}` : `--batch_${id}`,
`Content-Type: application/http`,
`Content-Transfer-Encoding: binary`,
];
batchBody.push("");
batchBody.push(
`${request.method} ${new URL(request.url).pathname} HTTP/1.1`
);
for (const [key, value] of request.headers.entries()) {
if (key === "content-type") {
batchBody.push("content-type: application/json;type=entry");
continue;
}
batchBody.push(`${key}: ${value}`);
}
if (request.body) {
const bodyJson = await request.text();
batchBody.push("", bodyJson);
} else {
batchBody.push("");
}
return batchBody.join("\r\n");
}
//#endregion Public Methods to support Batch
/**
* Sends a batch request containing multiple Request or ChangeSet items.
*
* @async
* @function Batch
* @param {Array<Request|ChangeSet>} items - An array of Request or ChangeSet items to be included in the batch request.
* @param {boolean} [continueOnError=false] - A flag indicating whether to continue processing subsequent requests if an error occurs.
* @throws {Error} Throws an error if the requests parameter is not a valid array or contains invalid items.
* @returns {Promise<Array<Response>>} The parsed response from the batch request.
*/
async Batch(items, continueOnError = false) {
//#region Parameter Validation
if (!Array.isArray(items)) {
throw new Error("requests parameter value is not a valid array.");
}
for (const request of items) {
if (!(request instanceof Request) && !(request instanceof ChangeSet)) {
throw new Error(
"requests parameter value contains items that are not Request or ChangeSet."
);
}
}
//#endregion Parameter Validation
const batchId = Math.random().toString(16).slice(2);
const batchItems = items.map((item, index) => {
if (item instanceof ChangeSet) {
return item.getChangeSetText(batchId);
}
if (item instanceof Request) {
return this.getBatchBody(item, batchId, false);
}
});
const resolvedItems = await Promise.all(batchItems);
const batchRequestBody = [
...resolvedItems,
`--batch_${batchId}--\r\n`,
].join("\r\n");
try {
const batchRequestHeaders = {
"Content-Type": `multipart/mixed; boundary=batch_${batchId}`,
"If-None-Match": null,
};
if (continueOnError) {
batchRequestHeaders["Prefer"] = "odata.continue-on-error";
}
const batchHeaders = new Headers(batchRequestHeaders);
const request = new Request(new URL("$batch", this.apiEndpoint), {
method: "POST",
headers: batchHeaders,
body: batchRequestBody,
});
const response = await this.Send(request);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
return this.#parseBatchText(text);
} catch (error) {
console.error(error.message);
throw error;
}
}
}
/**
* Represents a set of changes used with batch processing.
*/
class ChangeSet {
#client; // Reference to the Client instance
// Validates the requests in the change set.
#validateRequests(requests) {
if (!Array.isArray(requests)) {
throw new Error("requests value is not a valid array.");
}
if (!requests.every((request) => request instanceof Request)) {
throw new Error("requests value is not an array of Request.");
}
if (requests.some((request) => request.method === "GET")) {
throw new Error("requests within change set cannot use GET method.");
}
}
// Sets default Content-ID headers for requests in the change set.
#setDefaultContentIds(requests) {
for (let i = 0; i < requests.length; i++) {
const request = requests[i];
if (request.headers.get("Content-ID") === null) {
request.headers.set("Content-ID", i.toString());
}
}
}
/**
* Gets the ID of the ChangeSet.
*
* @returns {string} The ID of the ChangeSet.
*/
get id() {
return this._id;
}
/**
* Sets the ID of the ChangeSet.
*
* @param {string} value - The new ID for the ChangeSet.
* @throws {Error} Throws an error if the value is not a string.
*/
set id(value) {
if (typeof value !== "string") {
throw new Error("id parameter value is not a string.");
}
this._id = value;
}
/**
* Creates an instance of ChangeSet.
*
* @constructor
* @param {Client} client - The Client instance to be used for the ChangeSet.
* @param {Array<Request>} requests - An array of Request objects to be included in the ChangeSet.
* @param {string} [id=null] - An optional ID for the ChangeSet. If not provided, a random ID will be generated.
* @throws {Error} Throws an error if the client parameter is not a valid Client instance.
*/
constructor(client, requests, id = null) {
if (!(client instanceof Client)) {
throw new Error("client parameter value is not a valid Client instance.");
}
this.#client = client;
this.requests = requests;
if (!id) {
this._id = Math.random().toString(16).slice(2);
}
}
/**
* Gets the array of Request objects in the change set.
*
* @returns {Array<Request>} The array of Request objects in the change set.
*/
get requests() {
return this._requests;
}
/**
* Sets the array of Request objects in the change set.
*
* @param {Array<Request>} value - The new array of Request objects to be included in the change set.
* @throws {Error} Throws an error if the value is not a valid array of Request objects.
*/
set requests(value) {
this.#validateRequests(value);
this.#setDefaultContentIds(value);
this._requests = value;
}
/**
* Gets the text for the changeset in the $batch operation.
*
* @async
* @function getChangeSetText
* @param {string} batchId - The unique identifier for the batch request.
* @returns {Promise<string>} The formatted changeset text for the batch operation.
*/
async getChangeSetText(batchId) {
const batchBody = [
`--batch_${batchId}\r\n`,
`Content-Type: multipart/mixed; boundary=changeset_${this.id}\r\n`,
];
let count = 1;
for (const request of this._requests) {
request.contentID = count.toString();
// Use the getBatchBody method setting inChangeSet parameter to true
const requestBody = await this.#client.getBatchBody(
request,
this.id,
true
);
batchBody.push(`\r\n${requestBody}`);
count++;
}
batchBody.push(`\r\n--changeset_${this.id}--\r\n`);
return batchBody.join("");
}
}
// Group public classes in a namespace object for export
const DataverseWebAPI = {
Client,
ChangeSet,
};
export { DataverseWebAPI };
Voir aussi
Utiliser l’API web Dataverse
Exemples d’API web
Exemples d’API web (C#)