From a9fd34831ce4c3b243ba892a557574d6b4fb794e Mon Sep 17 00:00:00 2001 From: Dogukan Karatas <61163577+dogukankaratas@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:09:12 +0200 Subject: [PATCH] feat: move from embed token api to share token api (#230) * new mutation for token exchange * variable based --- .../speckle/GetByUrl.pqm | 3 +- .../speckle/api/ExchangeToken.pqm | 203 +++++++++--------- .../speckle/api/Parser.pqm | 3 +- .../speckle/api/SendToServer.pqm | 3 +- 4 files changed, 102 insertions(+), 110 deletions(-) diff --git a/src/powerbi-data-connector/speckle/GetByUrl.pqm b/src/powerbi-data-connector/speckle/GetByUrl.pqm index 1676ea9..c64308f 100644 --- a/src/powerbi-data-connector/speckle/GetByUrl.pqm +++ b/src/powerbi-data-connector/speckle/GetByUrl.pqm @@ -66,7 +66,8 @@ powerfulToken, {"profile:read", "streams:read", "users:read"}, parsedUrl[projectId], - parsedUrl[baseUrl] + parsedUrl[baseUrl], + parsedUrl[resourceIdString] ), // throw error if token exchange failed - do NOT use powerful token as fallback diff --git a/src/powerbi-data-connector/speckle/api/ExchangeToken.pqm b/src/powerbi-data-connector/speckle/api/ExchangeToken.pqm index 19fd2ec..88004ed 100644 --- a/src/powerbi-data-connector/speckle/api/ExchangeToken.pqm +++ b/src/powerbi-data-connector/speckle/api/ExchangeToken.pqm @@ -1,9 +1,6 @@ // Function to exchange powerful token for weak limited token -(powerfulToken as text, scopes as list, projectId as text, serverUrl as text) as record => +(powerfulToken as text, scopes as list, projectId as text, serverUrl as text, optional resourceIdString as text) as record => let - // Import the parser function for URL handling - Parser = Extension.LoadFunction("Parser.pqm"), - // Helper function to load .pqm modules dynamically Extension.LoadFunction = (fileName as text) => let @@ -33,114 +30,106 @@ else null, - // Token lifetime: 10 years (315,360,000,000 milliseconds) - TokenLifespanMs = 10 * 365 * 24 * 3600 * 1000, - - // Generate token name with timestamp - TokenName = "Limited Power BI Visual Token - " & DateTime.ToText(DateTime.LocalNow(), "yyyy-MM-dd HH:mm"), - - // Build scopes array string for GraphQL (e.g., ["profile:read", "streams:read"]) - ScopesArray = Text.Combine( - List.Transform(scopes, each """" & _ & """"), - ", " - ), - - // Build GraphQL mutation - GraphQLMutation = " - mutation { - apiTokenCreate(token: { - name: """ & TokenName & """, - scopes: [" & ScopesArray & "], - lifespan: " & Number.ToText(TokenLifespanMs) & ", - limitResources: [{ - type: project, - id: """ & projectId & """ - }] - }) - }", - - // Execute token exchange if validation passes - Result = if ValidationError <> null then - [ - Success = false, - Token = null, - ErrorMessage = ValidationError - ] + // Ensure serverUrl ends with / + NormalizedServerUrl = if Text.End(serverUrl, 1) = "/" then + serverUrl else - try - let - // Ensure serverUrl ends with / - NormalizedServerUrl = if Text.End(serverUrl, 1) = "/" then - serverUrl - else - serverUrl & "/", + serverUrl & "/", - // Make GraphQL request - Response = Web.Contents( - NormalizedServerUrl & "graphql", - [ - Headers = [ - #"Method" = "POST", - #"Content-Type" = "application/json", - #"Authorization" = "Bearer " & powerfulToken - ], - Content = Json.FromValue([ - query = GraphQLMutation - ]), - ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}, - Timeout = #duration(0, 0, 0, 10) - ] - ), + // New Share Token API mutation with variables + NewGraphQLQuery = "mutation CreateEmbedShareToken($input: CreateEmbedShareTokenInput!) { + sharingMutations { + createEmbedShareToken(input: $input) { + token + } + } + }", + NewGraphQLVariables = [ + input = [ + projectId = projectId, + resourceIdString = resourceIdString + ] + ], - StatusCode = Value.Metadata(Response)[Response.Status], + // Legacy apiTokenCreate mutation with variables + TokenLifespanMs = 10 * 365 * 24 * 3600 * 1000, + TokenName = "Limited Power BI Visual Token - " & DateTime.ToText(DateTime.LocalNow(), "yyyy-MM-dd HH:mm"), + LegacyGraphQLQuery = "mutation CreateApiToken($token: ApiTokenCreateInput!) { + apiTokenCreate(token: $token) + }", + LegacyGraphQLVariables = [ + token = [ + name = TokenName, + scopes = scopes, + lifespan = TokenLifespanMs, + limitResources = {[ + type = "project", + id = projectId + ]} + ] + ], - // Parse response if successful - ParsedResult = if StatusCode >= 200 and StatusCode < 300 then - let - JsonResponse = Json.Document(Response), + // Helper: execute a GraphQL query with variables and extract token + ExecuteGraphQL = (query as text, variables as record, extractToken as function) => + let + Response = Web.Contents( + NormalizedServerUrl & "graphql", + [ + Headers = [ + #"Method" = "POST", + #"Content-Type" = "application/json", + #"Authorization" = "Bearer " & powerfulToken + ], + Content = Json.FromValue([ + query = query, + variables = variables + ]), + ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}, + Timeout = #duration(0, 0, 0, 10) + ] + ), + StatusCode = Value.Metadata(Response)[Response.Status], + JsonResponse = if StatusCode >= 200 and StatusCode < 300 then + Json.Document(Response) + else + null, + HasErrors = JsonResponse <> null and Record.HasFields(JsonResponse, {"errors"}), + Token = if JsonResponse <> null and not HasErrors then + try extractToken(JsonResponse) otherwise null + else + null, + ErrorMsg = if HasErrors then + try JsonResponse[errors]{0}[message] otherwise "GraphQL mutation failed" + else if JsonResponse = null then + "Request failed with status " & Number.ToText(StatusCode) + else + null + in + [Success = Token <> null, Token = Token, ErrorMessage = ErrorMsg], - // Check for GraphQL errors - HasErrors = Record.HasFields(JsonResponse, {"errors"}), + // Try new API first, fall back to legacy + Result = if ValidationError <> null then + [Success = false, Token = null, ErrorMessage = ValidationError] + else + let + newResult = if resourceIdString <> null then + try ExecuteGraphQL( + NewGraphQLQuery, + NewGraphQLVariables, + each [data][sharingMutations][createEmbedShareToken][token] + ) otherwise [Success = false, Token = null, ErrorMessage = "New API request failed"] + else + [Success = false, Token = null, ErrorMessage = null], - // Extract token from response - WeakToken = if not HasErrors then - JsonResponse[data][apiTokenCreate] - else - null, - - ErrorMsg = if HasErrors then - try - JsonResponse[errors]{0}[message] - otherwise - "GraphQL mutation failed with unknown error" - else - null - in - if WeakToken <> null then - [ - Success = true, - Token = WeakToken, - ErrorMessage = null - ] - else - [ - Success = false, - Token = null, - ErrorMessage = ErrorMsg - ] - else - [ - Success = false, - Token = null, - ErrorMessage = "GraphQL request failed with status " & Number.ToText(StatusCode) - ] - in - ParsedResult - otherwise - [ - Success = false, - Token = null, - ErrorMessage = "Token exchange request failed with exception" - ] + finalResult = if newResult[Success] then + newResult + else + try ExecuteGraphQL( + LegacyGraphQLQuery, + LegacyGraphQLVariables, + each [data][apiTokenCreate] + ) otherwise [Success = false, Token = null, ErrorMessage = "Token exchange request failed"] + in + finalResult in Result diff --git a/src/powerbi-data-connector/speckle/api/Parser.pqm b/src/powerbi-data-connector/speckle/api/Parser.pqm index fe640fd..950bf89 100644 --- a/src/powerbi-data-connector/speckle/api/Parser.pqm +++ b/src/powerbi-data-connector/speckle/api/Parser.pqm @@ -54,5 +54,6 @@ modelId = if isFederated then null else processedModels{0}[modelId], versionId = if isFederated then null else processedModels{0}[versionId], isFederated = isFederated, - federatedModels = if isFederated then processedModels else null + federatedModels = if isFederated then processedModels else null, + resourceIdString = rawModelSegment ] diff --git a/src/powerbi-data-connector/speckle/api/SendToServer.pqm b/src/powerbi-data-connector/speckle/api/SendToServer.pqm index 9e8923a..1033dac 100644 --- a/src/powerbi-data-connector/speckle/api/SendToServer.pqm +++ b/src/powerbi-data-connector/speckle/api/SendToServer.pqm @@ -33,7 +33,8 @@ powerfulToken, {"profile:read", "streams:read", "users:read"}, parsedUrl[projectId], - parsedUrl[baseUrl] + parsedUrl[baseUrl], + parsedUrl[resourceIdString] ), // throw error if token exchange failed - do NOT use powerful token as fallback