Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f558d5dce3 | |||
| f9093a34e5 | |||
| 23cb40c4ab | |||
| 9bd6140065 | |||
| a14bb06d4e | |||
| 01576e77df | |||
| f1e16e9d55 | |||
| 4f210dae04 | |||
| 8669cf59f8 | |||
| 6e6402cb38 | |||
| 7b62630fd1 | |||
| eee4f0f8c2 | |||
| 23fa79bdd9 | |||
| edc9917a20 | |||
| 99cffeba8c | |||
| 7e92e7a2aa | |||
| 15aa627d7f | |||
| 8844f1837e | |||
| 6d1b85a14f | |||
| b2d6568b66 | |||
| 72f455393b | |||
| bd3fe6a47c | |||
| e7710f0f8e | |||
| e9034027eb | |||
| 685384b147 | |||
| 5973aaea10 | |||
| 639287f69c | |||
| a43decd473 | |||
| a532061879 | |||
| 15ac7c041e | |||
| 770d45f981 | |||
| 50b59a88c3 | |||
| 0bad9914c8 | |||
| aee00f3f21 | |||
| 2abdd4b645 | |||
| 71ef1acd8b | |||
| 23114a2912 | |||
| ee989f927d | |||
| 1ff15c7b44 | |||
| 6e9fb590a0 | |||
| 9700865934 | |||
| cf9e5a70b6 | |||
| 50c360ae79 | |||
| b671945fa7 | |||
| eb4787ddfa | |||
| 3ffd6c63e0 | |||
| a5a8316ee0 | |||
| 17083c9474 | |||
| 3e6f9988d6 |
@@ -0,0 +1,58 @@
|
||||
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
||||
# See: https://circleci.com/docs/2.0/configuration-reference
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
win: circleci/windows@5.0
|
||||
|
||||
jobs:
|
||||
build-connector:
|
||||
executor:
|
||||
name: win/default
|
||||
shell: powershell.exe
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Set connector internal version"
|
||||
command: |
|
||||
$VERSION = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "2.0.0.$($env:WORKFLOW_NUM)" } else { $env:CIRCLE_TAG }
|
||||
(Get-Content ./Speckle.pq).replace('[Version = "2.0.0"]', '[Version = "'+$($env:VERSION)+'"]') | Set-Content ./Speckle.pq
|
||||
- run:
|
||||
name: "Build Data Connector"
|
||||
command: "msbuild Speckle.proj /restore /consoleloggerparameters:NoSummary /property:GenerateFullPaths=true"
|
||||
- store_artifacts:
|
||||
path: ./bin
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- bin/*
|
||||
deploy-connector:
|
||||
docker:
|
||||
- image: cibuilds/github:0.10
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: "Publish Release on GitHub"
|
||||
command: |
|
||||
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} ./bin/
|
||||
workflows:
|
||||
build:
|
||||
jobs:
|
||||
- build-connector
|
||||
deploy:
|
||||
jobs:
|
||||
- build-connector:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/ # For testing only: /ci\/.*/
|
||||
tags:
|
||||
only: /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w{1,10})?$/
|
||||
- deploy-connector:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/ # For testing only: /ci\/.*/
|
||||
tags:
|
||||
only: /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w{1,10})?$/
|
||||
requires:
|
||||
- build-connector
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
PROJECT_NUMBER: 11
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
|
||||
echo "$PROJECT_ID"
|
||||
echo "$STATUS_FIELD_ID"
|
||||
|
||||
|
||||
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo "$DONE_ID"
|
||||
|
||||
@@ -52,9 +52,9 @@ jobs:
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Update Status
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
@@ -75,4 +75,3 @@ jobs:
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
PROJECT_NUMBER: 11
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Add Issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
@@ -46,5 +46,5 @@ jobs:
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "powerquery",
|
||||
"request": "launch",
|
||||
"name": "Test powerquery file",
|
||||
"program": "${workspaceFolder}/${command:AskForPowerQueryFileName}",
|
||||
"additionalArgs": ["--logMashupEngineTraces", "user"],
|
||||
"preLaunchTask": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"powerquery.general.mode": "SDK",
|
||||
"powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\Speckle.mez",
|
||||
"powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\Speckle.query.pq"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "powerquery",
|
||||
"operation": "msbuild",
|
||||
"additionalArgs": [
|
||||
"/restore",
|
||||
"/consoleloggerparameters:NoSummary",
|
||||
"/property:GenerateFullPaths=true"
|
||||
],
|
||||
"problemMatcher": ["$msCompile"],
|
||||
"group": "build",
|
||||
"label": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "tests",
|
||||
"command": "${config:powerquery.sdk.tools.location}\\PQTest.exe",
|
||||
"args": [
|
||||
"run-test",
|
||||
"--extension",
|
||||
"${config:powerquery.sdk.defaultExtension}",
|
||||
"--queryFile",
|
||||
"${workspaceFolder}\\tests",
|
||||
"--prettyPrint"
|
||||
],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
[Version = "2.0.0"]
|
||||
section Speckle;
|
||||
|
||||
// The data source definition, used when connecting to any speckle server
|
||||
Speckle = [
|
||||
// This is used when running the connector on an on-premises data gateway
|
||||
TestConnection = (path) => {"Speckle.Api.GetUser", path},
|
||||
// This is the custom authentication strategy for our Connector
|
||||
Authentication = [
|
||||
OAuth = [
|
||||
Label = "Speckle Login (latest only)",
|
||||
StartLogin = (clientApplication, dataSourcePath, state, display) =>
|
||||
[
|
||||
LoginUri = Text.Combine({"https://latest.speckle.dev/authn/verify", "1ea917e25a", state}, "/"),
|
||||
CallbackUri = "https://oauth.powerbi.com/views/oauthredirect.html",
|
||||
WindowHeight = 800,
|
||||
WindowWidth = 600,
|
||||
Context = null
|
||||
],
|
||||
FinishLogin = (clientApplication, dataSourcePath, context, callbackUri, state) =>
|
||||
let
|
||||
Parts = Uri.Parts(callbackUri)[Query],
|
||||
Source = Web.Contents(
|
||||
Text.Combine({"https://latest.speckle.dev", "auth", "token"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Json.FromValue(
|
||||
[
|
||||
accessCode = Parts[access_code],
|
||||
appId = "1ea917e25a",
|
||||
appSecret = "7d1cb26028",
|
||||
challenge = state
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source)
|
||||
in
|
||||
[
|
||||
access_token = json[token],
|
||||
scope = null,
|
||||
token_type = "bearer",
|
||||
refresh_token = json[refreshToken]
|
||||
],
|
||||
Refresh = (dataSourcePath, refreshToken) =>
|
||||
let
|
||||
Source = Web.Contents(
|
||||
Text.Combine({"https://latest.speckle.dev", "auth", "token"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Json.FromValue(
|
||||
[
|
||||
refreshToken = refreshToken,
|
||||
appId = "1ea917e25a",
|
||||
appSecret = "7d1cb26028"
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source)
|
||||
in
|
||||
[
|
||||
access_token = json[token],
|
||||
scope = null,
|
||||
token_type = "bearer",
|
||||
refresh_token = json[refreshToken]
|
||||
],
|
||||
Logout = (clientApplication, dataSourcePath, accessToken) =>
|
||||
let
|
||||
Source = Web.Contents(
|
||||
Text.Combine({"https://latest.speckle.dev", "auth", "logout"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Json.FromValue([
|
||||
token = accessToken
|
||||
])
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source)
|
||||
in
|
||||
json
|
||||
],
|
||||
Key = [
|
||||
KeyLabel = "Personal Access Token",
|
||||
Label = "Private stream"
|
||||
],
|
||||
Implicit = [
|
||||
Label = "Public stream"
|
||||
]
|
||||
],
|
||||
Label = "Speckle"
|
||||
];
|
||||
|
||||
// Gets the object referenced by a specific speckle URL
|
||||
[DataSource.Kind = "Speckle", Publish = "Get.ByUrl.Publish"]
|
||||
shared Speckle.GetByUrl.Structured = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Get.ByUrl.pqm"),
|
||||
type function (
|
||||
url as (
|
||||
Uri.Type meta [
|
||||
Documentation.FieldCaption = "Gets a Speckle Object preserving it's structure",
|
||||
Documentation.FieldDescription = "The url of a stream in a Speckle server. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {
|
||||
"https://speckle.xyz/streams/23401adf",
|
||||
"https://speckle.xyz/streams/23401adf/branches/main"
|
||||
}
|
||||
]
|
||||
)
|
||||
) as record meta [
|
||||
Documentation.Name = "Speckle - Get Structured Object by URL",
|
||||
Documentation.LongDescription = "Returns the Speckle object the URL points to, while also preserving it's structure.
|
||||
Supports all types of stream url:#(lf)
|
||||
- Stream: will get the latest commit on the 'main' branch (i.e. 'https://speckle.xyz/streams/STREAM_ID')#(lf)
|
||||
- Branch: will get the latest commit on the specified branch (i.e. 'https://speckle.xyz/streams/STREAM_ID/branches/BRANCH_NAME')#(lf)
|
||||
- Commit: will get a specific commit from the stream (i.e. 'https://speckle.xyz/streams/STREAM_ID/commits/COMMIT_ID')
|
||||
"
|
||||
]
|
||||
);
|
||||
|
||||
// [DataSource.Kind = "Speckle", Publish = "NavTable.Publish"]
|
||||
// shared Speckle.GetObjectAsNavTable = Value.ReplaceType(
|
||||
// NavigationTable.Simple, type function (url as Uri.Type) as table
|
||||
// );
|
||||
// Get's a flat list of speckle objects from a URL
|
||||
[DataSource.Kind = "Speckle", Publish = "GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
type function (
|
||||
url as (
|
||||
Uri.Type meta [
|
||||
Documentation.FieldCaption = "Stream URL",
|
||||
Documentation.FieldDescription = "The url of a stream in a Speckle server. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {
|
||||
"https://speckle.xyz/streams/23401adf",
|
||||
"https://speckle.xyz/streams/23401adf/branches/main"
|
||||
}
|
||||
]
|
||||
)
|
||||
) as table meta [
|
||||
Documentation.Name = "Speckle - Get stream by URL",
|
||||
Documentation.LongDescription = "Returns a flat list of all objects contained in a specific Speckle stream/branch/commit/object.
|
||||
Supports all types of stream url:#(lf)
|
||||
- Stream: will get the latest commit on the 'main' branch (i.e. 'https://speckle.xyz/streams/STREAM_ID')#(lf)
|
||||
- Branch: will get the latest commit on the specified branch (i.e. 'https://speckle.xyz/streams/STREAM_ID/branches/BRANCH_NAME')#(lf)
|
||||
- Commit: will get a specific commit from the stream (i.e. 'https://speckle.xyz/streams/STREAM_ID/commits/COMMIT_ID')
|
||||
"
|
||||
]
|
||||
);
|
||||
|
||||
// Gets the current authenticated user, if any
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Api.GetUser = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Api.GetUser.pqm"), type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
// Generic fetch function to our GraphQL endpoint
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Api.Fetch = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Api.Fetch.pqm"),
|
||||
type function (url as Uri.Type, optional query as text, optional variables as record) as record
|
||||
);
|
||||
|
||||
// Parses a stream url and returns a record with the type and values
|
||||
shared Speckle.ParseUrl = Speckle.LoadFunction("ParseStreamUrl.pqm");
|
||||
|
||||
// [DataSource.Kind = "Speckle"]
|
||||
// shared Speckle.Api.REST.GetObject = Value.ReplaceType(
|
||||
// Speckle.LoadFunction("Api.REST.GetObject.pqm"),
|
||||
// type function (url as Uri.Type, optional streamId as text, optional objectId as text) as list
|
||||
// );
|
||||
Get.ByUrl.Publish = GetPublish("GetStream");
|
||||
|
||||
NavTable.Publish = GetPublish("GetObjectAsNavTable");
|
||||
|
||||
GetByUrl.Publish = GetPublish("GetByUrl");
|
||||
|
||||
GetPublish = Speckle.LoadFunction("GetPublish.pqm");
|
||||
|
||||
// Navigation table utility function
|
||||
Table.ToNavigationTable = Speckle.LoadFunction("Table.ToNavigationTable.pqm");
|
||||
|
||||
// Function to load `pqm` files
|
||||
shared Speckle.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Speckle.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="BuildMez">
|
||||
<PropertyGroup>
|
||||
<OutputPath Condition="'$(OutputPath)' == ''">$(MSBuildProjectDirectory)\bin\</OutputPath>
|
||||
<IntermediateOutputPath Condition="'$(IntermediateOutputPath)' == ''">$(MSBuildProjectDirectory)\obj\</IntermediateOutputPath>
|
||||
<MezIntermediatePath>$(IntermediateOutputPath)MEZ\</MezIntermediatePath>
|
||||
<MezOutputPath>$(OutputPath)$(MsBuildProjectName).mez</MezOutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<MezContent Include="Speckle.pq" />
|
||||
<MezContent Include="utilities\**\*.pqm" />
|
||||
<MezContent Include="speckle\**\*.pqm" />
|
||||
<MezContent Include="assets\SpeckleLogo16.png" />
|
||||
<MezContent Include="assets\SpeckleLogo20.png" />
|
||||
<MezContent Include="assets\SpeckleLogo24.png" />
|
||||
<MezContent Include="assets\SpeckleLogo32.png" />
|
||||
<MezContent Include="assets\SpeckleLogo40.png" />
|
||||
<MezContent Include="assets\SpeckleLogo48.png" />
|
||||
<MezContent Include="assets\SpeckleLogo64.png" />
|
||||
<MezContent Include="assets\SpeckleLogo80.png" />
|
||||
<MezContent Include="assets\resources.resx" />
|
||||
</ItemGroup>
|
||||
<Target Name="BuildMez" AfterTargets="Build" Inputs="@(MezContent)" Outputs="$(MezOutputPath)">
|
||||
<RemoveDir Directories="$(MezIntermediatePath)" />
|
||||
<Copy SourceFiles="@(MezContent)" DestinationFolder="$(MezIntermediatePath)" />
|
||||
<MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
|
||||
<ZipDirectory SourceDirectory="$(MezIntermediatePath)" DestinationFile="$(MezOutputPath)" Overwrite="true" />
|
||||
</Target>
|
||||
<Target Name="CopyToConnectors" AfterTargets="BuildMez">
|
||||
<Message Text="Copying .mez file to: $(UserProfile)\Documents\Power BI Desktop\Custom Connectors" Importance="High" />
|
||||
<MakeDir Directories="$(UserProfile)\Documents\Power BI Desktop\Custom Connectors\" />
|
||||
<Copy SourceFiles="$(MezOutputPath)" DestinationFolder="$(UserProfile)\Documents\Power BI Desktop\Custom Connectors\"/>
|
||||
</Target>
|
||||
<Target Name="Clean">
|
||||
<RemoveDir Directories="$(MezIntermediatePath)" />
|
||||
<Delete Files="$(MezOutputPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.Api.Fetch("https://latest.speckle.dev") in Record.ToTable(result)
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30804.86
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Speckle", "Speckle\Speckle.mproj", "{D61F9470-E614-45C7-8047-E354FF219408}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D61F9470-E614-45C7-8047-E354FF219408}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{D61F9470-E614-45C7-8047-E354FF219408}.Debug|x86.Build.0 = Debug|x86
|
||||
{D61F9470-E614-45C7-8047-E354FF219408}.Release|x86.ActiveCfg = Release|x86
|
||||
{D61F9470-E614-45C7-8047-E354FF219408}.Release|x86.Build.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {765772BB-45FA-4661-9573-F0F182FEB7C1}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,121 +0,0 @@
|
||||
<Project DefaultTargets="BuildExtension" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{0b5fe03d-aad9-4bc4-9edf-b543d5e50d29}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>MyRootNamespace</RootNamespace>
|
||||
<AssemblyName>MyAssemblyName</AssemblyName>
|
||||
<EnableUnmanagedDebugging>False</EnableUnmanagedDebugging>
|
||||
<AllowNativeQuery>False</AllowNativeQuery>
|
||||
<AsAction>False</AsAction>
|
||||
<FastCombine>False</FastCombine>
|
||||
<ClearLog>False</ClearLog>
|
||||
<ShowEngineTraces>False</ShowEngineTraces>
|
||||
<ShowUserTraces>False</ShowUserTraces>
|
||||
<LegacyRedirects>False</LegacyRedirects>
|
||||
<SuppressRowErrors>False</SuppressRowErrors>
|
||||
<SuppressCellErrors>False</SuppressCellErrors>
|
||||
<MaxRows>1000</MaxRows>
|
||||
<ExtensionProject>Yes</ExtensionProject>
|
||||
<Name>Speckle</Name>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<!--Should be true, fix this when the debugger is implemented -->
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Speckle.pq">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo16.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo20.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo24.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo32.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo40.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo48.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo64.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SpeckleLogo80.png">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="resources.resx">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Content Include="Speckle.query.pq">
|
||||
<SubType>Code</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<UsingTask TaskName="BuildExtension" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
|
||||
<ParameterGroup>
|
||||
<InputDirectory ParameterType="System.String" Required="true" />
|
||||
<OutputFile ParameterType="System.String" Required="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Using Namespace="System.Globalization" />
|
||||
<Using Namespace="System.IO.Compression " />
|
||||
<Code Type="Fragment" Language="cs"><![CDATA[
|
||||
using(FileStream fileStream = File.Create(OutputFile))
|
||||
using(ZipArchive archiveOut = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
{
|
||||
foreach(string fullPath in Directory.EnumerateFiles(InputDirectory))
|
||||
{
|
||||
string filename = Path.GetFileName(fullPath);
|
||||
|
||||
archiveOut.CreateEntryFromFile(fullPath, filename, CompressionLevel.Optimal);
|
||||
}
|
||||
}
|
||||
]]></Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
<Target Name="BuildExtension" DependsOnTargets="ExtensionClean">
|
||||
<ItemGroup>
|
||||
<PQFiles Include="@(Compile)" Condition="'%(Extension)' == '.pq'" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<NonPQFiles Include="@(Compile)" Condition="'%(Extension)' != '.pq'" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(IntermediateOutputPath)" />
|
||||
<MakeDir Directories="$(OutputPath)" />
|
||||
<Copy SourceFiles="@(NonPQFiles)" DestinationFolder="$(IntermediateOutputPath)" />
|
||||
<Copy SourceFiles="@(PQFiles)" DestinationFiles="@(PQFiles->'$(IntermediateOutputPath)%(RecursiveDir)%(FileName).m')" />
|
||||
<BuildExtension InputDirectory="$(IntermediateOutputPath)" OutputFile="$(OutputPath)\$(ProjectName).mez" />
|
||||
<Message Text="Copying .mez file to: $(UserProfile)\Documents\Power BI Desktop\Custom Connectors" Importance="High" />
|
||||
<MakeDir Directories="$(UserProfile)\Documents\Power BI Desktop\Custom Connectors\" />
|
||||
<Copy SourceFiles="$(OutputPath)\$(ProjectName).mez" DestinationFolder="$(UserProfile)\Documents\Power BI Desktop\Custom Connectors">
|
||||
</Copy>
|
||||
</Target>
|
||||
<Target Name="ExtensionClean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(OutputPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,260 +0,0 @@
|
||||
section Speckle;
|
||||
|
||||
/* This is an additional nav bar that can display branches of a stream: not using this for now
|
||||
[DataSource.Kind="Speckle", Publish="Speckle.Publish"]
|
||||
shared Speckle.Contents = Value.ReplaceType(NavigationTable.Simple, type function (url as Uri.Type) as any);
|
||||
|
||||
// set up nav table
|
||||
shared NavigationTable.Simple = (url) as table =>
|
||||
let
|
||||
baseUrl = Uri.Parts(url)[Host] as text,
|
||||
streamId = Text.Split(Uri.Parts(url)[Path], "/"){2},
|
||||
objects = Speckle.GetBranches(baseUrl, streamId),
|
||||
table = #table(
|
||||
{"Name", "Key", "Data", "ItemKind", "ItemName", "IsLeaf"},
|
||||
List.InsertRange(objects, List.Count(objects), {{"GetCommit", "GetCommit", Speckle.GetObjectFromObject(baseUrl, streamId), "Function", "Function", true}})
|
||||
),
|
||||
NavTable = Table.ToNavigationTable(table, {"Key"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf")
|
||||
in
|
||||
NavTable;
|
||||
|
||||
Speckle.GetBranches = (url, id) =>
|
||||
let
|
||||
Source = Web.Contents(
|
||||
Text.Combine({"https:/", url, "graphql"}, "/"),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST",
|
||||
#"Content-Type"="application/json"
|
||||
],
|
||||
Content=Text.ToBinary("{""query"": ""query { stream( id: \"""&id&"\"" ) { branches { items { name commits { items { id message sourceApplication authorName createdAt } } } } } }""}")
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source),
|
||||
branches = #"JSON"[data][stream][branches][items],
|
||||
branchList = List.Generate(
|
||||
() => [x = 0, y = Speckle.GetBranchAsList(branches{x})],
|
||||
each [x] < List.Count(branches),
|
||||
each [x = [x] + 1, y = Speckle.GetBranchAsList(branches{x})],
|
||||
each [y]
|
||||
)
|
||||
in
|
||||
branchList;
|
||||
|
||||
Speckle.GetBranchAsList = (branchRecord) =>
|
||||
let
|
||||
commits = Table.FromRecords(branchRecord[commits][items]),
|
||||
list = {branchRecord[name], branchRecord[name], commits, "Table", "Table", true}
|
||||
in
|
||||
list;
|
||||
*/
|
||||
[DataSource.Kind="Speckle", Publish="Speckle.Publish"]
|
||||
shared Speckle.Contents = Value.ReplaceType(CommitTable, type function (StreamUrl as Uri.Type) as any);
|
||||
|
||||
/* INFO: Variables will not be instantiated (or any code run) until they are used */
|
||||
shared CommitTable = (url) as table =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
s = Text.Combine({"https://", Uri.Parts(url)[Host]}),
|
||||
server = Speckle.LogToMatomo(s),
|
||||
|
||||
segments = Text.Split(Text.AfterDelimiter(Uri.Parts(url)[Path], "/", 0), "/"),
|
||||
streamId = segments{1},
|
||||
branchName = if( List.Count(segments) = 4 and segments{2} = "branches" ) then segments{3} else null,
|
||||
commitId = if (List.Count(segments) = 4 and segments{2} = "commits" ) then segments{3} else null,
|
||||
objectId = if (List.Count(segments) = 4 and segments{2} = "objects" ) then segments{3} else null,
|
||||
|
||||
commitTable = if (commitId <> null) then Speckle.GetObjectFromCommit(server, streamId, commitId)
|
||||
else if (objectId <> null) then Speckle.GetObjectFromObject(server, streamId, objectId, false)
|
||||
else if (branchName <> null) then #table( { "Error" }, { { "Invalid URL, use a stream or commit or object url" } } ) // currently not implemented, see reason below
|
||||
else Speckle.GetObjectFromStream(server, streamId)
|
||||
in
|
||||
commitTable;
|
||||
|
||||
|
||||
/* Since everything is lazily evaluated, we must join and split the result of the matomo call with the server, and spit back the server url for PowerBI to actually log the calls to Matomo */
|
||||
Speckle.LogToMatomo = (server) =>
|
||||
let
|
||||
matomoUrl = "https://speckle.matomo.cloud/matomo.php",
|
||||
action = "receive/manual",
|
||||
appName = "Power BI",
|
||||
userId = "powerBIuser",
|
||||
|
||||
params = [
|
||||
idsite = "2",
|
||||
rec = "1",
|
||||
apiv = "1",
|
||||
uid = userId,
|
||||
action_name = action,
|
||||
url = Text.Combine({"http://connectors/PowerBI/", action}),
|
||||
urlref = Text.Combine({"http://connectors/PowerBI/", action}),
|
||||
_cvar = Text.FromBinary(Json.FromValue([hostApplication = appName]))
|
||||
],
|
||||
visitQuery = Uri.BuildQueryString(params),
|
||||
visitRes = Web.Contents(Text.Combine({matomoUrl, "?", visitQuery}),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST"
|
||||
],
|
||||
Content=Text.ToBinary(server)
|
||||
]),
|
||||
|
||||
eventParams = [
|
||||
idsite = "2",
|
||||
rec = "1",
|
||||
apiv = "1",
|
||||
uid = userId,
|
||||
_cvar = Text.FromBinary(Json.FromValue([hostApplication = appName])),
|
||||
e_c = appName,
|
||||
e_a = action
|
||||
],
|
||||
eventQuery = Uri.BuildQueryString(eventParams),
|
||||
eventRes = Web.Contents(Text.Combine({ matomoUrl, "?", eventQuery}),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST"
|
||||
],
|
||||
Content=Text.ToBinary(server)
|
||||
]),
|
||||
Result = Text.FromBinary(visitRes) & Text.FromBinary(eventRes),
|
||||
Combined = Text.Combine({server,Result},"___"),
|
||||
Split = Text.Split(Combined,"___"){0}
|
||||
in
|
||||
Split;
|
||||
|
||||
Speckle.GetObjectFromStream = (server, streamId) =>
|
||||
|
||||
let
|
||||
branchName = "main",
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST",
|
||||
#"Content-Type"="application/json"
|
||||
],
|
||||
Content=Text.ToBinary("{""query"": ""query { stream( id: \"""&streamId&"\"" ) { branch (name: \"""&branchName&"\""){ commits (limit: 1) { items { referencedObject } } } } }""}")
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source),
|
||||
objectId = #"JSON"[data][stream][branch][commits][items]{0}[referencedObject],
|
||||
objectsTable = Speckle.GetObjectFromObject(server, streamId, objectId, true)
|
||||
in
|
||||
objectsTable;
|
||||
|
||||
/* Not implemented since power query M uri does not have a decode method...def not manually writing a method to handle special chars and emojis
|
||||
Speckle.GetObjectFromBranch = (server, streamId, branchName) =>
|
||||
let
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST",
|
||||
#"Content-Type"="application/json"
|
||||
],
|
||||
Content=Text.ToBinary("{""query"": ""query { stream( id: \"""&streamId&"\"" ) { branch (name: \"""&branchName&"\""){ commits (limit: 1) { items { referencedObject } } } } }""}")
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source),
|
||||
objectId = #"JSON"[data][stream][branch][commits][items]{0}[referencedObject],
|
||||
objectsTable = Speckle.GetObjectFromObject(server, streamId, objectId)
|
||||
in
|
||||
objectsTable;
|
||||
*/
|
||||
|
||||
shared Speckle.GetObjectFromCommit = (server, streamId, commitId) =>
|
||||
let
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST",
|
||||
#"Content-Type"="application/json"
|
||||
],
|
||||
Content=Text.ToBinary("{""query"": ""query { stream( id: \"""&streamId&"\"" ) { commit (id: \"""&commitId&"\""){ referencedObject } } }""}")
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source),
|
||||
objectId = #"JSON"[data][stream][commit][referencedObject],
|
||||
objectsTable = Speckle.GetObjectFromObject(server, streamId, objectId, true)
|
||||
in
|
||||
objectsTable;
|
||||
|
||||
Speckle.GetObjectFromObject = (server, streamId, objectId, IsCommitObject) =>
|
||||
let
|
||||
query = if (IsCommitObject) then "{""query"": ""query { stream( id: \"""&streamId&"\"" ) { object (id: \"""&objectId&"\"") { children { objects { data } } } } }""}"
|
||||
else "{""query"": ""query { stream( id: \"""&streamId&"\"" ) { object (id: \"""&objectId&"\"") { data } } }""}",
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers=[
|
||||
#"Method"="POST",
|
||||
#"Content-Type"="application/json"
|
||||
],
|
||||
Content=Text.ToBinary(query)
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source),
|
||||
objects = if (IsCommitObject) then #"JSON"[data][stream][object][children][objects]
|
||||
else {#"JSON"[data][stream][object][data]},
|
||||
|
||||
// remove closures from records, and remove DataChunk records
|
||||
removeClosureField = List.Transform(objects, each Record.RemoveFields(_, "__closure", MissingField.Ignore)),
|
||||
removeDatachunkRecords = List.RemoveItems(removeClosureField, List.FindText(removeClosureField, "Speckle.Core.Models.DataChunk")),
|
||||
|
||||
objectsTable = Table.FromRecords(removeDatachunkRecords)
|
||||
in
|
||||
objectsTable;
|
||||
|
||||
// Data Source Kind description
|
||||
Speckle = [
|
||||
Authentication = [
|
||||
Key = [
|
||||
KeyLabel="Personal Access Token",
|
||||
Label = "Private stream"
|
||||
],
|
||||
Implicit = [
|
||||
Label = "Public stream"
|
||||
]
|
||||
],
|
||||
Label = Extension.LoadString("Speckle Connector")
|
||||
];
|
||||
|
||||
// Data Source UI publishing description
|
||||
Speckle.Publish = [
|
||||
Beta = true,
|
||||
Category = "Other",
|
||||
ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") },
|
||||
LearnMoreUrl = "https://speckle.guide",
|
||||
SourceImage = Speckle.Icons,
|
||||
SourceTypeImage = Speckle.Icons
|
||||
];
|
||||
|
||||
Speckle.Icons = [
|
||||
Icon16 = { Extension.Contents("SpeckleLogo16.png"), Extension.Contents("SpeckleLogo20.png"), Extension.Contents("SpeckleLogo24.png"), Extension.Contents("SpeckleLogo32.png") },
|
||||
Icon32 = { Extension.Contents("SpeckleLogo32.png"), Extension.Contents("SpeckleLogo40.png"), Extension.Contents("SpeckleLogo48.png"), Extension.Contents("SpeckleLogo64.png") }
|
||||
];
|
||||
|
||||
// copy and pasted function from microsoft docs since it's not included yet in M standard lib
|
||||
Table.ToNavigationTable = (
|
||||
table as table,
|
||||
keyColumns as list,
|
||||
nameColumn as text,
|
||||
dataColumn as text,
|
||||
itemKindColumn as text,
|
||||
itemNameColumn as text,
|
||||
isLeafColumn as text
|
||||
) as table =>
|
||||
let
|
||||
tableType = Value.Type(table),
|
||||
newTableType = Type.AddTableKey(tableType, keyColumns, true) meta
|
||||
[
|
||||
NavigationTable.NameColumn = nameColumn,
|
||||
NavigationTable.DataColumn = dataColumn,
|
||||
NavigationTable.ItemKindColumn = itemKindColumn,
|
||||
Preview.DelayColumn = itemNameColumn,
|
||||
NavigationTable.IsLeafColumn = isLeafColumn
|
||||
],
|
||||
navigationTable = Value.ReplaceType(table, newTableType)
|
||||
in
|
||||
navigationTable;
|
||||
@@ -1,5 +0,0 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let
|
||||
result = Speckle.Contents("https://speckle.xyz/streams/5b97b37b8b")
|
||||
in
|
||||
result
|
||||
@@ -1,129 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ButtonHelp" xml:space="preserve">
|
||||
<value>Connect to Speckle</value>
|
||||
</data>
|
||||
<data name="ButtonTitle" xml:space="preserve">
|
||||
<value>Speckle</value>
|
||||
</data>
|
||||
<data name="DataSourceLabel" xml:space="preserve">
|
||||
<value>Speckle</value>
|
||||
</data>
|
||||
</root>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="GetByUrl.Help" xml:space="preserve">
|
||||
<value>Connect to Speckle by Stream URL</value>
|
||||
</data>
|
||||
<data name="GetByUrl.Label" xml:space="preserve">
|
||||
<value>Speckle</value>
|
||||
</data>
|
||||
<data name="GetByUrl.Title" xml:space="preserve">
|
||||
<value>Speckle - Get stream by URL</value>
|
||||
</data>
|
||||
<data name="GetObjFromBranch.Help" xml:space="preserve">
|
||||
<value>Connect to Speckle by serer URL, stream ID and branch name</value>
|
||||
</data>
|
||||
<data name="GetObjFromBranch.Label" xml:space="preserve">
|
||||
<value>Get the latest commit from a stream's branch</value>
|
||||
</data>
|
||||
<data name="GetObjFromBranch.Title" xml:space="preserve">
|
||||
<value>Speckle - Get Stream branch</value>
|
||||
</data>
|
||||
<data name="GetObjFromCommit.Help" xml:space="preserve">
|
||||
<value>Connect to Speckle by server URL, stream ID and commit ID</value>
|
||||
</data>
|
||||
<data name="GetObjFromCommit.Label" xml:space="preserve">
|
||||
<value>A label</value>
|
||||
</data>
|
||||
<data name="GetObjFromCommit.Title" xml:space="preserve">
|
||||
<value>Speckle - Get Stream commit</value>
|
||||
</data>
|
||||
<data name="GetStream.Help" xml:space="preserve">
|
||||
<value>Connect to Speckle by server URL and stream ID</value>
|
||||
</data>
|
||||
<data name="GetStream.Label" xml:space="preserve">
|
||||
<value>Speckle</value>
|
||||
</data>
|
||||
<data name="GetStream.Title" xml:space="preserve">
|
||||
<value>Speckle - Get Stream</value>
|
||||
</data>
|
||||
<data name="GetObjectAsNavTable.Title" xml:space="preserve">
|
||||
<value>Speckle - Get Object as NavTable</value>
|
||||
</data>
|
||||
<data name="GetObjectAsNavTable.Label" xml:space="preserve">
|
||||
<value>Speckle</value>
|
||||
</data>
|
||||
<data name="GetObjectAsNavTable.Help" xml:space="preserve">
|
||||
<value>Returns a navigation table for a given object</value>
|
||||
</data>
|
||||
<data name="Traverse.Title" xml:space="preserve">
|
||||
<value>Traverse an object and populate refs</value>
|
||||
</data>
|
||||
<data name="Traverse.Label" xml:space="preserve">
|
||||
<value>Traverse</value>
|
||||
</data>
|
||||
<data name="Traverse.Help" xml:space="preserve">
|
||||
<value>Traverse help</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,48 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, branchName as text, limit as number) as list =>
|
||||
let
|
||||
decodedBranchName = Record.Field(
|
||||
Record.Field(Uri.Parts("http://www.dummy.com?" & Uri.BuildQueryString([A = branchName])), "Query"),
|
||||
"A"
|
||||
),
|
||||
// Hacky way to decode base64 strings: Put them in a url query param and parse the URL
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $branchName: String!, $limit: Int!) {
|
||||
stream( id: $streamId ) {
|
||||
branch (name: $branchName ){
|
||||
commits (limit: $limit) {
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
res = Fetch(server, query, [streamId = streamId, branchName = decodedBranchName, limit = limit]),
|
||||
branch = res[stream][branch],
|
||||
commits = branch[commits][items]
|
||||
in
|
||||
if branch = null then
|
||||
error Text.Format("The branch '#{0}' does not exist in stream '#{1}'", {decodedBranchName, streamId})
|
||||
else if List.Count(branch[commits][items]) = 0 then
|
||||
error Text.Format("The branch '#{0}' in stream #{1} has no commits", {decodedBranchName, streamId})
|
||||
else
|
||||
commits
|
||||
@@ -0,0 +1,42 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Traverse = Extension.LoadFunction("Traverse.pqm"),
|
||||
GetObject = Extension.LoadFunction("Api.GetObject.pqm"),
|
||||
GetStreamCommit = Extension.LoadFunction("Get.StreamCommit.pqm"),
|
||||
GetBranchCommits = Extension.LoadFunction("Get.BranchCommits.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
ParseStreamUrl = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
CleanUpObject = Extension.LoadFunction("CleanUpObject.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(url as text) as record =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
stream = ParseStreamUrl(url),
|
||||
id = stream[id],
|
||||
server = stream[server],
|
||||
commit =
|
||||
if (stream[urlType] = "Stream") then
|
||||
GetBranchCommits(server, id, "main", 1){0}
|
||||
else if (stream[urlType] = "Branch") then
|
||||
GetBranchCommits(server, id, stream[branch], 1){0}
|
||||
else if (stream[urlType] = "Commit") then
|
||||
GetStreamCommit(server, id, stream[commit])
|
||||
else
|
||||
//We deal with object URLs directly
|
||||
[referencedObject = stream[object]],
|
||||
object = GetObject(server, id, commit[referencedObject])
|
||||
in
|
||||
Traverse(CleanUpObject(object) meta [server = server, stream = id, commit = commit])
|
||||
@@ -0,0 +1,36 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, commitId as text) as record =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $commitId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
commit (id: $commitId) {
|
||||
id
|
||||
sourceApplication
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}",
|
||||
variables = [streamId = streamId, commitId = commitId],
|
||||
#"JSON" = Fetch(server, query, variables),
|
||||
commit = #"JSON"[stream][commit]
|
||||
in
|
||||
if commit = null then
|
||||
error "The commit did not exist on this stream"
|
||||
else
|
||||
commit
|
||||
@@ -0,0 +1,64 @@
|
||||
(appName as text) =>
|
||||
let
|
||||
replaced = Text.Replace(appName, " ", ""), name = Text.Lower(replaced)
|
||||
in
|
||||
if Text.Contains(name, "dynamo") then
|
||||
"dynamo"
|
||||
else if Text.Contains(name, "revit") then
|
||||
"revit"
|
||||
else if Text.Contains(name, "autocad") then
|
||||
"autocad"
|
||||
else if Text.Contains(name, "civil") then
|
||||
"civil"
|
||||
else if Text.Contains(name, "rhino") then
|
||||
"rhino"
|
||||
else if Text.Contains(name, "grasshopper") then
|
||||
"grasshopper"
|
||||
else if Text.Contains(name, "unity") then
|
||||
"unity"
|
||||
else if Text.Contains(name, "gsa") then
|
||||
"gsa"
|
||||
else if Text.Contains(name, "microstation") then
|
||||
"microstation"
|
||||
else if Text.Contains(name, "openroads") then
|
||||
"openroads"
|
||||
else if Text.Contains(name, "openrail") then
|
||||
"openrail"
|
||||
else if Text.Contains(name, "openbuildings") then
|
||||
"openbuildings"
|
||||
else if Text.Contains(name, "etabs") then
|
||||
"etabs"
|
||||
else if Text.Contains(name, "sap") then
|
||||
"sap"
|
||||
else if Text.Contains(name, "csibridge") then
|
||||
"csibridge"
|
||||
else if Text.Contains(name, "safe") then
|
||||
"safe"
|
||||
else if Text.Contains(name, "teklastructures") then
|
||||
"teklastructures"
|
||||
else if Text.Contains(name, "dxf") then
|
||||
"dxf"
|
||||
else if Text.Contains(name, "excel") then
|
||||
"excel"
|
||||
else if Text.Contains(name, "unreal") then
|
||||
"unreal"
|
||||
else if Text.Contains(name, "powerbi") then
|
||||
"powerbi"
|
||||
else if Text.Contains(name, "blender") then
|
||||
"blender"
|
||||
else if Text.Contains(name, "qgis") then
|
||||
"qgis"
|
||||
else if Text.Contains(name, "arcgis") then
|
||||
"arcgis"
|
||||
else if Text.Contains(name, "sketchup") then
|
||||
"sketchup"
|
||||
else if Text.Contains(name, "archicad") then
|
||||
"archicad"
|
||||
else if Text.Contains(name, "topsolid") then
|
||||
"topsolid"
|
||||
else if Text.Contains(name, "python") then
|
||||
"python"
|
||||
else if Text.Contains(name, "net") then
|
||||
"net"
|
||||
else
|
||||
"other"
|
||||
@@ -0,0 +1,48 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
GetObjectFromCommit = Extension.LoadFunction("GetObjectFromCommit.pqm"),
|
||||
GetObjectFromBranch = Extension.LoadFunction("GetObjectFromBranch.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
ParseStreamUrl = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(url as text) as table =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
stream = ParseStreamUrl(url),
|
||||
id = stream[id],
|
||||
server = stream[server],
|
||||
commitObjectsTable =
|
||||
if (stream[urlType] = "Commit") then
|
||||
GetObjectFromCommit(server, id, stream[commit])
|
||||
else if (stream[urlType] = "Object") then
|
||||
GetAllObjectChildren(server, id, stream[object])
|
||||
else if (stream[urlType] = "Branch") then
|
||||
GetObjectFromBranch(server, id, stream[branch])
|
||||
else
|
||||
GetObjectFromBranch(server, id, "main"),
|
||||
removeEmpty = Table.RemoveLastN(commitObjectsTable, 1),
|
||||
addStreamUrl = Table.AddColumn(removeEmpty, "Stream URL", each server & "/streams/" & id),
|
||||
addUrlType = Table.AddColumn(addStreamUrl, "URL Type", each stream[urlType]),
|
||||
addObjectIdCol = Table.AddColumn(addUrlType, "Object ID", each try[data][id] otherwise null),
|
||||
addSpeckleTypeCol = Table.AddColumn(
|
||||
addObjectIdCol, "speckle_type", each try[data][speckle_type] otherwise null
|
||||
),
|
||||
final = Table.ReorderColumns(
|
||||
addSpeckleTypeCol, {"Stream URL", "URL Type", "Object ID", "speckle_type", "data"}
|
||||
)
|
||||
in
|
||||
final
|
||||
@@ -0,0 +1,55 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, branchName as text) as table =>
|
||||
let
|
||||
decodedBranchName = Record.Field(
|
||||
Record.Field(Uri.Parts("http://www.dummy.com?" & Uri.BuildQueryString([A = branchName])), "Query"),
|
||||
"A"
|
||||
),
|
||||
// Hacky way to decode base64 strings: Put them in a url query param and parse the URL
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $branchName: String!) {
|
||||
stream( id: $streamId ) {
|
||||
branch (name: $branchName ){
|
||||
commits (limit: 1) {
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
res = Fetch(server, query, [streamId = streamId, branchName = decodedBranchName]),
|
||||
branch = res[stream][branch],
|
||||
commit = branch[commits][items]{0},
|
||||
objectsTable = GetAllObjectChildren(server, streamId, commit[referencedObject]),
|
||||
rr = CommitReceived(server, streamId, commit)
|
||||
in
|
||||
if branch = null then
|
||||
error Text.Format("The branch '#{0}' does not exist in stream '#{1}'", {decodedBranchName, streamId})
|
||||
else if List.Count(branch[commits][items]) = 0 then
|
||||
error Text.Format("The branch '#{0}' in stream #{1} has no commits", {decodedBranchName, streamId})
|
||||
else
|
||||
// Force evaluation of read receipt (ideally it should happen after fetching, but can't find a way)
|
||||
if rr then
|
||||
objectsTable
|
||||
else
|
||||
objectsTable
|
||||
@@ -0,0 +1,42 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, commitId as text) as table =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $commitId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
commit (id: $commitId) {
|
||||
id
|
||||
sourceApplication
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}",
|
||||
variables = [streamId = streamId, commitId = commitId],
|
||||
#"JSON" = Fetch(server, query, variables),
|
||||
commit = #"JSON"[stream][commit],
|
||||
objectsTable = GetAllObjectChildren(server, streamId, commit[referencedObject]),
|
||||
rr = CommitReceived(server, streamId, commit)
|
||||
in
|
||||
if commit = null then
|
||||
error "The commit did not exist on this stream"
|
||||
else if rr then
|
||||
objectsTable
|
||||
else
|
||||
objectsTable
|
||||
@@ -0,0 +1,33 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Speckle.LogEvent = Extension.LoadFunction("LogEvent.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server, streamId, commit) =>
|
||||
let
|
||||
query = "mutation($input: CommitReceivedInput!) {
|
||||
commitReceive(input: $input)
|
||||
}",
|
||||
variables = [
|
||||
input = [
|
||||
streamId = streamId,
|
||||
commitId = commit[id],
|
||||
sourceApplication = "PowerBI"
|
||||
]
|
||||
],
|
||||
s = Speckle.LogEvent(server, commit)
|
||||
in
|
||||
// Read receipts should fail gracefully no matter what
|
||||
try Speckle.Api.Fetch(s, query, variables)[commitReceive] otherwise false
|
||||
@@ -0,0 +1,33 @@
|
||||
(server as text, optional query as text, optional variables as record) as record =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
defaultQuery = "query {
|
||||
activeUser {
|
||||
email
|
||||
name
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
version
|
||||
}
|
||||
}",
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400},
|
||||
Content = Json.FromValue([query = Text.From(query ?? defaultQuery), variables = variables])
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source)
|
||||
in
|
||||
// Check if response contains errors, if so, return first error.
|
||||
if Record.HasFields(#"JSON", {"errors"}) then
|
||||
error #"JSON"[errors]{0}[message]
|
||||
else
|
||||
#"JSON"[data]
|
||||
@@ -0,0 +1,35 @@
|
||||
let
|
||||
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm"),
|
||||
Speckle.Api.GetObjectChildren = Extension.LoadFunction("Api.GetObjectChildren.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
// Read all pages of data.
|
||||
// After every page, we check the "nextCursor" record on the metadata of the previous request.
|
||||
// Table.GenerateByPage will keep asking for more pages until we return null.
|
||||
(server as text, streamId as text, objectId as text, optional cursor as text) as table =>
|
||||
Table.GenerateByPage(
|
||||
(previous) =>
|
||||
let
|
||||
// if previous is null, then this is our first page of data
|
||||
nextCursor = if (previous = null) then cursor else Value.Metadata(previous)[Cursor]?,
|
||||
// if the cursor is null but the prevous page is not, we've reached the end
|
||||
page =
|
||||
if (previous <> null and nextCursor = null) then
|
||||
null
|
||||
else
|
||||
Speckle.Api.GetObjectChildren(server, streamId, objectId, 1000, nextCursor)
|
||||
in
|
||||
page
|
||||
) meta [server = server, streamId = streamId, objectId = objectId]
|
||||
@@ -0,0 +1,28 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, objectId as text) =>
|
||||
let
|
||||
query = "query($streamId: String!, $objectId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
object (id: $objectId) {
|
||||
data
|
||||
}
|
||||
}
|
||||
}",
|
||||
#"JSON" = Speckle.Api.Fetch(server, query, [streamId = streamId, objectId = objectId])
|
||||
in
|
||||
#"JSON"[stream][object][data]
|
||||
@@ -0,0 +1,54 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Speckle.CleanUpObjects = Extension.LoadFunction("CleanUpObjects.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(
|
||||
server as text,
|
||||
streamId as text,
|
||||
objectId as text,
|
||||
optional limit as number,
|
||||
optional cursor as text,
|
||||
optional select as list
|
||||
) =>
|
||||
let
|
||||
query = "query($streamId: String!, $objectId: String!, $limit: Int, $cursor: String, $select: [String]) {
|
||||
stream( id: $streamId ) {
|
||||
object (id: $objectId) {
|
||||
children(select: $select, limit: $limit, cursor: $cursor) {
|
||||
cursor
|
||||
objects {
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
#"JSON" = Speckle.Api.Fetch(
|
||||
server,
|
||||
query,
|
||||
[
|
||||
streamId = streamId,
|
||||
objectId = objectId,
|
||||
limit = limit,
|
||||
cursor = cursor,
|
||||
select = select
|
||||
]
|
||||
),
|
||||
children = #"JSON"[stream][object][children],
|
||||
nextCursor = children[cursor],
|
||||
clean = Speckle.CleanUpObjects(children[objects])
|
||||
in
|
||||
Table.FromRecords(clean) meta [Cursor = nextCursor]
|
||||
@@ -0,0 +1,24 @@
|
||||
(url as text) =>
|
||||
let
|
||||
query = "query {
|
||||
activeUser { name email }
|
||||
}",
|
||||
// Imports
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
server = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
// Read receipts should fail gracefully no matter what
|
||||
try Speckle.Api.Fetch(server, query)[activeUser][email] otherwise "hey"
|
||||
@@ -0,0 +1,38 @@
|
||||
(server as text, optional streamId as text, optional objectId as text) as table =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "objects", streamId, objectId}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "GET",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400}
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source),
|
||||
clean = List.Select(json, each _[speckle_type] <> "Speckle.Core.Models.DataChunk"),
|
||||
t = Table.FromColumns({clean}, {"data"}),
|
||||
addStreamUrl = Table.AddColumn(t, "Stream URL", each server & "/streams/" & streamId),
|
||||
addObjectIdCol = Table.AddColumn(addStreamUrl, "Object ID", each try _[data][id] otherwise null),
|
||||
addSpeckleTypeCol = Table.AddColumn(
|
||||
addObjectIdCol, "speckle_type", each try _[data][speckle_type] otherwise null
|
||||
),
|
||||
Speckle.CleanUpObjects = Extension.LoadFunction("CleanUpObjects.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
addSpeckleTypeCol
|
||||
@@ -0,0 +1,7 @@
|
||||
(object as record) as record =>
|
||||
let
|
||||
hiddenFields = {"__closure", "totalChildrenCount"},
|
||||
// remove closures from records
|
||||
clean = Record.RemoveFields(object, hiddenFields, MissingField.Ignore)
|
||||
in
|
||||
clean
|
||||
@@ -0,0 +1,17 @@
|
||||
(objects as list) as list =>
|
||||
let
|
||||
// remove closures from records, and remove DataChunk records
|
||||
removeClosureField = List.Transform(
|
||||
objects, each [data = Record.RemoveFields(_[data], "__closure", MissingField.Ignore)]
|
||||
),
|
||||
removeTotals = List.Transform(
|
||||
removeClosureField,
|
||||
each
|
||||
[
|
||||
data = try
|
||||
Record.RemoveFields(_[data], "totalChildrenCount", MissingField.Ignore) otherwise _[data]
|
||||
]
|
||||
),
|
||||
removed = List.Select(removeTotals, each _[data][speckle_type] <> "Speckle.Core.Models.DataChunk")
|
||||
in
|
||||
removed
|
||||
@@ -0,0 +1,30 @@
|
||||
let
|
||||
beta = true,
|
||||
category = "Other",
|
||||
icons = [
|
||||
Icon16 = {
|
||||
Extension.Contents("SpeckleLogo16.png"),
|
||||
Extension.Contents("SpeckleLogo20.png"),
|
||||
Extension.Contents("SpeckleLogo24.png"),
|
||||
Extension.Contents("SpeckleLogo32.png")
|
||||
},
|
||||
Icon32 = {
|
||||
Extension.Contents("SpeckleLogo32.png"),
|
||||
Extension.Contents("SpeckleLogo40.png"),
|
||||
Extension.Contents("SpeckleLogo48.png"),
|
||||
Extension.Contents("SpeckleLogo64.png")
|
||||
}
|
||||
]
|
||||
in
|
||||
(key as text) as record =>
|
||||
[
|
||||
Beta = beta,
|
||||
Category = category,
|
||||
ButtonText = {
|
||||
Extension.LoadString(Text.Format("#{0}.Title", {key})),
|
||||
Extension.LoadString(Text.Format("#{0}.Label", {key}))
|
||||
},
|
||||
LearnMoreUrl = "https://speckle.guide",
|
||||
SourceImage = icons,
|
||||
SourceTypeImage = icons
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
let
|
||||
GetApplicationSlug = Extension.LoadFunction("GetApplicationSlug.pqm"),
|
||||
GetUser = Extension.LoadFunction("GetUser.pqm"),
|
||||
Hash = Extension.LoadFunction("Hash.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
(server as text, commit as any) =>
|
||||
let
|
||||
trackUrl = "https://analytics.speckle.systems/track?ip=1",
|
||||
user = GetUser(server),
|
||||
body = [
|
||||
event = "Receive",
|
||||
properties = [
|
||||
server_id = Hash(server),
|
||||
token = "acd87c5a50b56df91a795e999812a3a4",
|
||||
hostApp = "powerbi",
|
||||
sourceHostApp = GetApplicationSlug(commit[sourceApplication]),
|
||||
sourceHostAppVersion = commit[sourceApplication]
|
||||
]
|
||||
],
|
||||
Result = Web.Contents(
|
||||
trackUrl,
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Accept" = "text/plain",
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Text.ToBinary(Text.Combine({"data=", Text.FromBinary(Json.FromValue(body))}))
|
||||
]
|
||||
),
|
||||
// Hack to force execution
|
||||
Join = Text.Combine({server, Text.From(Json.Document(Result))}, "_____"),
|
||||
Disjoin = Text.Split(Join, "_____"){0}
|
||||
in
|
||||
Disjoin
|
||||
@@ -0,0 +1,27 @@
|
||||
(url as text) as record =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
server = Text.Combine({"https://", Uri.Parts(url)[Host]}),
|
||||
segments = Text.Split(Text.AfterDelimiter(Uri.Parts(url)[Path], "/", 0), "/"),
|
||||
streamId = segments{1},
|
||||
branchName = if (List.Count(segments) = 4 and segments{2} = "branches") then segments{3} else null,
|
||||
commitId = if (List.Count(segments) = 4 and segments{2} = "commits") then segments{3} else null,
|
||||
objectId = if (List.Count(segments) = 4 and segments{2} = "objects") then segments{3} else null,
|
||||
urlType =
|
||||
if (commitId <> null) then
|
||||
"Commit"
|
||||
else if (objectId <> null) then
|
||||
"Object"
|
||||
else if (branchName <> null) then
|
||||
"Branch"
|
||||
else
|
||||
"Stream"
|
||||
in
|
||||
[
|
||||
urlType = urlType,
|
||||
server = server,
|
||||
id = streamId,
|
||||
branch = branchName,
|
||||
commit = commitId,
|
||||
object = objectId
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
let
|
||||
GetObject = Extension.LoadFunction("Api.GetObject.pqm"),
|
||||
Diagnostics.Log = Extension.LoadFunction("Diagnostics.pqm")[LogValue],
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
//TODO: Not implemented yet
|
||||
TraverseTable = (item as table) as table => item,
|
||||
// Will traverse an undetermined value (list, table, record).
|
||||
TraverseValue = (i as any) as any =>
|
||||
let
|
||||
item = Diagnostics.Log("Traverse value", i) meta Value.Metadata(i)
|
||||
in
|
||||
if Value.Is(item, type list) then
|
||||
// Return a transformed list by traversing all items
|
||||
Diagnostics.Log(
|
||||
"List travered",
|
||||
List.Transform(item, (a) => @TraverseValue(Value.ReplaceMetadata(a, Value.Metadata(i))))
|
||||
)
|
||||
else if Value.Is(item, type record) then
|
||||
// Traverse this record individually
|
||||
TraverseRecord(item)
|
||||
else if Value.Is(item, type table) then
|
||||
// Traverse this table
|
||||
TraverseTable(item)
|
||||
else
|
||||
// If none of the above, assume it's just a primitive type and return it as-is.
|
||||
item,
|
||||
// Traverses a generic record
|
||||
TraverseRecord = (object as record) as any =>
|
||||
let
|
||||
isSpeckle = Diagnostics.Log("Is Speckle", Record.HasFields(object, {"speckle_type"})),
|
||||
isReference = Diagnostics.Log("Is Reference", object[speckle_type] = "reference"),
|
||||
// Get the names of all fields
|
||||
fields = Record.FieldNames(object),
|
||||
// Remove all known fields that don't need traversing
|
||||
cleanFields = List.RemoveItems(fields, {"id", "speckle_type", "applicationId"}),
|
||||
// Transform the list of field names into a set of transform operations
|
||||
transformOps = List.Transform(
|
||||
cleanFields, each {_, (a) => TraverseValue(Value.ReplaceMetadata(a, Value.Metadata(object)))}
|
||||
),
|
||||
// Get the object's metadata (server and stream will be saved in here)
|
||||
info = Value.Metadata(object)
|
||||
in
|
||||
// Transform all fields and return the modified object
|
||||
if (isReference) then
|
||||
// Swap reference for call to GetObject
|
||||
() =>
|
||||
TraverseValue(
|
||||
Value.ReplaceMetadata(
|
||||
GetObject(info[server], info[stream], object[referencedId]), Value.Metadata(object)
|
||||
)
|
||||
)
|
||||
else
|
||||
try Record.TransformFields(object, transformOps, MissingField.Error) otherwise error "oopsies"
|
||||
in
|
||||
TraverseValue
|
||||
@@ -0,0 +1,2 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.Api.Fetch("https://latest.speckle.dev") in Record.ToTable(result)
|
||||
@@ -0,0 +1,7 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let
|
||||
result = Speckle.Api.REST.GetObject(
|
||||
"https://latest.speckle.dev", "5f284e5c70", "85e5f250fe591ea74d8d5dc1137a9341"
|
||||
)
|
||||
in
|
||||
result
|
||||
@@ -0,0 +1,30 @@
|
||||
section UnitTestingUnitTests;
|
||||
|
||||
UT = Speckle.LoadFunction("Facts.pqm");
|
||||
Fact = UT[Fact];
|
||||
|
||||
Facts.Summarize = UT[SummarizeFacts];
|
||||
|
||||
shared Speckle.UnitTest = [
|
||||
// Put any common variables here if you only want them to be evaluated once
|
||||
// Fact(<Name of the Test>, <Expected Value>, <Actual Value>)
|
||||
// <Expected Value> and <Actual Value> can be a literal or let statement
|
||||
facts = {
|
||||
Fact(
|
||||
"Check that this function returns 'ABC'",
|
||||
// name of the test
|
||||
"ABC",
|
||||
// expected value
|
||||
UnitTesting.ReturnsABC()
|
||||
// expression to evaluate (let or single statement)
|
||||
),
|
||||
Fact("Check that this function returns '123'", "123", UnitTesting.Returns123()),
|
||||
Fact("Result should contain 5 rows", 5, Table.RowCount(UnitTesting.ReturnsTableWithFiveRows())),
|
||||
Fact("Values should be equal (using a let statement)", "Hello World", let a = "Hello World" in a)
|
||||
},
|
||||
report = Facts.Summarize(facts)
|
||||
][report];
|
||||
|
||||
shared UnitTesting.ReturnsABC = () => "ABC";
|
||||
shared UnitTesting.Returns123 = () => "123";
|
||||
shared UnitTesting.ReturnsTableWithFiveRows = () => Table.Repeat(#table({"a"}, {{1}}), 5);
|
||||
@@ -0,0 +1,2 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.Get.ByUrl("https://latest.speckle.dev/streams/3d25474a18") in Record.ToTable(result)
|
||||
@@ -0,0 +1,2 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.GetByUrl("https://latest.speckle.dev/streams/5f284e5c70/objects/85e5f250fe591ea74d8d5dc1137a9341") in result
|
||||
@@ -0,0 +1,376 @@
|
||||
let
|
||||
Diagnostics.LogValue = (prefix, value) =>
|
||||
Diagnostics.Trace(
|
||||
TraceLevel.Information,
|
||||
prefix & ": " & (try Diagnostics.ValueToText(value) otherwise "<error getting value>"),
|
||||
value
|
||||
),
|
||||
Diagnostics.LogValue2 = (prefix, value, result, optional delayed) =>
|
||||
Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Diagnostics.ValueToText(value), result, delayed),
|
||||
Diagnostics.LogFailure = (text, function) =>
|
||||
let
|
||||
result = try function()
|
||||
in
|
||||
if result[HasError] then
|
||||
Diagnostics.LogValue2(text, result[Error], () => error result[Error], true)
|
||||
else
|
||||
result[Value],
|
||||
Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function =>
|
||||
Function.From(Value.Type(innerFunction), (list) => outerFunction(() => Function.Invoke(innerFunction, list))),
|
||||
Diagnostics.WrapHandlers = (handlers as record) as record =>
|
||||
Record.FromList(
|
||||
List.Transform(
|
||||
Record.FieldNames(handlers),
|
||||
(h) =>
|
||||
Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))
|
||||
),
|
||||
Record.FieldNames(handlers)
|
||||
),
|
||||
Diagnostics.ValueToText = (value) =>
|
||||
let
|
||||
_canBeIdentifier = (x) =>
|
||||
let
|
||||
keywords = {
|
||||
"and",
|
||||
"as",
|
||||
"each",
|
||||
"else",
|
||||
"error",
|
||||
"false",
|
||||
"if",
|
||||
"in",
|
||||
"is",
|
||||
"let",
|
||||
"meta",
|
||||
"not",
|
||||
"otherwise",
|
||||
"or",
|
||||
"section",
|
||||
"shared",
|
||||
"then",
|
||||
"true",
|
||||
"try",
|
||||
"type"
|
||||
},
|
||||
charAlpha = (c as number) => (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c = 95,
|
||||
charDigit = (c as number) => c >= 48 and c <= 57
|
||||
in
|
||||
try
|
||||
charAlpha(Character.ToNumber(Text.At(x, 0)))
|
||||
and List.MatchesAll(
|
||||
Text.ToList(x), (c) => let num = Character.ToNumber(c) in charAlpha(num)
|
||||
or charDigit(num)
|
||||
)
|
||||
and not List.MatchesAny(keywords, (li) => li = x) otherwise false,
|
||||
Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ",
|
||||
Serialize.Date = (x) =>
|
||||
"#date(" & Text.From(Date.Year(x)) & ", " & Text.From(Date.Month(x)) & ", " & Text.From(Date.Day(x))
|
||||
& ") ",
|
||||
Serialize.Datetime = (x) =>
|
||||
"#datetime("
|
||||
& Text.From(Date.Year(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Date.Month(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Date.Day(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Hour(DateTime.Time(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Minute(DateTime.Time(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Second(DateTime.Time(x)))
|
||||
& ") ",
|
||||
Serialize.Datetimezone = (x) =>
|
||||
let
|
||||
dtz = DateTimeZone.ToRecord(x)
|
||||
in
|
||||
"#datetimezone("
|
||||
& Text.From(dtz[Year])
|
||||
& ", "
|
||||
& Text.From(dtz[Month])
|
||||
& ", "
|
||||
& Text.From(dtz[Day])
|
||||
& ", "
|
||||
& Text.From(dtz[Hour])
|
||||
& ", "
|
||||
& Text.From(dtz[Minute])
|
||||
& ", "
|
||||
& Text.From(dtz[Second])
|
||||
& ", "
|
||||
& Text.From(dtz[ZoneHours])
|
||||
& ", "
|
||||
& Text.From(dtz[ZoneMinutes])
|
||||
& ") ",
|
||||
Serialize.Duration = (x) =>
|
||||
let
|
||||
dur = Duration.ToRecord(x)
|
||||
in
|
||||
"#duration("
|
||||
& Text.From(dur[Days])
|
||||
& ", "
|
||||
& Text.From(dur[Hours])
|
||||
& ", "
|
||||
& Text.From(dur[Minutes])
|
||||
& ", "
|
||||
& Text.From(dur[Seconds])
|
||||
& ") ",
|
||||
Serialize.Function = (x) =>
|
||||
_serialize_function_param_type(
|
||||
Type.FunctionParameters(Value.Type(x)), Type.FunctionRequiredParameters(Value.Type(x))
|
||||
)
|
||||
& " as "
|
||||
& _serialize_function_return_type(Value.Type(x))
|
||||
& " => (...) ",
|
||||
Serialize.List = (x) =>
|
||||
"{"
|
||||
& List.Accumulate(
|
||||
x, "", (seed, item) => if seed = "" then Serialize(item) else seed & ", " & Serialize(item)
|
||||
)
|
||||
& "} ",
|
||||
Serialize.Logical = (x) => Text.From(x),
|
||||
Serialize.Null = (x) => "null",
|
||||
Serialize.Number = (x) =>
|
||||
let
|
||||
Text.From = (i as number) as text =>
|
||||
if Number.IsNaN(i) then
|
||||
"#nan"
|
||||
else if i = Number.PositiveInfinity then
|
||||
"#infinity"
|
||||
else if i = Number.NegativeInfinity then
|
||||
"-#infinity"
|
||||
else
|
||||
Text.From(i)
|
||||
in
|
||||
Text.From(x),
|
||||
Serialize.Record = (x) =>
|
||||
"[ "
|
||||
& List.Accumulate(
|
||||
Record.FieldNames(x),
|
||||
"",
|
||||
(seed, item) =>
|
||||
(if seed = "" then Serialize.Identifier(item) else seed & ", " & Serialize.Identifier(
|
||||
item
|
||||
))
|
||||
& " = "
|
||||
& Serialize(Record.Field(x, item))
|
||||
)
|
||||
& " ] ",
|
||||
Serialize.Table = (x) =>
|
||||
"#table( type " & _serialize_table_type(Value.Type(x)) & ", " & Serialize(Table.ToRows(x)) & ") ",
|
||||
Serialize.Text = (x) => """" & _serialize_text_content(x) & """",
|
||||
_serialize_text_content = (x) =>
|
||||
let
|
||||
escapeText = (n as number) as text =>
|
||||
"#(#)(" & Text.PadStart(Number.ToText(n, "X", "en-US"), 4, "0") & ")"
|
||||
in
|
||||
List.Accumulate(
|
||||
List.Transform(
|
||||
Text.ToList(x),
|
||||
(c) =>
|
||||
let
|
||||
n = Character.ToNumber(c)
|
||||
in
|
||||
if n = 9 then
|
||||
"#(#)(tab)"
|
||||
else if n = 10 then
|
||||
"#(#)(lf)"
|
||||
else if n = 13 then
|
||||
"#(#)(cr)"
|
||||
else if n = 34 then
|
||||
""""""
|
||||
else if n = 35 then
|
||||
"#(#)(#)"
|
||||
else if n < 32 then
|
||||
escapeText(n)
|
||||
else if n < 127 then
|
||||
Character.FromNumber(n)
|
||||
else
|
||||
escapeText(n)
|
||||
),
|
||||
"",
|
||||
(s, i) => s & i
|
||||
),
|
||||
Serialize.Identifier = (x) => if _canBeIdentifier(x) then x else "#""" & _serialize_text_content(x) & """",
|
||||
Serialize.Time = (x) =>
|
||||
"#time("
|
||||
& Text.From(Time.Hour(x))
|
||||
& ", "
|
||||
& Text.From(Time.Minute(x))
|
||||
& ", "
|
||||
& Text.From(Time.Second(x))
|
||||
& ") ",
|
||||
Serialize.Type = (x) => "type " & _serialize_typename(x),
|
||||
_serialize_typename = (x, optional funtype as logical) =>
|
||||
/* Optional parameter: Is this being used as part of a function signature? */ let
|
||||
isFunctionType = (x as type) =>
|
||||
try if Type.FunctionReturn(x) is type then true else false otherwise false,
|
||||
isTableType = (x as type) =>
|
||||
try if Type.TableSchema(x) is table then true else false otherwise false,
|
||||
isRecordType = (x as type) =>
|
||||
try if Type.ClosedRecord(x) is type then true else false otherwise false,
|
||||
isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false
|
||||
in
|
||||
if funtype = null and isTableType(x) then
|
||||
_serialize_table_type(x)
|
||||
else if funtype = null and isListType(x) then
|
||||
"{ " & @_serialize_typename(Type.ListItem(x)) & " }"
|
||||
else if funtype = null and isFunctionType(x) then
|
||||
"function " & _serialize_function_type(x)
|
||||
else if funtype = null and isRecordType(x) then
|
||||
_serialize_record_type(x)
|
||||
else if x = type any then
|
||||
"any"
|
||||
else
|
||||
let
|
||||
base = Type.NonNullable(x)
|
||||
in
|
||||
(if Type.IsNullable(x) then "nullable " else "")
|
||||
& (
|
||||
if base = type anynonnull then
|
||||
"anynonnull"
|
||||
else if base = type binary then
|
||||
"binary"
|
||||
else if base = type date then
|
||||
"date"
|
||||
else if base = type datetime then
|
||||
"datetime"
|
||||
else if base = type datetimezone then
|
||||
"datetimezone"
|
||||
else if base = type duration then
|
||||
"duration"
|
||||
else if base = type logical then
|
||||
"logical"
|
||||
else if base = type none then
|
||||
"none"
|
||||
else if base = type null then
|
||||
"null"
|
||||
else if base = type number then
|
||||
"number"
|
||||
else if base = type text then
|
||||
"text"
|
||||
else if base = type time then
|
||||
"time"
|
||||
else if base = type type then
|
||||
"type"
|
||||
else /* Abstract types: */ if base = type function then
|
||||
"function"
|
||||
else if base = type table then
|
||||
"table"
|
||||
else if base = type record then
|
||||
"record"
|
||||
else if base = type list then
|
||||
"list"
|
||||
else
|
||||
"any /*Actually unknown type*/"
|
||||
),
|
||||
_serialize_table_type = (x) =>
|
||||
let
|
||||
schema = Type.TableSchema(x)
|
||||
in
|
||||
"table "
|
||||
& (
|
||||
if Table.IsEmpty(schema) then
|
||||
""
|
||||
else
|
||||
"["
|
||||
& List.Accumulate(
|
||||
List.Transform(
|
||||
Table.ToRecords(Table.Sort(schema, "Position")),
|
||||
each Serialize.Identifier(_[Name]) & " = " & _[Kind]
|
||||
),
|
||||
"",
|
||||
(seed, item) => (if seed = "" then item else seed & ", " & item)
|
||||
)
|
||||
& "] "
|
||||
),
|
||||
_serialize_record_type = (x) =>
|
||||
let
|
||||
flds = Type.RecordFields(x)
|
||||
in
|
||||
if Record.FieldCount(flds) = 0 then
|
||||
"record"
|
||||
else
|
||||
"["
|
||||
& List.Accumulate(
|
||||
Record.FieldNames(flds),
|
||||
"",
|
||||
(seed, item) =>
|
||||
seed
|
||||
& (if seed <> "" then ", " else "")
|
||||
& (
|
||||
Serialize.Identifier(item)
|
||||
& "="
|
||||
& _serialize_typename(Record.Field(flds, item)[Type])
|
||||
)
|
||||
)
|
||||
& (if Type.IsOpenRecord(x) then ",..." else "")
|
||||
& "]",
|
||||
_serialize_function_type = (x) =>
|
||||
_serialize_function_param_type(Type.FunctionParameters(x), Type.FunctionRequiredParameters(x))
|
||||
& " as "
|
||||
& _serialize_function_return_type(x),
|
||||
_serialize_function_param_type = (t, n) =>
|
||||
let
|
||||
funsig = Table.ToRecords(
|
||||
Table.TransformColumns(
|
||||
Table.AddIndexColumn(Record.ToTable(t), "isOptional", 1), {"isOptional", (x) => x > n}
|
||||
)
|
||||
)
|
||||
in
|
||||
"("
|
||||
& List.Accumulate(
|
||||
funsig,
|
||||
"",
|
||||
(seed, item) =>
|
||||
(if seed = "" then "" else seed & ", ")
|
||||
& (if item[isOptional] then "optional " else "")
|
||||
& Serialize.Identifier(item[Name])
|
||||
& " as "
|
||||
& _serialize_typename(item[Value], true)
|
||||
)
|
||||
& ")",
|
||||
_serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true),
|
||||
Serialize = (x) as text =>
|
||||
if x is binary then
|
||||
try Serialize.Binary(x) otherwise "null /*serialize failed*/"
|
||||
else if x is date then
|
||||
try Serialize.Date(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetime then
|
||||
try Serialize.Datetime(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetimezone then
|
||||
try Serialize.Datetimezone(x) otherwise "null /*serialize failed*/"
|
||||
else if x is duration then
|
||||
try Serialize.Duration(x) otherwise "null /*serialize failed*/"
|
||||
else if x is function then
|
||||
try Serialize.Function(x) otherwise "null /*serialize failed*/"
|
||||
else if x is list then
|
||||
try Serialize.List(x) otherwise "null /*serialize failed*/"
|
||||
else if x is logical then
|
||||
try Serialize.Logical(x) otherwise "null /*serialize failed*/"
|
||||
else if x is null then
|
||||
try Serialize.Null(x) otherwise "null /*serialize failed*/"
|
||||
else if x is number then
|
||||
try Serialize.Number(x) otherwise "null /*serialize failed*/"
|
||||
else if x is record then
|
||||
try Serialize.Record(x) otherwise "null /*serialize failed*/"
|
||||
else if x is table then
|
||||
try Serialize.Table(x) otherwise "null /*serialize failed*/"
|
||||
else if x is text then
|
||||
try Serialize.Text(x) otherwise "null /*serialize failed*/"
|
||||
else if x is time then
|
||||
try Serialize.Time(x) otherwise "null /*serialize failed*/"
|
||||
else if x is type then
|
||||
try Serialize.Type(x) otherwise "null /*serialize failed*/"
|
||||
else
|
||||
"[#_unable_to_serialize_#]"
|
||||
in
|
||||
try Serialize(value) otherwise "<serialization failed>"
|
||||
in
|
||||
[
|
||||
LogValue = Diagnostics.LogValue,
|
||||
LogValue2 = Diagnostics.LogValue2,
|
||||
LogFailure = Diagnostics.LogFailure,
|
||||
WrapFunctionResult = Diagnostics.WrapFunctionResult,
|
||||
WrapHandlers = Diagnostics.WrapHandlers,
|
||||
ValueToText = Diagnostics.ValueToText
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
// This is here as reference for copy/pasting wherever there is need for importing pqm files.
|
||||
let
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Extension.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
]
|
||||
in
|
||||
Extension.LoadFunction
|
||||
@@ -0,0 +1,231 @@
|
||||
let
|
||||
/// COMMON UNIT TESTING CODE
|
||||
Fact = (_subject as text, _expected, _actual) as record =>
|
||||
[
|
||||
expected = try _expected,
|
||||
safeExpected = if expected[HasError] then "Expected : " & @ValueToText(expected[Error]) else expected[
|
||||
Value
|
||||
],
|
||||
actual = try _actual,
|
||||
safeActual = if actual[HasError] then "Actual : " & @ValueToText(actual[Error]) else actual[Value],
|
||||
attempt = try safeExpected = safeActual,
|
||||
result = if attempt[HasError] or not attempt[Value] then "Failure" else "Success",
|
||||
resultOp = if result = "Success" then " = " else " <> ",
|
||||
addendumEvalAttempt = if attempt[HasError] then @ValueToText(attempt[Error]) else "",
|
||||
addendumEvalExpected = try @ValueToText(safeExpected) otherwise "...",
|
||||
addendumEvalActual = try @ValueToText(safeActual) otherwise "...",
|
||||
fact = [
|
||||
Result = result & " " & addendumEvalAttempt,
|
||||
Notes = _subject,
|
||||
Details = " (" & addendumEvalExpected & resultOp & addendumEvalActual & ")"
|
||||
]
|
||||
][fact],
|
||||
Facts = (_subject as text, _predicates as list) => List.Transform(_predicates, each Fact(_subject, _{0}, _{1})),
|
||||
Facts.Summarize = (_facts as list) as table =>
|
||||
[
|
||||
Fact.CountSuccesses = (count, i) =>
|
||||
[
|
||||
result = try i[Result],
|
||||
sum = if result[HasError] or not Text.StartsWith(result[Value], "Success") then count else count + 1
|
||||
][sum],
|
||||
passed = List.Accumulate(_facts, 0, Fact.CountSuccesses),
|
||||
total = List.Count(_facts),
|
||||
format = if passed = total then "All #{0} Passed !!!" else "#{0} Passed - #{1} Failed",
|
||||
result = if passed = total then "Success" else "Failed",
|
||||
rate = Number.IntegerDivide(100 * passed, total),
|
||||
header = [
|
||||
Result = result,
|
||||
Notes = Text.Format(format, {passed, total - passed}),
|
||||
Details = Text.Format("#{0}% success rate", {rate})
|
||||
],
|
||||
report = Table.FromRecords(List.Combine({{header}, _facts}))
|
||||
][report],
|
||||
ValueToText = (value, optional depth) =>
|
||||
let
|
||||
List.TransformAndCombine = (list, transform, separator) =>
|
||||
Text.Combine(List.Transform(list, transform), separator),
|
||||
Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ",
|
||||
Serialize.Function = (x) =>
|
||||
_serialize_function_param_type(
|
||||
Type.FunctionParameters(Value.Type(x)), Type.FunctionRequiredParameters(Value.Type(x))
|
||||
)
|
||||
& " as "
|
||||
& _serialize_function_return_type(Value.Type(x))
|
||||
& " => (...) ",
|
||||
Serialize.List = (x) => "{" & List.TransformAndCombine(x, Serialize, ", ") & "} ",
|
||||
Serialize.Record = (x) =>
|
||||
"[ "
|
||||
& List.TransformAndCombine(
|
||||
Record.FieldNames(x),
|
||||
(item) => Serialize.Identifier(item) & " = " & Serialize(Record.Field(x, item)),
|
||||
", "
|
||||
)
|
||||
& " ] ",
|
||||
Serialize.Table = (x) =>
|
||||
"#table( type " & _serialize_table_type(Value.Type(x)) & ", " & Serialize(Table.ToRows(x)) & ") ",
|
||||
Serialize.Identifier = Expression.Identifier,
|
||||
Serialize.Type = (x) => "type " & _serialize_typename(x),
|
||||
_serialize_typename = (x, optional funtype as logical) =>
|
||||
/* Optional parameter: Is this being used as part of a function signature? */ let
|
||||
isFunctionType = (x as type) =>
|
||||
try if Type.FunctionReturn(x) is type then true else false otherwise false,
|
||||
isTableType = (x as type) =>
|
||||
try if Type.TableSchema(x) is table then true else false otherwise false,
|
||||
isRecordType = (x as type) =>
|
||||
try if Type.ClosedRecord(x) is type then true else false otherwise false,
|
||||
isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false
|
||||
in
|
||||
if funtype = null and isTableType(x) then
|
||||
_serialize_table_type(x)
|
||||
else if funtype = null and isListType(x) then
|
||||
"{ " & @_serialize_typename(Type.ListItem(x)) & " }"
|
||||
else if funtype = null and isFunctionType(x) then
|
||||
"function " & _serialize_function_type(x)
|
||||
else if funtype = null and isRecordType(x) then
|
||||
_serialize_record_type(x)
|
||||
else if x = type any then
|
||||
"any"
|
||||
else
|
||||
let
|
||||
base = Type.NonNullable(x)
|
||||
in
|
||||
(if Type.IsNullable(x) then "nullable " else "")
|
||||
& (
|
||||
if base = type anynonnull then
|
||||
"anynonnull"
|
||||
else if base = type binary then
|
||||
"binary"
|
||||
else if base = type date then
|
||||
"date"
|
||||
else if base = type datetime then
|
||||
"datetime"
|
||||
else if base = type datetimezone then
|
||||
"datetimezone"
|
||||
else if base = type duration then
|
||||
"duration"
|
||||
else if base = type logical then
|
||||
"logical"
|
||||
else if base = type none then
|
||||
"none"
|
||||
else if base = type null then
|
||||
"null"
|
||||
else if base = type number then
|
||||
"number"
|
||||
else if base = type text then
|
||||
"text"
|
||||
else if base = type time then
|
||||
"time"
|
||||
else if base = type type then
|
||||
"type"
|
||||
else /* Abstract types: */ if base = type function then
|
||||
"function"
|
||||
else if base = type table then
|
||||
"table"
|
||||
else if base = type record then
|
||||
"record"
|
||||
else if base = type list then
|
||||
"list"
|
||||
else
|
||||
"any /*Actually unknown type*/"
|
||||
),
|
||||
_serialize_table_type = (x) =>
|
||||
let
|
||||
schema = Type.TableSchema(x)
|
||||
in
|
||||
"table "
|
||||
& (
|
||||
if Table.IsEmpty(schema) then
|
||||
""
|
||||
else
|
||||
"["
|
||||
& List.TransformAndCombine(
|
||||
Table.ToRecords(Table.Sort(schema, "Position")),
|
||||
each Serialize.Identifier(_[Name]) & " = " & _[Kind],
|
||||
", "
|
||||
)
|
||||
& "] "
|
||||
),
|
||||
_serialize_record_type = (x) =>
|
||||
let
|
||||
flds = Type.RecordFields(x)
|
||||
in
|
||||
if Record.FieldCount(flds) = 0 then
|
||||
"record"
|
||||
else
|
||||
"["
|
||||
& List.TransformAndCombine(
|
||||
Record.FieldNames(flds),
|
||||
(item) =>
|
||||
Serialize.Identifier(item)
|
||||
& "="
|
||||
& _serialize_typename(Record.Field(flds, item)[Type]),
|
||||
", "
|
||||
)
|
||||
& (if Type.IsOpenRecord(x) then ", ..." else "")
|
||||
& "]",
|
||||
_serialize_function_type = (x) =>
|
||||
_serialize_function_param_type(Type.FunctionParameters(x), Type.FunctionRequiredParameters(x))
|
||||
& " as "
|
||||
& _serialize_function_return_type(x),
|
||||
_serialize_function_param_type = (t, n) =>
|
||||
let
|
||||
funsig = Table.ToRecords(
|
||||
Table.TransformColumns(
|
||||
Table.AddIndexColumn(Record.ToTable(t), "isOptional", 1), {"isOptional", (x) => x > n}
|
||||
)
|
||||
)
|
||||
in
|
||||
"("
|
||||
& List.TransformAndCombine(
|
||||
funsig,
|
||||
(item) =>
|
||||
(if item[isOptional] then "optional " else "")
|
||||
& Serialize.Identifier(item[Name])
|
||||
& " as "
|
||||
& _serialize_typename(item[Value], true),
|
||||
", "
|
||||
)
|
||||
& ")",
|
||||
_serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true),
|
||||
Serialize = (x) as text =>
|
||||
if x is binary then
|
||||
try Serialize.Binary(x) otherwise "null /*serialize failed*/"
|
||||
else if x is date then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetime then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetimezone then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is duration then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is function then
|
||||
try Serialize.Function(x) otherwise "null /*serialize failed*/"
|
||||
else if x is list then
|
||||
try Serialize.List(x) otherwise "null /*serialize failed*/"
|
||||
else if x is logical then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is null then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is number then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is record then
|
||||
try Serialize.Record(x) otherwise "null /*serialize failed*/"
|
||||
else if x is table then
|
||||
try Serialize.Table(x) otherwise "null /*serialize failed*/"
|
||||
else if x is text then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is time then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is type then
|
||||
try Serialize.Type(x) otherwise "null /*serialize failed*/"
|
||||
else
|
||||
"[#_unable_to_serialize_#]"
|
||||
in
|
||||
try Serialize(value) otherwise "<serialization failed>"
|
||||
in
|
||||
[
|
||||
Fact = Fact,
|
||||
Facts = Facts,
|
||||
SummarizeFacts = Facts.Summarize,
|
||||
ValueToText = ValueToText
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
(Value as text) =>
|
||||
let
|
||||
Solution = Binary.ToText(
|
||||
Binary.FromList(
|
||||
Binary.ToList(Binary.Compress(Text.ToBinary(Value, BinaryEncoding.Base64), Compression.GZip))
|
||||
)
|
||||
)
|
||||
in
|
||||
if Value = null then
|
||||
null
|
||||
else
|
||||
Solution
|
||||
@@ -0,0 +1,23 @@
|
||||
(getNextPage as function) as table =>
|
||||
let
|
||||
listOfPages = List.Generate(
|
||||
() => getNextPage(null),
|
||||
// get the first page of data
|
||||
(lastPage) => lastPage <> null,
|
||||
// stop when the function returns null
|
||||
(lastPage) => getNextPage(lastPage)
|
||||
// pass the previous page to the next function call
|
||||
),
|
||||
// concatenate the pages together
|
||||
tableOfPages = Table.FromList(listOfPages, Splitter.SplitByNothing(), {"Column1"}),
|
||||
firstRow = tableOfPages{0} ?
|
||||
in
|
||||
// if we didn't get back any pages of data, return an empty table
|
||||
// otherwise set the table type based on the columns of the first page
|
||||
if (firstRow = null) then
|
||||
Table.FromRows({})
|
||||
else
|
||||
Value.ReplaceType(
|
||||
Table.ExpandTableColumn(tableOfPages, "Column1", Table.ColumnNames(firstRow[Column1])),
|
||||
Value.Type(firstRow[Column1])
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
(
|
||||
table as table,
|
||||
keyColumns as list,
|
||||
nameColumn as text,
|
||||
dataColumn as text,
|
||||
itemKindColumn as text,
|
||||
itemNameColumn as text,
|
||||
isLeafColumn as text
|
||||
) as table =>
|
||||
let
|
||||
tableType = Value.Type(table),
|
||||
newTableType = Type.AddTableKey(tableType, keyColumns, true) meta [
|
||||
NavigationTable.NameColumn = nameColumn,
|
||||
NavigationTable.DataColumn = dataColumn,
|
||||
NavigationTable.ItemKindColumn = itemKindColumn,
|
||||
Preview.DelayColumn = itemNameColumn,
|
||||
NavigationTable.IsLeafColumn = isLeafColumn
|
||||
],
|
||||
navigationTable = Value.ReplaceType(table, newTableType)
|
||||
in
|
||||
navigationTable
|
||||
@@ -0,0 +1,14 @@
|
||||
(producer as function, interval as function, optional count as number) as any =>
|
||||
let
|
||||
list = List.Generate(
|
||||
() => {0, null},
|
||||
(state) => state{0} <> null and (count = null or state{0} < count),
|
||||
(state) =>
|
||||
if state{1} <> null then
|
||||
{null, state{1}}
|
||||
else
|
||||
{1 + state{0}, Function.InvokeAfter(() => producer(state{0}), interval(state{0}))},
|
||||
(state) => state{1}
|
||||
)
|
||||
in
|
||||
List.Last(list)
|
||||