Saltar al contenido principal
Adaptive Planning
Última actualización: 2025-04-04
Pasos de ejemplo: exportación a Microsoft Fabric mediante el origen de datos de Planning y el cargador de la nube personalizada

Pasos de ejemplo: exportación a Microsoft Fabric mediante el origen de datos de Planning y el cargador de la nube personalizada

Este ejemplo ilustra cómo hacer lo siguiente:
  • Cree un origen de datos de Planning para una hoja de Adaptive Planning. Este ejemplo utiliza una hoja modelada, pero puede utilizar cualquier tipo de hoja que admita el origen de datos de Planning.
  • Registre una aplicación en Microsoft Entra.
  • Cree un cargador de la nube personalizada.
  • Ejecute o programe el cargador de la nube personalizada.
Quiere cargar datos de una hoja modelada de Adaptive Planning en Microsoft Fabric mediante integraciones de diseño de Adaptive Planning. Para cargar esos datos, debe hacer lo siguiente:
  • Defina un origen de datos de Planning (PDS).
  • Configure un Custom Cloud Loader (CCL) que exporte desde el PDS a Microsoft Fabric.
Quiere utilizar el JavaScript proporcionado para la CCL para:
  • Leer datos de una hoja modelada de Adaptive Planning.
  • Cree un token de acceso de OAuth con el tipo de concesión client_credentials con el ID de cliente y el secreto proporcionados.
  • Pruebe la conexión entre Adaptive Planning y Microsoft Fabric.
  • Cargue los datos mediante un archivo CSV en Microsoft Fabric.
  • Aproveche la API de OneLake.
  • Acceda para registrar una aplicación en el centro de administración de Microsoft Entra.
  • Credenciales para Microsoft Fabric
  • Seguridad: permiso
    Desarrollador de integración
    .
  1. Cree un origen de datos de Planning para su hoja modelada.
    Cuando seleccione fuentes de Adaptive Planning para importar, seleccione su hoja.
  2. Registre una aplicación en el centro de administración de Microsoft Entra para utilizarla en la CCL de Adaptive Planning para OAuth.
    Asigne un nombre a la aplicación CustomCloudLoaderMSFarbric.
  3. Asigne los siguientes permisos de Microsoft Graph a la aplicación registrada en Microsoft Entra:
    Nombre de API o permiso
    Tipo
    Descripción
    Files.Read.All
    Solicitud
    Leer archivos en todas las colecciones de sitios.
    Files.ReadWrite.All
    Solicitud
    Leer y escribir archivos en todas las colecciones de sitios.
    Files.ReadWrite.AppFolder
    Solicitud
    Tener acceso completo a la carpeta de la aplicación sin que un usuario haya iniciado sesión.
    Files.SelectedOperations.Selected
    Solicitud
    Acceder a los archivos seleccionados sin un usuario conectado.
    User.Read
    Delegado
    Conéctese y lea el perfil de usuario.
  4. Proporcione a la aplicación Microsoft Entra acceso al espacio de trabajo con un nivel de acceso mínimo de
    Colaborador
    .
  5. Añada la siguiente configuración en la ficha
    Configuración de diseñador
    :
    Nombre
    Componente de configuración
    Espacio de trabajo de destino
    Texto estático
    Lago central de destino
    Texto estático
    ID de entorno de cliente
    Texto estático
    Secreto de cliente
    Contraseña
    ID de cliente
    Texto estático
    Cree las siguientes secuencias de comandos en la ficha
    Secuencias de comandos
    :
    • AzureCloudUtil
    • EnvSettings
    • MsFabricCCL1
    AzureCloudUtil Script
    Esta secuencia de comandos contiene los siguientes métodos de utilidad:
    • requestNewToken: genera un token de acceso de OAuth mediante el tipo de concesión client_credentials con el ID de cliente y el secreto proporcionados.
    • getWorkspace: recupera información sobre un espacio de trabajo especificado mediante el workspaceIdentifier de ese espacio de trabajo.
    • getWorkspaces: recupera todos los espacios de trabajo accesibles para las credenciales proporcionadas.
    • getWorkspaceItems: enumera todos los elementos de un espacio de trabajo mediante el workspace_id de ese espacio de trabajo.
    • getLakehouse: recupera información sobre un elemento de Lakehouse especificado dentro de un espacio de trabajo mediante el identificador de lago (lakehouse_identifier) de ese elemento.
    • uploadRowsIntoFabricTable: carga filas de un archivo CSV en la tabla de Lakehouse.
    • createFileResource: realiza una llamada al punto final de OneLake Files/<file resource> con action=file para crear un recurso de archivo.
    • uploadFileContent: realiza una llamada al punto final de OneLake Files/<file resource> con action=append para cargar filas CSV.
    • flushAndFinalizedFileContent: llama al punto final de OneLake Files/<file resource> con action=flush para finalizar y confirmar todas las filas CSV.
    • trackJobProgress: supervisa el progreso de la carga de filas del archivo CSV en la tabla de Lakehouse.
    • makeHttpsReq: envía solicitudes HTTP.
    Introduzca este texto como
    secuencia de comandos
    :
    const azureCloudUtil = { requestNewToken: function (scope = EnvSettings.default_scope) { var tenant_id = EnvSettings.tenant_id; var client_id = EnvSettings.client_id; var client_secret = EnvSettings.client_secret; var method = 'POST'; var body = 'grant_type=client_credentials&client_id='+ client_id +'&scope=' + scope; var headers = { "Content-Type": 'application/x-www-form-urlencoded', "Authorization": 'Basic ' + ai.util.encodeBase64(client_id + ':' + client_secret, "") }; var response = null; try { var url = EnvSettings.token_url.replace('{tenant_id}', tenant_id); response = makeHttpsReq(method, url, body, headers, null, false); // ai.log.logInfo('requestNewToken HTTP response ', JSON.stringify(response.getBody())); } catch (exception) { ai.log.logError('requestNewToken: HTTPS Request failed', '' + exception); return null; } if (response.getHttpCode() == '200') { var responseBody = response.getBody(); return JSON.parse(responseBody); } else { ai.log.logError('requestNewToken: responseBody', responseBody); ai.log.logError('requestNewToken: HttpCode: ' , response.getHttpCode()); return null; } }, getWorkspace: function (workspaceIdentifier) { var response = azureCloudUtil.getWorkspaces(); if (!isSuccessStatusCode(response)) { throw new Error("Unable to fetch workspace information!"); } var responseBody = response.getBody(); var workspaceList = JSON.parse(responseBody); if (!workspaceList || !workspaceList.value || !Array.isArray(workspaceList.value)) { throw new Error("Invalid input JSON. Expected a structure with a 'value' array."); } const lowerWorkspaceIdentifier = workspaceIdentifier.toLowerCase(); const foundWorkspace = ( workspaceList.value.find(entry => entry.id.toLowerCase() === lowerWorkspaceIdentifier || entry.displayName.toLowerCase() === lowerWorkspaceIdentifier ) || null ); if (foundWorkspace === null) { throw new Error(`The resource '${workspaceIdentifier}' could not be found or you may not have the required permissions to access it.`); } return foundWorkspace; }, getWorkspaces: function () { var targetUrl = EnvSettings.ms_fabric_base_url + '/workspaces' var method = 'GET'; return makeHttpsReq(method, targetUrl, '', null); }, getWorkspaceItems: function (workspace_id) { var targetUrl = EnvSettings.ms_fabric_base_url + '/workspaces/' + workspace_id + '/items' var method = 'GET'; return makeHttpsReq(method, targetUrl, '', null); }, getLakehouse: function (workspace, lakehouse_identifier) { var response = azureCloudUtil.getWorkspaceItems(workspace.id); if (!isSuccessStatusCode(response)) { throw new Error("Unable to fetch workspace items!"); } var responseBody = response.getBody(); var workspaceItems = JSON.parse(responseBody); if (!workspaceItems || !workspaceItems.value || !Array.isArray(workspaceItems.value)) { throw new Error("Invalid input JSON. Expected a structure with a 'value' array."); } const lower_lakehouse_identifier = lakehouse_identifier.toLowerCase(); var foundItem = ( workspaceItems.value.find(entry => entry.type === "Lakehouse" && ( entry.id.toLowerCase() === lower_lakehouse_identifier || entry.displayName.toLowerCase() === lower_lakehouse_identifier ) ) || null); if (foundItem === null) { foundItem = ( workspaceItems.value.find(entry => entry.id.toLowerCase() === lower_lakehouse_identifier || entry.displayName.toLowerCase() === lower_lakehouse_identifier ) || null); } if (foundItem === null) { throw new Error(`The resource '${lower_lakehouse_identifier}' could not be found or you may not have the required permissions to access it.`); } if (foundItem.type !== "Lakehouse") { throw new Error(lakehouse_identifier + ' is of type ' + foundItem.type + ', not Lakehouse.'); } return foundItem; }, uploadRowsIntoFabricTable: function(workspaceId, lakehouseId, tableName, inputCsvFile) { const targetUrl = `${EnvSettings.ms_fabric_base_url}/workspaces/${workspaceId}/lakehouses/${lakehouseId}/tables/${tableName}/load`; const headers = { "Content-Type": "application/json", }; const body = { relativePath: `Files/${inputCsvFile}`, pathType: "File", mode: "overwrite", // Mode can be "overwrite", "append", "update", or "upsert" formatOptions:{ header: true, delimiter: ",", format: "CSV" } }; const triggerResponse = makeHttpsReq("POST", targetUrl, JSON.stringify(body), headers); if (isSuccessStatusCode(triggerResponse)) { ai.log.logInfo('uploadRowsIntoFabricTable: ' + tableName, `Copy from the CSV file 'Files/${inputCsvFile}' initiated.`); } else { ai.log.logError('uploadRowsIntoFabricTable: ' + tableName, 'An error occurred while starting the copy process.' + triggerResponse.getBody()); return false; } const terminalStates = ["Succeeded", "Failed"]; const recentJobStatusResponse = trackJobProgress(triggerResponse, terminalStates, 'Copy CSV file'); if (isSuccessStatusCode(triggerResponse)) { ai.log.logInfo('uploadRowsIntoFabricTable: ' + tableName, `Successfully copied all rows from the CSV file 'Files/${inputCsvFile}'.`); } else { ai.log.logError('uploadRowsIntoFabricTable: ' + tableName, 'Failed to copy rows from the CSV file ' + inputCsvFile + '. ' + triggerResponse.getBody()); return false; } return true; }, createFileResource: function(workspace, lakehouse, inputCsvFile) { const encodedWorkspaceName = workspace.displayName.replace(/ /g, "%20"); const targetUrl = `${EnvSettings.onelake_dfs_base_url}/${encodedWorkspaceName}/${lakehouse.displayName}.lakehouse/Files/${inputCsvFile}?resource=file`; const headers = { "Content-Type": "application/json", }; //ai.log.logInfo('createFileResource: targetUrl' , targetUrl); const response = makeHttpsReq("PUT", targetUrl, '', headers, EnvSettings.azure_storage_scope); if (!isSuccessStatusCode(response)) { ai.log.logError('createFileResource: ' + inputCsvFile, 'Failed to create the file resource.' + response.getBody()); return false; } ai.log.logInfo('createFileResource: ' + inputCsvFile, `Created a file resource.`); return true; }, uploadFileContent: function (workspace, lakehouse, inputCsvFile, inputCsvFileBody, position) { const encodedWorkspaceName = workspace.displayName.replace(/ /g, "%20"); const targetUrl = `${EnvSettings.onelake_dfs_base_url}/${encodedWorkspaceName}/${lakehouse.displayName}.lakehouse/Files/${inputCsvFile}?action=append&position=${position}`; const contentLength = getByteLength(inputCsvFileBody); const headers = { "x-ms-version": "2021-06-08", "Content-Type": "text/csv", "Content-Length": contentLength }; const response = makeHttpsReq("PATCH", targetUrl, inputCsvFileBody, headers, EnvSettings.azure_storage_scope); if (!isSuccessStatusCode(response)) { ai.log.logError('uploadFileContent: ' + inputCsvFile, 'Failed to upload the file content.'+ response.getBody()); return -1; } ai.log.logInfo('uploadFileContent: ' + inputCsvFile, `Uploaded file content.`); return contentLength; }, flushAndFinalizedFileContent: function(workspace, lakehouse, inputCsvFile, fileContentLength) { const encodedWorkspaceName = workspace.displayName.replace(/ /g, "%20"); const targetUrl = `${EnvSettings.onelake_dfs_base_url}/${encodedWorkspaceName}/${lakehouse.displayName}.lakehouse/Files/${inputCsvFile}?action=flush&position=${fileContentLength}`; const headers = { "Content-Type": "application/json", "x-ms-version": "2021-06-08" }; const response = makeHttpsReq("PATCH", targetUrl, '', headers, EnvSettings.azure_storage_scope); if (isSuccessStatusCode(response)) { ai.log.logInfo('flushAndFinalizedFileContent: ' + inputCsvFile, `The file content flushed and finalized.`); return true; } else { ai.log.logError('flushAndFinalizedFileContent: ' + inputCsvFile, 'Failed to flush and finalize the file content.'); return false; } } } function makeHttpsReq(httpMethodType, targetUrl, httpBody, httpHeaders, scope = EnvSettings.default_scope, addAuthHeader = true) { var method = httpMethodType; var headers = httpHeaders || { 'Content-Type': 'application/json' }; if (EnvSettings.use_attached_oauth === false && addAuthHeader === true) { var tokenResponse = azureCloudUtil.requestNewToken(scope); if (tokenResponse === null || tokenResponse === undefined || tokenResponse['access_token'] === null || tokenResponse['access_token'] === undefined ) { throw new Error('Unable to generate access token. Check for invalid credentials, incorrect scopes, or misconfigured endpoints.'); } EnvSettings.accessTokenResponse = tokenResponse; headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer '+ EnvSettings.accessTokenResponse['access_token'], }; } var response = null; try { // ai.log.logInfo('targetUrl with method', targetUrl + ' ' + method); // ai.log.logInfo('httpBody', JSON.stringify(httpBody)); // ai.log.logInfo('headers', JSON.stringify(headers)); if (EnvSettings.use_attached_oauth === true) { response = ai.https.authorizedRequest(targetUrl, method, '' + httpBody, headers); } else { response = ai.https.request(targetUrl, method, '' + httpBody, headers); } // ai.log.logInfo('Response Body : ', response.getBody()); if (!isSuccessStatusCode(response)) { ai.log.logError('makeHttpsReq: HTTP response code', response.getHttpCode()); } } catch (exception) { ai.log.logError('makeHttpsReq: HTTPS Request failed', '' + exception); throw new Error('An unexpected communication failure occurred. ' + exception); } return response; } function isSuccessStatusCode(response) { if (response === null || response === undefined) { return false; } if (isNaN(response.getHttpCode())) { return false; } var responseCode = parseInt(response.getHttpCode()); return responseCode >= 200 && responseCode < 300; } function sleepX(milliSeconds) { var startTime = new Date().getTime(); while (new Date().getTime() < startTime + milliSeconds); } function trackJobProgress(triggerResponse, terminalStates, jobName) { var httpUrlLocation = triggerResponse.getHeaders()['Location']; var method = 'GET'; const headers = { "Content-Type": "application/json", }; var maxCheckLimit = EnvSettings.max_check_status_limit; var jobStatusResponse = null; var jsonJobStatus = null; var lcaseLatestJobStatus = null; do { if (lcaseLatestJobStatus !== null) { ai.log.logInfo(jobName, lcaseLatestJobStatus); sleepX(1000 * 5); } jobStatusResponse = makeHttpsReq(method, httpUrlLocation, '', headers); maxCheckLimit--; jsonJobStatus = {}; lcaseLatestJobStatus = 'unknown'; if (jobStatusResponse !== null && isSuccessStatusCode(jobStatusResponse)) { jsonJobStatus = JSON.parse(jobStatusResponse.getBody()); lcaseLatestJobStatus = jsonJobStatus.status.toLowerCase(); } } while(maxCheckLimit > 0 && jsonJobStatus !== null && !(terminalStates.some(word => word.toLowerCase() === lcaseLatestJobStatus))); if (maxCheckLimit > 0){ ai.log.logInfo(jobName, 'The job has been completed.'); } else { ai.log.logInfo(jobName, `Terminated status check as max limit ${maxCheckLimit} reached.`); } ai.log.logInfo(jobName + ' - final status', jsonJobStatus.status); if (jsonJobStatus.Error !== null && jsonJobStatus.Error !== undefined && jsonJobStatus.Error.trim().length() > 0) { ai.log.logError(jobName + ' - error', JSON.stringify(jsonJobStatus.Error)); } return jsonJobStatus; } function getByteLength(str) { let byteLength = 0; if (str !== null && str !== undefined) { for (let i = 0; i < str.length; i++) { const charCode = str.charCodeAt(i); if (charCode <= 0x7f) { byteLength += 1; } else if (charCode <= 0x7ff) { byteLength += 2; } else if (charCode <= 0xffff) { byteLength += 3; } else { byteLength += 4; } } } return byteLength; }
    Secuencia de comandos EnvSettings
    Esta secuencia de comandos incluye un objeto JSON que contiene la configuración de CCL para:
    • batchSize: determina el número máximo de filas permitidas en cada lote de carga.
    • max_check_status_limit: establece el número máximo de intentos para comprobar el progreso del trabajo antes de finalizar el proceso.
    Introduzca este texto como
    secuencia de comandos
    :
    var EnvSettings = { ms_fabric_base_url:'https://api.fabric.microsoft.com/v1', onelake_dfs_base_url:'https://onelake.dfs.fabric.microsoft.com', ms_fabric_workspace_url:'https://api.fabric.microsoft.com/v1/workspaces', default_scope: 'https://api.fabric.microsoft.com/.default', items_scope: 'https://api.fabric.microsoft.com/Workspace.ReadWrite.All https://api.fabric.microsoft.com/Item.ReadWrite.All', onelake_scope: 'https://onelake.dfs.fabric.microsoft.com/.default', azure_storage_scope: 'https://storage.azure.com/.default', batchSize: 1000, target_workspace_identifier:null, target_lakehouse_identifier: null, max_check_status_limit:25, use_attached_oauth: false, token_url: 'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token', tenant_id:null, client_id:null, client_secret:null, accessTokenResponse: null, };
    MsFabricCCL1 Script
    Esta secuencia de comandos prueba la conexión al verificar si puede recuperar la lista del espacio de trabajo de MS Fabric con el ID de cliente y el secreto proporcionados.
    La secuencia de comandos implementa la devolución de llamada exportData(context) dentro de la secuencia de comandos CCL, utilizando el contexto proporcionado para acceder al lector PDS. El método readRow() del lector prepara los datos CSV para cargarlos en MS Fabric Lakehouse.
    La secuencia de comandos aprovecha la API de OneLake para enviar datos siempre que se alcance el límite de tamaño de fila de lote. El tamaño máximo de lote se puede configurar en el archivo EnvSettings.
    Introduzca este texto como
    secuencia de comandos
    :
    'use strict'; /******************************************************************************* * MainFunction *******************************************************************************/ import { ai } from '/Script/Source/Integration2/CustomCloudScripts/CustomCloudScriptApi.js'; // ***************************************************************************** // Test Connection // ***************************************************************************** function testConnection(context) { var dataSource = context.getDataSource(); initSettings(dataSource); var testConnectionResult = false; var workspaceList = azureCloudUtil.getWorkspaces(); if (workspaceList === null || workspaceList === undefined) { ai.log.logInfo('Workspace List', 'Empty or undefined'); } else { var workspaceItems = JSON.parse(workspaceList.getBody()); if (!workspaceItems || !workspaceItems.value || !Array.isArray(workspaceItems.value)) { ai.log.logError('Invalid response', "Invalid response JSON. Expected a structure with a 'value' array."); } else { testConnectionResult = true; } } return testConnectionResult; } function exportData(context) { var reader = context.createTableReader(); var dataSource = context.getDataSource(); initSettings(dataSource); var sourceTableName = 'PersonnelModelSheet'; var csvFileName = `${sourceTableName}.csv`; var workspace = azureCloudUtil.getWorkspace(EnvSettings.target_workspace_identifier); // ai.log.logInfo('workspace', workspace.id); var lakehouse = azureCloudUtil.getLakehouse(workspace, EnvSettings.target_lakehouse_identifier); // ai.log.logInfo('lakehouse', lakehouse); var totalLineCount = generateAndPushCSVData(reader, workspace, lakehouse, sourceTableName, csvFileName); ai.log.logInfo('exportData', 'Pushed ' + totalLineCount + ' lines'); } function generateAndPushCSVData(reader, workspace, lakehouse, sourceTableName, csvFileName) { const tagetBatchSize = EnvSettings.batchSize; var headers = null; var totalColumnCount = null; var csvColumnCount = null; let row; var csvRows = []; var lineCount = 0; var totalByteSize = 0; var recentCallResult = false; var recentUploadByteSize = 0; while ((row = reader.readRow()) !== null && row !== undefined) { if (totalColumnCount === null) { totalColumnCount = row.length; csvColumnCount = row.length - 2; headers = createHeaderRow(csvColumnCount); if (!Array.isArray(headers) || headers.length === 0) { throw new Error("Headers must be a non-empty array."); } // Add headers as the first row of the CSV csvRows = [headers.join(",")]; var result = azureCloudUtil.createFileResource(workspace, lakehouse, csvFileName); if (result === false) { return 0; } } if (!Array.isArray(row) || row.length < headers.length) { throw new Error("Each row must be an array with the same number of columns as the headers."); } // Escape values and join them with commas const escapedRow = row.map(value => typeof value === "string" ? `"${value.replace(/"/g, '""')}"` : value ); csvRows.push(escapedRow.join(",")); lineCount++; // reached batch limit if (lineCount >= tagetBatchSize && (lineCount % tagetBatchSize) === 0){ var csvData = prepareCsvData(csvRows, totalByteSize); recentUploadByteSize = azureCloudUtil.uploadFileContent(workspace, lakehouse, csvFileName, csvData, totalByteSize); if (recentUploadByteSize < 1) { throw new Error("An error occurred while uploading rows."); } totalByteSize += recentUploadByteSize; csvRows = []; } } if (csvRows.length > 0 ){ var csvData = prepareCsvData(csvRows, totalByteSize); recentUploadByteSize = azureCloudUtil.uploadFileContent(workspace, lakehouse, csvFileName, csvData, totalByteSize); if (recentUploadByteSize < 1) { throw new Error("An error occurred while uploading rows."); } totalByteSize += recentUploadByteSize; } if (lineCount > 0) { recentCallResult = azureCloudUtil.flushAndFinalizedFileContent(workspace, lakehouse, csvFileName, totalByteSize); if (recentCallResult === true) { recentCallResult = azureCloudUtil.uploadRowsIntoFabricTable(workspace.id, lakehouse.id, sourceTableName, csvFileName); } else { ai.log.logError('generateAndPushCSVData - table load status ', 'Failed to flush and finalize the uploaded rows. Check the system and try again.'); } if (recentCallResult === false) { return 0; } } return lineCount; } function prepareCsvData(csvRows, totalByteSize){ if (csvRows === null || csvRows === undefined || csvRows.length === 0 ) { return ""; } var csvData = null; if (totalByteSize === 0) { csvData = csvRows.join("\n"); } else { csvData = ("\n" + csvRows.join("\n")); } return csvData; } function createHeaderRow(columnCount) { if (columnCount <= 0 || !Number.isInteger(columnCount)) { throw new Error("Column count must be a positive integer."); } const headerRow = Array.from({ length: columnCount }, (_, index) => `Column${index + 1}`); return headerRow; } function initSettings( dataSource ) { EnvSettings.target_workspace_identifier = getSetting(dataSource, "Target Workspace"); EnvSettings.target_lakehouse_identifier = getSetting(dataSource, "Target Lakehouse"); EnvSettings.tenant_id = getSetting(dataSource, "Tenant Id"); EnvSettings.client_id = getSetting(dataSource, "Client Id"); EnvSettings.client_secret = getSetting(dataSource, "Client Secret"); } function getSetting(dataSource, propKey, defaultValue = null) { var setting = dataSource.getSetting(propKey) if (setting === null || setting === undefined) { return defaultValue; } var value = dataSource.getSetting(propKey).getValue(); if (value === null || value === undefined) { return defaultValue; } return value.trim(); }
  6. Ejecute el cargador de la nube personalizada manualmente o prográmelo como una tarea de integración.