├── .gitattributes ├── .gitignore ├── Ignite2018 ├── README.md ├── WWIDW-Sales.pbix ├── WWIDW-SalesDQ.pbix └── WideWorldImporters │ ├── Diagnostics.pqm │ ├── OdbcConstants.pqm │ ├── README.md │ ├── WideWorldImporters.mproj │ ├── WideWorldImporters.pq │ ├── WideWorldImporters.query.pq │ ├── WideWorldImporters16.png │ ├── WideWorldImporters20.png │ ├── WideWorldImporters24.png │ ├── WideWorldImporters32.png │ ├── WideWorldImporters40.png │ ├── WideWorldImporters48.png │ ├── WideWorldImporters64.png │ ├── WideWorldImporters80.png │ └── resources.resx ├── LICENSE ├── MBAS2019 ├── Fireball │ ├── Fireball.mproj │ ├── Fireball.pq │ ├── Fireball.query.pq │ ├── Fireball16.png │ ├── Fireball20.png │ ├── Fireball24.png │ ├── Fireball32.png │ ├── Fireball40.png │ ├── Fireball48.png │ ├── Fireball64.png │ ├── Fireball80.png │ └── resources.resx ├── FireballComplete │ ├── Fireball.mproj │ ├── Fireball.pq │ ├── Fireball.query.pq │ ├── Fireball16.png │ ├── Fireball20.png │ ├── Fireball24.png │ ├── Fireball32.png │ ├── Fireball40.png │ ├── Fireball48.png │ ├── Fireball64.png │ ├── Fireball80.png │ └── resources.resx └── TripPin │ ├── TripPin.mproj │ ├── TripPin.pq │ ├── TripPin.query.pq │ ├── TripPin16.png │ ├── TripPin20.png │ ├── TripPin24.png │ ├── TripPin32.png │ ├── TripPin40.png │ ├── TripPin48.png │ ├── TripPin64.png │ ├── TripPin80.png │ └── resources.resx ├── NASA ├── Diagnostics.pqm ├── NASA.mproj ├── NASA.pq ├── NASA.query.pq ├── NASA16.png ├── NASA20.png ├── NASA24.png ├── NASA32.png ├── NASA40.png ├── NASA48.png ├── NASA64.png ├── NASA80.png ├── README.md ├── Table.ChangeType.pqm ├── Table.ToNavigationTable.pqm ├── apikey └── resources.resx ├── Phoenix2018 └── README.md ├── README.md └── Steam ├── Steam.mproj ├── Steam.pq ├── Steam.query.pq ├── Steam16.png ├── Steam20.png ├── Steam24.png ├── Steam32.png ├── Steam40.png ├── Steam48.png ├── Steam64.png ├── Steam80.png ├── apikey └── resources.resx /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | *.sln 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | *.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | paket-files/ 247 | 248 | # FAKE - F# Make 249 | .fake/ 250 | 251 | # JetBrains Rider 252 | .idea/ 253 | *.sln.iml 254 | -------------------------------------------------------------------------------- /Ignite2018/README.md: -------------------------------------------------------------------------------- 1 | # Custom Connector Sample from Microsoft Ignite 2018 2 | 3 | This custom connector was created for the [Microsoft Ignite 2018](https://channel9.msdn.com/Events/Ignite/2018) event. It demonstrates how a custom connector can be used to provide an analyst friendly connector with custom branding for an internal data source. 4 | 5 | ## Environment requirements 6 | 7 | - Local SQL Server instance 8 | - [WideWorldImportersDW](https://github.com/Microsoft/sql-server-samples/releases/tag/wide-world-importers-v1.0) 9 | - SQL Server Native Client ODBC 11.0 10 | 11 | The custom connector assumes the `WideWorldImportersDW` database is available on `localhost` sql server instance. To change these settings, update the hard coded values in 12 | the .pq connector file. 13 | 14 | Icon adapted from: https://commons.wikimedia.org/wiki/File:Blue_globe_icon.svg -------------------------------------------------------------------------------- /Ignite2018/WWIDW-Sales.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WWIDW-Sales.pbix -------------------------------------------------------------------------------- /Ignite2018/WWIDW-SalesDQ.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WWIDW-SalesDQ.pbix -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/Diagnostics.pqm: -------------------------------------------------------------------------------- 1 | let 2 | Diagnostics.LogValue = (prefix, value) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & (try Diagnostics.ValueToText(value) otherwise ""), value), 3 | Diagnostics.LogValue2 = (prefix, value, result, optional delayed) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Diagnostics.ValueToText(value), result, delayed), 4 | Diagnostics.LogFailure = (text, function) => 5 | let 6 | result = try function() 7 | in 8 | if result[HasError] then Diagnostics.LogValue2(text, result[Error], () => error result[Error], true) else result[Value], 9 | 10 | Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function => 11 | Function.From(Value.Type(innerFunction), (list) => outerFunction(() => Function.Invoke(innerFunction, list))), 12 | 13 | Diagnostics.WrapHandlers = (handlers as record) as record => 14 | Record.FromList( 15 | List.Transform( 16 | Record.FieldNames(handlers), 17 | (h) => Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))), 18 | Record.FieldNames(handlers)), 19 | 20 | Diagnostics.ValueToText = (value) => 21 | let 22 | _canBeIdentifier = (x) => 23 | let 24 | keywords = {"and", "as", "each", "else", "error", "false", "if", "in", "is", "let", "meta", "not", "otherwise", "or", "section", "shared", "then", "true", "try", "type" }, 25 | charAlpha = (c as number) => (c>= 65 and c <= 90) or (c>= 97 and c <= 122) or c=95, 26 | charDigit = (c as number) => c>= 48 and c <= 57 27 | in 28 | try 29 | charAlpha(Character.ToNumber(Text.At(x,0))) 30 | and 31 | List.MatchesAll( 32 | Text.ToList(x), 33 | (c)=> let num = Character.ToNumber(c) in charAlpha(num) or charDigit(num) 34 | ) 35 | and not 36 | List.MatchesAny( keywords, (li)=> li=x ) 37 | otherwise 38 | false, 39 | 40 | Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ", 41 | 42 | Serialize.Date = (x) => "#date(" & 43 | Text.From(Date.Year(x)) & ", " & 44 | Text.From(Date.Month(x)) & ", " & 45 | Text.From(Date.Day(x)) & ") ", 46 | 47 | Serialize.Datetime = (x) => "#datetime(" & 48 | Text.From(Date.Year(DateTime.Date(x))) & ", " & 49 | Text.From(Date.Month(DateTime.Date(x))) & ", " & 50 | Text.From(Date.Day(DateTime.Date(x))) & ", " & 51 | Text.From(Time.Hour(DateTime.Time(x))) & ", " & 52 | Text.From(Time.Minute(DateTime.Time(x))) & ", " & 53 | Text.From(Time.Second(DateTime.Time(x))) & ") ", 54 | 55 | Serialize.Datetimezone =(x) => let 56 | dtz = DateTimeZone.ToRecord(x) 57 | in 58 | "#datetimezone(" & 59 | Text.From(dtz[Year]) & ", " & 60 | Text.From(dtz[Month]) & ", " & 61 | Text.From(dtz[Day]) & ", " & 62 | Text.From(dtz[Hour]) & ", " & 63 | Text.From(dtz[Minute]) & ", " & 64 | Text.From(dtz[Second]) & ", " & 65 | Text.From(dtz[ZoneHours]) & ", " & 66 | Text.From(dtz[ZoneMinutes]) & ") ", 67 | 68 | Serialize.Duration = (x) => let 69 | dur = Duration.ToRecord(x) 70 | in 71 | "#duration(" & 72 | Text.From(dur[Days]) & ", " & 73 | Text.From(dur[Hours]) & ", " & 74 | Text.From(dur[Minutes]) & ", " & 75 | Text.From(dur[Seconds]) & ") ", 76 | 77 | Serialize.Function = (x) => _serialize_function_param_type( 78 | Type.FunctionParameters(Value.Type(x)), 79 | Type.FunctionRequiredParameters(Value.Type(x)) ) & 80 | " as " & 81 | _serialize_function_return_type(Value.Type(x)) & 82 | " => (...) ", 83 | 84 | Serialize.List = (x) => "{" & 85 | List.Accumulate(x, "", (seed,item) => if seed="" then Serialize(item) else seed & ", " & Serialize(item)) & 86 | "} ", 87 | 88 | Serialize.Logical = (x) => Text.From(x), 89 | 90 | Serialize.Null = (x) => "null", 91 | 92 | Serialize.Number = (x) => 93 | let Text.From = (i as number) as text => 94 | if Number.IsNaN(i) then "#nan" else 95 | if i=Number.PositiveInfinity then "#infinity" else 96 | if i=Number.NegativeInfinity then "-#infinity" else 97 | Text.From(i) 98 | in 99 | Text.From(x), 100 | 101 | Serialize.Record = (x) => "[ " & 102 | List.Accumulate( 103 | Record.FieldNames(x), 104 | "", 105 | (seed,item) => 106 | (if seed="" then Serialize.Identifier(item) else seed & ", " & Serialize.Identifier(item)) & " = " & Serialize(Record.Field(x, item)) 107 | ) & 108 | " ] ", 109 | 110 | Serialize.Table = (x) => "#table( type " & 111 | _serialize_table_type(Value.Type(x)) & 112 | ", " & 113 | Serialize(Table.ToRows(x)) & 114 | ") ", 115 | 116 | Serialize.Text = (x) => """" & 117 | _serialize_text_content(x) & 118 | """", 119 | 120 | _serialize_text_content = (x) => let 121 | escapeText = (n as number) as text => "#(#)(" & Text.PadStart(Number.ToText(n, "X", "en-US"), 4, "0") & ")" 122 | in 123 | List.Accumulate( 124 | List.Transform( 125 | Text.ToList(x), 126 | (c) => let n=Character.ToNumber(c) in 127 | if n = 9 then "#(#)(tab)" else 128 | if n = 10 then "#(#)(lf)" else 129 | if n = 13 then "#(#)(cr)" else 130 | if n = 34 then """""" else 131 | if n = 35 then "#(#)(#)" else 132 | if n < 32 then escapeText(n) else 133 | if n < 127 then Character.FromNumber(n) else 134 | escapeText(n) 135 | ), 136 | "", 137 | (s,i)=>s&i 138 | ), 139 | 140 | Serialize.Identifier = (x) => 141 | if _canBeIdentifier(x) then 142 | x 143 | else 144 | "#""" & 145 | _serialize_text_content(x) & 146 | """", 147 | 148 | Serialize.Time = (x) => "#time(" & 149 | Text.From(Time.Hour(x)) & ", " & 150 | Text.From(Time.Minute(x)) & ", " & 151 | Text.From(Time.Second(x)) & ") ", 152 | 153 | Serialize.Type = (x) => "type " & _serialize_typename(x), 154 | 155 | 156 | _serialize_typename = (x, optional funtype as logical) => /* Optional parameter: Is this being used as part of a function signature? */ 157 | let 158 | isFunctionType = (x as type) => try if Type.FunctionReturn(x) is type then true else false otherwise false, 159 | isTableType = (x as type) => try if Type.TableSchema(x) is table then true else false otherwise false, 160 | isRecordType = (x as type) => try if Type.ClosedRecord(x) is type then true else false otherwise false, 161 | isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false 162 | in 163 | 164 | if funtype=null and isTableType(x) then _serialize_table_type(x) else 165 | if funtype=null and isListType(x) then "{ " & @_serialize_typename( Type.ListItem(x) ) & " }" else 166 | if funtype=null and isFunctionType(x) then "function " & _serialize_function_type(x) else 167 | if funtype=null and isRecordType(x) then _serialize_record_type(x) else 168 | 169 | if x = type any then "any" else 170 | let base = Type.NonNullable(x) in 171 | (if Type.IsNullable(x) then "nullable " else "") & 172 | (if base = type anynonnull then "anynonnull" else 173 | if base = type binary then "binary" else 174 | if base = type date then "date" else 175 | if base = type datetime then "datetime" else 176 | if base = type datetimezone then "datetimezone" else 177 | if base = type duration then "duration" else 178 | if base = type logical then "logical" else 179 | if base = type none then "none" else 180 | if base = type null then "null" else 181 | if base = type number then "number" else 182 | if base = type text then "text" else 183 | if base = type time then "time" else 184 | if base = type type then "type" else 185 | 186 | /* Abstract types: */ 187 | if base = type function then "function" else 188 | if base = type table then "table" else 189 | if base = type record then "record" else 190 | if base = type list then "list" else 191 | 192 | "any /*Actually unknown type*/"), 193 | 194 | _serialize_table_type = (x) => 195 | let 196 | schema = Type.TableSchema(x) 197 | in 198 | "table " & 199 | (if Table.IsEmpty(schema) then "" else 200 | "[" & List.Accumulate( 201 | List.Transform( 202 | Table.ToRecords(Table.Sort(schema,"Position")), 203 | each Serialize.Identifier(_[Name]) & " = " & _[Kind]), 204 | "", 205 | (seed,item) => (if seed="" then item else seed & ", " & item ) 206 | ) & "] " ), 207 | 208 | _serialize_record_type = (x) => 209 | let flds = Type.RecordFields(x) 210 | in 211 | if Record.FieldCount(flds)=0 then "record" else 212 | "[" & List.Accumulate( 213 | Record.FieldNames(flds), 214 | "", 215 | (seed,item) => 216 | seed & 217 | (if seed<>"" then ", " else "") & 218 | (Serialize.Identifier(item) & "=" & _serialize_typename(Record.Field(flds,item)[Type]) ) 219 | ) & 220 | (if Type.IsOpenRecord(x) then ",..." else "") & 221 | "]", 222 | 223 | _serialize_function_type = (x) => _serialize_function_param_type( 224 | Type.FunctionParameters(x), 225 | Type.FunctionRequiredParameters(x) ) & 226 | " as " & 227 | _serialize_function_return_type(x), 228 | 229 | _serialize_function_param_type = (t,n) => 230 | let 231 | funsig = Table.ToRecords( 232 | Table.TransformColumns( 233 | Table.AddIndexColumn( Record.ToTable( t ), "isOptional", 1 ), 234 | { "isOptional", (x)=> x>n } ) ) 235 | in 236 | "(" & 237 | List.Accumulate( 238 | funsig, 239 | "", 240 | (seed,item)=> 241 | (if seed="" then "" else seed & ", ") & 242 | (if item[isOptional] then "optional " else "") & 243 | Serialize.Identifier(item[Name]) & " as " & _serialize_typename(item[Value], true) ) 244 | & ")", 245 | 246 | _serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true), 247 | 248 | Serialize = (x) as text => 249 | if x is binary then try Serialize.Binary(x) otherwise "null /*serialize failed*/" else 250 | if x is date then try Serialize.Date(x) otherwise "null /*serialize failed*/" else 251 | if x is datetime then try Serialize.Datetime(x) otherwise "null /*serialize failed*/" else 252 | if x is datetimezone then try Serialize.Datetimezone(x) otherwise "null /*serialize failed*/" else 253 | if x is duration then try Serialize.Duration(x) otherwise "null /*serialize failed*/" else 254 | if x is function then try Serialize.Function(x) otherwise "null /*serialize failed*/" else 255 | if x is list then try Serialize.List(x) otherwise "null /*serialize failed*/" else 256 | if x is logical then try Serialize.Logical(x) otherwise "null /*serialize failed*/" else 257 | if x is null then try Serialize.Null(x) otherwise "null /*serialize failed*/" else 258 | if x is number then try Serialize.Number(x) otherwise "null /*serialize failed*/" else 259 | if x is record then try Serialize.Record(x) otherwise "null /*serialize failed*/" else 260 | if x is table then try Serialize.Table(x) otherwise "null /*serialize failed*/" else 261 | if x is text then try Serialize.Text(x) otherwise "null /*serialize failed*/" else 262 | if x is time then try Serialize.Time(x) otherwise "null /*serialize failed*/" else 263 | if x is type then try Serialize.Type(x) otherwise "null /*serialize failed*/" else 264 | "[#_unable_to_serialize_#]" 265 | in 266 | try Serialize(value) otherwise "" 267 | in 268 | [ 269 | LogValue = Diagnostics.LogValue, 270 | LogValue2 = Diagnostics.LogValue2, 271 | LogFailure = Diagnostics.LogFailure, 272 | WrapFunctionResult = Diagnostics.WrapFunctionResult, 273 | WrapHandlers = Diagnostics.WrapHandlers, 274 | ValueToText = Diagnostics.ValueToText 275 | ] -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/README.md: -------------------------------------------------------------------------------- 1 | # Custom Connector Sample from Microsoft Ignite 2018 2 | 3 | This custom connector was created for the [Microsoft Ignite 2018](https://channel9.msdn.com/Events/Ignite/2018) event. It demonstrates how a custom connector can be used to provide an analyst friendly connector with custom branding for an internal data source. 4 | 5 | ## Environment requirements 6 | 7 | - Local SQL Server instance 8 | - [WideWorldImportersDW](https://github.com/Microsoft/sql-server-samples/releases/tag/wide-world-importers-v1.0) 9 | - SQL Server Native Client ODBC 11.0 10 | 11 | The custom connector assumes the `WideWorldImportersDW` database is available on `localhost` sql server instance. To change these settings, update the hard coded values in 12 | the .pq connector file. 13 | 14 | Icon adapted from: https://commons.wikimedia.org/wiki/File:Blue_globe_icon.svg 15 | Power BI report adapted from: https://github.com/Microsoft/sql-server-samples/tree/master/samples/databases/wide-world-importers/power-bi-dashboards -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | WideWorldImporters 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Content 72 | 73 | 74 | Content 75 | 76 | 77 | Code 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters.pq: -------------------------------------------------------------------------------- 1 | // Adapted from the SqlODBC sample: https://github.com/Microsoft/DataConnectors/tree/master/samples/ODBC/SqlODBC 2 | section WideWorldImporters; 3 | 4 | EnableTraceOutput = false; 5 | 6 | /**************************** 7 | * ODBC Driver Configuration 8 | ****************************/ 9 | 10 | Config_DriverName = "SQL Server Native Client 11.0"; 11 | Config_SqlConformance = ODBC[SQL_SC][SQL_SC_SQL92_FULL]; // null, 1, 2, 4, 8 12 | Config_UseParameterBindings = false; // true, false, null 13 | Config_StringLiterateEscapeCharacters = { "\" }; // ex. { "\" } 14 | Config_UseCastInsteadOfConvert = null; // true, false, null 15 | Config_SupportsTop = true; // true, false 16 | Config_EnableDirectQuery = true; // true, false 17 | 18 | [DataSource.Kind="WideWorldImporters", Publish="WideWorldImporters.Publish"] 19 | shared WideWorldImporters.Contents = () => FormatNavTable(OdbcImpl()); 20 | 21 | // Creates a friendlier view of the default ODBC nav table. 22 | // Note that adding/removing columns will remove 23 | // the nav table metadata (since it changes the table type), 24 | // adding/removing rows will not impact its nav tableness. 25 | FormatNavTable = (root as table) as table => 26 | let 27 | database = root{[Name="WideWorldImportersDW"]}[Data], 28 | // we only want tables in the Dimension and Fact schemas 29 | filtered = Table.SelectRows(database, each [Name] = "Dimension" or [Name] = "Fact") 30 | in 31 | filtered; 32 | 33 | // returns the result of the call to Odbc.DataSource 34 | OdbcImpl = () => 35 | let 36 | ConnectionString = [ 37 | Driver = Config_DriverName, 38 | Server = "localhost", 39 | Database = "WideWorldImportersDW", 40 | ApplicationIntent = "readonly" 41 | ], 42 | 43 | Credential = Extension.CurrentCredential(), 44 | CredentialConnectionString = 45 | if Credential[AuthenticationKind]? = "UsernamePassword" then 46 | // set connection string parameters used for basic authentication 47 | [ UID = Credential[Username], PWD = Credential[Password] ] 48 | else if (Credential[AuthenticationKind]? = "Windows") then 49 | // set connection string parameters used for windows/kerberos authentication 50 | [ Trusted_Connection="Yes" ] 51 | else 52 | error Error.Record("Error", "Unhandled authentication kind: " & Credential[AuthenticationKind]?), 53 | 54 | defaultConfig = BuildOdbcConfig(), 55 | 56 | options = [ 57 | HierarchicalNavigation = true, 58 | HideNativeQuery = true, 59 | SoftNumbers = true, 60 | TolerateConcatOverflow = true, 61 | ClientConnectionPooling = true, 62 | 63 | SqlCapabilities = defaultConfig[SqlCapabilities] & [ 64 | FractionalSecondsScale = 3 65 | ], 66 | 67 | SQLGetInfo = defaultConfig[SQLGetInfo] & [ 68 | // place custom overrides here 69 | SQL_SQL92_PREDICATES = ODBC[SQL_SP][All], 70 | SQL_AGGREGATE_FUNCTIONS = ODBC[SQL_AF][All] 71 | ] 72 | ], 73 | 74 | OdbcDatasource = Odbc.DataSource(ConnectionString, options) 75 | in 76 | OdbcDatasource; 77 | 78 | // Data Source Kind description 79 | WideWorldImporters = [ 80 | TestConnection = (dataSourcePath) => { "WideWorldImporters.Contents" }, 81 | // Set supported types of authentication 82 | Authentication = [ 83 | Windows = [] 84 | ], 85 | Label = Extension.LoadString("DataSourceLabel") 86 | ]; 87 | 88 | // Data Source UI publishing description 89 | WideWorldImporters.Publish = [ 90 | Beta = true, 91 | Category = "Other", 92 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 93 | LearnMoreUrl = "https://powerbi.microsoft.com/", 94 | 95 | SupportsDirectQuery = Config_EnableDirectQuery, 96 | 97 | SourceImage = WideWorldImporters.Icons, 98 | SourceTypeImage = WideWorldImporters.Icons 99 | ]; 100 | 101 | WideWorldImporters.Icons = [ 102 | Icon16 = { Extension.Contents("WideWorldImporters16.png"), Extension.Contents("WideWorldImporters20.png"), Extension.Contents("WideWorldImporters24.png"), Extension.Contents("WideWorldImporters32.png") }, 103 | Icon32 = { Extension.Contents("WideWorldImporters32.png"), Extension.Contents("WideWorldImporters40.png"), Extension.Contents("WideWorldImporters48.png"), Extension.Contents("WideWorldImporters64.png") } 104 | ]; 105 | 106 | // build settings based on configuration variables 107 | BuildOdbcConfig = () as record => 108 | let 109 | defaultConfig = [ 110 | SqlCapabilities = [], 111 | SQLGetFunctions = [], 112 | SQLGetInfo = [] 113 | ], 114 | 115 | withParams = 116 | if (Config_UseParameterBindings = false) then 117 | let 118 | caps = defaultConfig[SqlCapabilities] & [ 119 | SqlCapabilities = [ 120 | SupportsNumericLiterals = true, 121 | SupportsStringLiterals = true, 122 | SupportsOdbcDateLiterals = true, 123 | SupportsOdbcTimeLiterals = true, 124 | SupportsOdbcTimestampLiterals = true 125 | ] 126 | ], 127 | funcs = defaultConfig[SQLGetFunctions] & [ 128 | SQLGetFunctions = [ 129 | SQL_API_SQLBINDPARAMETER = false 130 | ] 131 | ] 132 | in 133 | defaultConfig & caps & funcs 134 | else 135 | defaultConfig, 136 | 137 | withEscape = 138 | if (Config_StringLiterateEscapeCharacters <> null) then 139 | let 140 | caps = withParams[SqlCapabilities] & [ 141 | SqlCapabilities = [ 142 | StringLiteralEscapeCharacters = Config_StringLiterateEscapeCharacters 143 | ] 144 | ] 145 | in 146 | withParams & caps 147 | else 148 | withParams, 149 | 150 | withTop = 151 | let 152 | caps = withEscape[SqlCapabilities] & [ 153 | SqlCapabilities = [ 154 | SupportsTop = Config_SupportsTop 155 | ] 156 | ] 157 | in 158 | withEscape & caps, 159 | 160 | withCastOrConvert = 161 | if (Config_UseCastInsteadOfConvert = true) then 162 | let 163 | caps = withTop[SQLGetFunctions] & [ 164 | SQLGetFunctions = [ 165 | SQL_CONVERT_FUNCTIONS = 0x2 /* SQL_FN_CVT_CAST */ 166 | ] 167 | ] 168 | in 169 | withTop & caps 170 | else 171 | withTop, 172 | 173 | withSqlConformance = 174 | if (Config_SqlConformance <> null) then 175 | let 176 | caps = withCastOrConvert[SQLGetInfo] & [ 177 | SQLGetInfo = [ 178 | SQL_SQL_CONFORMANCE = Config_SqlConformance 179 | ] 180 | ] 181 | in 182 | withCastOrConvert & caps 183 | else 184 | withCastOrConvert 185 | in 186 | withSqlConformance; 187 | 188 | // 189 | // Load common library functions 190 | // 191 | Extension.LoadFunction = (name as text) => 192 | let 193 | binary = Extension.Contents(name), 194 | asText = Text.FromBinary(binary) 195 | in 196 | Expression.Evaluate(asText, #shared); 197 | 198 | // Diagnostics module contains multiple functions. We can take the ones we need. 199 | Diagnostics = Extension.LoadFunction("Diagnostics.pqm"); 200 | Diagnostics.LogValue = if (EnableTraceOutput) then Diagnostics[LogValue] else (prefix, value) => value; 201 | 202 | // OdbcConstants contains numeric constants from the ODBC header files, and a 203 | // helper function to create bitfield values. 204 | ODBC = Extension.LoadFunction("OdbcConstants.pqm"); 205 | Odbc.Flags = ODBC[Flags]; -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters.query.pq: -------------------------------------------------------------------------------- 1 | section WideWorldImportersUnitTest; 2 | 3 | // Reuseable queries 4 | Root = WideWorldImporters.Contents(); 5 | 6 | shared WideWorldImporters.UnitTest = 7 | [ 8 | // Fact(, , ) 9 | // and can be a literal or let statement 10 | facts = 11 | { 12 | Fact("Root is nav table", true, Test.IsNavTable(Root)), 13 | Fact("Root contains two tables", true, Table.RowCount(Root) = 2), 14 | Fact("Children are nav tables", true, Test.IsNavTable(Root{0}[Data]) and Test.IsNavTable(Root{1}[Data])), 15 | Fact("Expected child tables", {"Dimension", "Fact"}, Root[Name]) 16 | }, 17 | 18 | report = Facts.Summarize(facts) 19 | 20 | ][report]; 21 | 22 | 23 | // 24 | // Unit test helpers 25 | // 26 | 27 | Test.HasColumn = (t as table, c as text) as logical => 28 | List.Contains(Table.ColumnNames(t), c); 29 | 30 | Test.IsNavTable = (t as table) as logical => 31 | List.Contains(Record.FieldNames(Value.Metadata(Value.Type(t))), "NavigationTable.NameColumn"); 32 | 33 | /// 34 | /// COMMON UNIT TESTING CODE 35 | /// 36 | 37 | Fact = (_subject as text, _expected, _actual) as record => 38 | [ expected = try _expected, 39 | safeExpected = if expected[HasError] then "Expected : "& @ValueToText(expected[Error]) else expected[Value], 40 | actual = try _actual, 41 | safeActual = if actual[HasError] then "Actual : "& @ValueToText(actual[Error]) else actual[Value], 42 | attempt = try safeExpected = safeActual, 43 | result = if attempt[HasError] or not attempt[Value] then "Failure ⛔" else "Success ✓", 44 | resultOp = if result = "Success ✓" then " = " else " <> ", 45 | addendumEvalAttempt = if attempt[HasError] then @ValueToText(attempt[Error]) else "", 46 | addendumEvalExpected = try @ValueToText(safeExpected) otherwise "...", 47 | addendumEvalActual = try @ValueToText (safeActual) otherwise "...", 48 | fact = 49 | [ Result = result &" "& addendumEvalAttempt, 50 | Notes =_subject, 51 | Details = " ("& addendumEvalExpected & resultOp & addendumEvalActual &")" 52 | ] 53 | ][fact]; 54 | 55 | Facts = (_subject as text, _predicates as list) => List.Transform(_predicates, each Fact(_subject,_{0},_{1})); 56 | 57 | Facts.Summarize = (_facts as list) as table => 58 | [ Fact.CountSuccesses = (count, i) => 59 | [ result = try i[Result], 60 | sum = if result[HasError] or not Text.StartsWith(result[Value], "Success") then count else count + 1 61 | ][sum], 62 | passed = List.Accumulate(_facts, 0, Fact.CountSuccesses), 63 | total = List.Count(_facts), 64 | format = if passed = total then "All #{0} Passed !!! ✓" else "#{0} Passed ☺ #{1} Failed ☹", 65 | result = if passed = total then "Success" else "⛔", 66 | rate = Number.IntegerDivide(100*passed, total), 67 | header = 68 | [ Result = result, 69 | Notes = Text.Format(format, {passed, total-passed}), 70 | Details = Text.Format("#{0}% success rate", {rate}) 71 | ], 72 | report = Table.FromRecords(List.Combine({{header},_facts})) 73 | ][report]; 74 | 75 | ValueToText = (value, optional depth) => 76 | let 77 | _canBeIdentifier = (x) => 78 | let 79 | keywords = {"and", "as", "each", "else", "error", "false", "if", "in", "is", "let", "meta", "not", "otherwise", "or", "section", "shared", "then", "true", "try", "type" }, 80 | charAlpha = (c as number) => (c>= 65 and c <= 90) or (c>= 97 and c <= 122) or c=95, 81 | charDigit = (c as number) => c>= 48 and c <= 57 82 | in 83 | try 84 | charAlpha(Character.ToNumber(Text.At(x,0))) 85 | and 86 | List.MatchesAll( 87 | Text.ToList(x), 88 | (c)=> let num = Character.ToNumber(c) in charAlpha(num) or charDigit(num) 89 | ) 90 | and not 91 | List.MatchesAny( keywords, (li)=> li=x ) 92 | otherwise 93 | false, 94 | 95 | Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ", 96 | 97 | Serialize.Date = (x) => "#date(" & 98 | Text.From(Date.Year(x)) & ", " & 99 | Text.From(Date.Month(x)) & ", " & 100 | Text.From(Date.Day(x)) & ") ", 101 | 102 | Serialize.Datetime = (x) => "#datetime(" & 103 | Text.From(Date.Year(DateTime.Date(x))) & ", " & 104 | Text.From(Date.Month(DateTime.Date(x))) & ", " & 105 | Text.From(Date.Day(DateTime.Date(x))) & ", " & 106 | Text.From(Time.Hour(DateTime.Time(x))) & ", " & 107 | Text.From(Time.Minute(DateTime.Time(x))) & ", " & 108 | Text.From(Time.Second(DateTime.Time(x))) & ") ", 109 | 110 | Serialize.Datetimezone =(x) => let 111 | dtz = DateTimeZone.ToRecord(x) 112 | in 113 | "#datetimezone(" & 114 | Text.From(dtz[Year]) & ", " & 115 | Text.From(dtz[Month]) & ", " & 116 | Text.From(dtz[Day]) & ", " & 117 | Text.From(dtz[Hour]) & ", " & 118 | Text.From(dtz[Minute]) & ", " & 119 | Text.From(dtz[Second]) & ", " & 120 | Text.From(dtz[ZoneHours]) & ", " & 121 | Text.From(dtz[ZoneMinutes]) & ") ", 122 | 123 | Serialize.Duration = (x) => let 124 | dur = Duration.ToRecord(x) 125 | in 126 | "#duration(" & 127 | Text.From(dur[Days]) & ", " & 128 | Text.From(dur[Hours]) & ", " & 129 | Text.From(dur[Minutes]) & ", " & 130 | Text.From(dur[Seconds]) & ") ", 131 | 132 | Serialize.Function = (x) => _serialize_function_param_type( 133 | Type.FunctionParameters(Value.Type(x)), 134 | Type.FunctionRequiredParameters(Value.Type(x)) ) & 135 | " as " & 136 | _serialize_function_return_type(Value.Type(x)) & 137 | " => (...) ", 138 | 139 | Serialize.List = (x) => "{" & 140 | List.Accumulate(x, "", (seed,item) => if seed="" then Serialize(item) else seed & ", " & Serialize(item)) & 141 | "} ", 142 | 143 | Serialize.Logical = (x) => Text.From(x), 144 | 145 | Serialize.Null = (x) => "null", 146 | 147 | Serialize.Number = (x) => 148 | let Text.From = (i as number) as text => 149 | if Number.IsNaN(i) then "#nan" else 150 | if i=Number.PositiveInfinity then "#infinity" else 151 | if i=Number.NegativeInfinity then "-#infinity" else 152 | Text.From(i) 153 | in 154 | Text.From(x), 155 | 156 | Serialize.Record = (x) => "[ " & 157 | List.Accumulate( 158 | Record.FieldNames(x), 159 | "", 160 | (seed,item) => 161 | (if seed="" then Serialize.Identifier(item) else seed & ", " & Serialize.Identifier(item)) & " = " & Serialize(Record.Field(x, item)) 162 | ) & 163 | " ] ", 164 | 165 | Serialize.Table = (x) => "#table( type " & 166 | _serialize_table_type(Value.Type(x)) & 167 | ", " & 168 | Serialize(Table.ToRows(x)) & 169 | ") ", 170 | 171 | Serialize.Text = (x) => """" & 172 | _serialize_text_content(x) & 173 | """", 174 | 175 | _serialize_text_content = (x) => let 176 | escapeText = (n as number) as text => "#(#)(" & Text.PadStart(Number.ToText(n, "X", "en-US"), 4, "0") & ")" 177 | in 178 | List.Accumulate( 179 | List.Transform( 180 | Text.ToList(x), 181 | (c) => let n=Character.ToNumber(c) in 182 | if n = 9 then "#(#)(tab)" else 183 | if n = 10 then "#(#)(lf)" else 184 | if n = 13 then "#(#)(cr)" else 185 | if n = 34 then """""" else 186 | if n = 35 then "#(#)(#)" else 187 | if n < 32 then escapeText(n) else 188 | if n < 127 then Character.FromNumber(n) else 189 | escapeText(n) 190 | ), 191 | "", 192 | (s,i)=>s&i 193 | ), 194 | 195 | Serialize.Identifier = (x) => 196 | if _canBeIdentifier(x) then 197 | x 198 | else 199 | "#""" & 200 | _serialize_text_content(x) & 201 | """", 202 | 203 | Serialize.Time = (x) => "#time(" & 204 | Text.From(Time.Hour(x)) & ", " & 205 | Text.From(Time.Minute(x)) & ", " & 206 | Text.From(Time.Second(x)) & ") ", 207 | 208 | Serialize.Type = (x) => "type " & _serialize_typename(x), 209 | 210 | 211 | _serialize_typename = (x, optional funtype as logical) => /* Optional parameter: Is this being used as part of a function signature? */ 212 | let 213 | isFunctionType = (x as type) => try if Type.FunctionReturn(x) is type then true else false otherwise false, 214 | isTableType = (x as type) => try if Type.TableSchema(x) is table then true else false otherwise false, 215 | isRecordType = (x as type) => try if Type.ClosedRecord(x) is type then true else false otherwise false, 216 | isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false 217 | in 218 | 219 | if funtype=null and isTableType(x) then _serialize_table_type(x) else 220 | if funtype=null and isListType(x) then "{ " & @_serialize_typename( Type.ListItem(x) ) & " }" else 221 | if funtype=null and isFunctionType(x) then "function " & _serialize_function_type(x) else 222 | if funtype=null and isRecordType(x) then _serialize_record_type(x) else 223 | 224 | if x = type any then "any" else 225 | let base = Type.NonNullable(x) in 226 | (if Type.IsNullable(x) then "nullable " else "") & 227 | (if base = type anynonnull then "anynonnull" else 228 | if base = type binary then "binary" else 229 | if base = type date then "date" else 230 | if base = type datetime then "datetime" else 231 | if base = type datetimezone then "datetimezone" else 232 | if base = type duration then "duration" else 233 | if base = type logical then "logical" else 234 | if base = type none then "none" else 235 | if base = type null then "null" else 236 | if base = type number then "number" else 237 | if base = type text then "text" else 238 | if base = type time then "time" else 239 | if base = type type then "type" else 240 | 241 | /* Abstract types: */ 242 | if base = type function then "function" else 243 | if base = type table then "table" else 244 | if base = type record then "record" else 245 | if base = type list then "list" else 246 | 247 | "any /*Actually unknown type*/"), 248 | 249 | _serialize_table_type = (x) => 250 | let 251 | schema = Type.TableSchema(x) 252 | in 253 | "table " & 254 | (if Table.IsEmpty(schema) then "" else 255 | "[" & List.Accumulate( 256 | List.Transform( 257 | Table.ToRecords(Table.Sort(schema,"Position")), 258 | each Serialize.Identifier(_[Name]) & " = " & _[Kind]), 259 | "", 260 | (seed,item) => (if seed="" then item else seed & ", " & item ) 261 | ) & "] " ), 262 | 263 | _serialize_record_type = (x) => 264 | let flds = Type.RecordFields(x) 265 | in 266 | if Record.FieldCount(flds)=0 then "record" else 267 | "[" & List.Accumulate( 268 | Record.FieldNames(flds), 269 | "", 270 | (seed,item) => 271 | seed & 272 | (if seed<>"" then ", " else "") & 273 | (Serialize.Identifier(item) & "=" & _serialize_typename(Record.Field(flds,item)[Type]) ) 274 | ) & 275 | (if Type.IsOpenRecord(x) then ",..." else "") & 276 | "]", 277 | 278 | _serialize_function_type = (x) => _serialize_function_param_type( 279 | Type.FunctionParameters(x), 280 | Type.FunctionRequiredParameters(x) ) & 281 | " as " & 282 | _serialize_function_return_type(x), 283 | 284 | _serialize_function_param_type = (t,n) => 285 | let 286 | funsig = Table.ToRecords( 287 | Table.TransformColumns( 288 | Table.AddIndexColumn( Record.ToTable( t ), "isOptional", 1 ), 289 | { "isOptional", (x)=> x>n } ) ) 290 | in 291 | "(" & 292 | List.Accumulate( 293 | funsig, 294 | "", 295 | (seed,item)=> 296 | (if seed="" then "" else seed & ", ") & 297 | (if item[isOptional] then "optional " else "") & 298 | Serialize.Identifier(item[Name]) & " as " & _serialize_typename(item[Value], true) ) 299 | & ")", 300 | 301 | _serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true), 302 | 303 | Serialize = (x) as text => 304 | if x is binary then try Serialize.Binary(x) otherwise "null /*serialize failed*/" else 305 | if x is date then try Serialize.Date(x) otherwise "null /*serialize failed*/" else 306 | if x is datetime then try Serialize.Datetime(x) otherwise "null /*serialize failed*/" else 307 | if x is datetimezone then try Serialize.Datetimezone(x) otherwise "null /*serialize failed*/" else 308 | if x is duration then try Serialize.Duration(x) otherwise "null /*serialize failed*/" else 309 | if x is function then try Serialize.Function(x) otherwise "null /*serialize failed*/" else 310 | if x is list then try Serialize.List(x) otherwise "null /*serialize failed*/" else 311 | if x is logical then try Serialize.Logical(x) otherwise "null /*serialize failed*/" else 312 | if x is null then try Serialize.Null(x) otherwise "null /*serialize failed*/" else 313 | if x is number then try Serialize.Number(x) otherwise "null /*serialize failed*/" else 314 | if x is record then try Serialize.Record(x) otherwise "null /*serialize failed*/" else 315 | if x is table then try Serialize.Table(x) otherwise "null /*serialize failed*/" else 316 | if x is text then try Serialize.Text(x) otherwise "null /*serialize failed*/" else 317 | if x is time then try Serialize.Time(x) otherwise "null /*serialize failed*/" else 318 | if x is type then try Serialize.Type(x) otherwise "null /*serialize failed*/" else 319 | "[#_unable_to_serialize_#]" 320 | in 321 | try Serialize(value) otherwise ""; -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters16.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters20.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters24.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters32.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters40.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters48.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters64.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/WideWorldImporters80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Ignite2018/WideWorldImporters/WideWorldImporters80.png -------------------------------------------------------------------------------- /Ignite2018/WideWorldImporters/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Wide World Importers 122 | 123 | 124 | Wide World Importers 125 | 126 | 127 | Wide World Importers 128 | 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matt Masson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Fireball 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Code 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball.pq: -------------------------------------------------------------------------------- 1 | // Connector based on the NASA/JPL Fireball API - https://ssd-api.jpl.nasa.gov/doc/fireball.html 2 | section Fireball; 3 | 4 | BaseUrl = "https://ssd-api.jpl.nasa.gov/fireball.api"; 5 | 6 | [DataSource.Kind="Fireball", Publish="Fireball.Publish"] 7 | shared Fireball.Contents = () => 8 | let 9 | source = Web.Contents(BaseUrl), 10 | json = Json.Document(source) 11 | in 12 | json; 13 | 14 | Fireball = [ 15 | Authentication = [ 16 | Anonymous = [] 17 | ], 18 | Label = Extension.LoadString("DataSourceLabel") 19 | ]; 20 | 21 | // Data Source UI publishing description 22 | Fireball.Publish = [ 23 | Beta = true, 24 | Category = "Other", 25 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 26 | LearnMoreUrl = "https://powerbi.microsoft.com/", 27 | SourceImage = Fireball.Icons, 28 | SourceTypeImage = Fireball.Icons 29 | ]; 30 | 31 | Fireball.Icons = [ 32 | Icon16 = { Extension.Contents("Fireball16.png"), Extension.Contents("Fireball20.png"), Extension.Contents("Fireball24.png"), Extension.Contents("Fireball32.png") }, 33 | Icon32 = { Extension.Contents("Fireball32.png"), Extension.Contents("Fireball40.png"), Extension.Contents("Fireball48.png"), Extension.Contents("Fireball64.png") } 34 | ]; 35 | -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = Fireball.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball16.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball20.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball24.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball32.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball40.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball48.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball64.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/Fireball80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/Fireball/Fireball80.png -------------------------------------------------------------------------------- /MBAS2019/Fireball/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Fireball 122 | 123 | 124 | Fireball 125 | 126 | 127 | Fireball 128 | 129 | -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Fireball 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Code 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball.pq: -------------------------------------------------------------------------------- 1 | // Connector based on the NASA/JPL Fireball API - https://ssd-api.jpl.nasa.gov/doc/fireball.html 2 | section Fireball; 3 | 4 | BaseUrl = "https://ssd-api.jpl.nasa.gov/fireball.api"; 5 | 6 | [DataSource.Kind="Fireball", Publish="Fireball.Publish"] 7 | shared Fireball.Contents = () => 8 | let 9 | source = Web.Contents(BaseUrl, [ 10 | Query = [ 11 | #"req-loc" = "true" 12 | ] 13 | ]), 14 | json = Json.Document(source), 15 | fields = json[fields], 16 | data = json[data], 17 | toTable = Table.FromList(data, Splitter.SplitByNothing()), 18 | withRecords = Table.AddColumn(toTable, "Custom", each Record.FromList([Column1], fields)), 19 | expand = Table.ExpandRecordColumn(withRecords, "Custom", fields), 20 | removeList = Table.RemoveColumns(expand, {"Column1"}), 21 | withTypes = Table.TransformColumnTypes(removeList,{{"date", type datetime}, {"energy", type number}, {"impact-e", type number}, {"lat", type number}, {"lat-dir", type text}, {"lon", type number}, {"lon-dir", type text}, {"alt", type number}, {"vel", type number}}) 22 | in 23 | withTypes; 24 | 25 | Fireball = [ 26 | Authentication = [ 27 | Anonymous = [] 28 | ], 29 | Label = Extension.LoadString("DataSourceLabel") 30 | ]; 31 | 32 | // Data Source UI publishing description 33 | Fireball.Publish = [ 34 | Beta = true, 35 | Category = "Other", 36 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 37 | LearnMoreUrl = "https://powerbi.microsoft.com/", 38 | SourceImage = Fireball.Icons, 39 | SourceTypeImage = Fireball.Icons 40 | ]; 41 | 42 | Fireball.Icons = [ 43 | Icon16 = { Extension.Contents("Fireball16.png"), Extension.Contents("Fireball20.png"), Extension.Contents("Fireball24.png"), Extension.Contents("Fireball32.png") }, 44 | Icon32 = { Extension.Contents("Fireball32.png"), Extension.Contents("Fireball40.png"), Extension.Contents("Fireball48.png"), Extension.Contents("Fireball64.png") } 45 | ]; 46 | -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = Fireball.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball16.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball20.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball24.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball32.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball40.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball48.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball64.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/Fireball80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/FireballComplete/Fireball80.png -------------------------------------------------------------------------------- /MBAS2019/FireballComplete/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Fireball 122 | 123 | 124 | Fireball 125 | 126 | 127 | Fireball 128 | 129 | -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | TripPin 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Code 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin.pq: -------------------------------------------------------------------------------- 1 | // This file contains your Data Connector logic 2 | section TripPin; 3 | 4 | [DataSource.Kind="TripPin", Publish="TripPin.Publish"] 5 | shared TripPin.Contents = () => OData.Feed("https://services.odata.org/v4/TripPinService/"); 6 | 7 | // Data Source Kind description 8 | TripPin = [ 9 | Authentication = [ 10 | // Key = [], 11 | // UsernamePassword = [], 12 | // Windows = [], 13 | Implicit = [] 14 | ], 15 | Label = Extension.LoadString("DataSourceLabel") 16 | ]; 17 | 18 | // Data Source UI publishing description 19 | TripPin.Publish = [ 20 | Beta = true, 21 | Category = "Other", 22 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 23 | LearnMoreUrl = "https://powerbi.microsoft.com/", 24 | SourceImage = TripPin.Icons, 25 | SourceTypeImage = TripPin.Icons 26 | ]; 27 | 28 | TripPin.Icons = [ 29 | Icon16 = { Extension.Contents("TripPin16.png"), Extension.Contents("TripPin20.png"), Extension.Contents("TripPin24.png"), Extension.Contents("TripPin32.png") }, 30 | Icon32 = { Extension.Contents("TripPin32.png"), Extension.Contents("TripPin40.png"), Extension.Contents("TripPin48.png"), Extension.Contents("TripPin64.png") } 31 | ]; 32 | -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = TripPin.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin16.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin20.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin24.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin32.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin40.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin48.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin64.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/TripPin80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/MBAS2019/TripPin/TripPin80.png -------------------------------------------------------------------------------- /MBAS2019/TripPin/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to TripPin 122 | 123 | 124 | TripPin 125 | 126 | 127 | TripPin 128 | 129 | -------------------------------------------------------------------------------- /NASA/Diagnostics.pqm: -------------------------------------------------------------------------------- 1 | let 2 | Diagnostics.LogValue = (prefix, value) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & (try Diagnostics.ValueToText(value) otherwise ""), value), 3 | Diagnostics.LogValue2 = (prefix, value, result, optional delayed) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Diagnostics.ValueToText(value), result, delayed), 4 | Diagnostics.LogFailure = (text, function) => 5 | let 6 | result = try function() 7 | in 8 | if result[HasError] then Diagnostics.LogValue2(text, result[Error], () => error result[Error], true) else result[Value], 9 | 10 | Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function => 11 | Function.From(Value.Type(innerFunction), (list) => outerFunction(() => Function.Invoke(innerFunction, list))), 12 | 13 | Diagnostics.WrapHandlers = (handlers as record) as record => 14 | Record.FromList( 15 | List.Transform( 16 | Record.FieldNames(handlers), 17 | (h) => Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))), 18 | Record.FieldNames(handlers)), 19 | 20 | Diagnostics.ValueToText = (value) => 21 | let 22 | _canBeIdentifier = (x) => 23 | let 24 | keywords = {"and", "as", "each", "else", "error", "false", "if", "in", "is", "let", "meta", "not", "otherwise", "or", "section", "shared", "then", "true", "try", "type" }, 25 | charAlpha = (c as number) => (c>= 65 and c <= 90) or (c>= 97 and c <= 122) or c=95, 26 | charDigit = (c as number) => c>= 48 and c <= 57 27 | in 28 | try 29 | charAlpha(Character.ToNumber(Text.At(x,0))) 30 | and 31 | List.MatchesAll( 32 | Text.ToList(x), 33 | (c)=> let num = Character.ToNumber(c) in charAlpha(num) or charDigit(num) 34 | ) 35 | and not 36 | List.MatchesAny( keywords, (li)=> li=x ) 37 | otherwise 38 | false, 39 | 40 | Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ", 41 | 42 | Serialize.Date = (x) => "#date(" & 43 | Text.From(Date.Year(x)) & ", " & 44 | Text.From(Date.Month(x)) & ", " & 45 | Text.From(Date.Day(x)) & ") ", 46 | 47 | Serialize.Datetime = (x) => "#datetime(" & 48 | Text.From(Date.Year(DateTime.Date(x))) & ", " & 49 | Text.From(Date.Month(DateTime.Date(x))) & ", " & 50 | Text.From(Date.Day(DateTime.Date(x))) & ", " & 51 | Text.From(Time.Hour(DateTime.Time(x))) & ", " & 52 | Text.From(Time.Minute(DateTime.Time(x))) & ", " & 53 | Text.From(Time.Second(DateTime.Time(x))) & ") ", 54 | 55 | Serialize.Datetimezone =(x) => let 56 | dtz = DateTimeZone.ToRecord(x) 57 | in 58 | "#datetimezone(" & 59 | Text.From(dtz[Year]) & ", " & 60 | Text.From(dtz[Month]) & ", " & 61 | Text.From(dtz[Day]) & ", " & 62 | Text.From(dtz[Hour]) & ", " & 63 | Text.From(dtz[Minute]) & ", " & 64 | Text.From(dtz[Second]) & ", " & 65 | Text.From(dtz[ZoneHours]) & ", " & 66 | Text.From(dtz[ZoneMinutes]) & ") ", 67 | 68 | Serialize.Duration = (x) => let 69 | dur = Duration.ToRecord(x) 70 | in 71 | "#duration(" & 72 | Text.From(dur[Days]) & ", " & 73 | Text.From(dur[Hours]) & ", " & 74 | Text.From(dur[Minutes]) & ", " & 75 | Text.From(dur[Seconds]) & ") ", 76 | 77 | Serialize.Function = (x) => _serialize_function_param_type( 78 | Type.FunctionParameters(Value.Type(x)), 79 | Type.FunctionRequiredParameters(Value.Type(x)) ) & 80 | " as " & 81 | _serialize_function_return_type(Value.Type(x)) & 82 | " => (...) ", 83 | 84 | Serialize.List = (x) => "{" & 85 | List.Accumulate(x, "", (seed,item) => if seed="" then Serialize(item) else seed & ", " & Serialize(item)) & 86 | "} ", 87 | 88 | Serialize.Logical = (x) => Text.From(x), 89 | 90 | Serialize.Null = (x) => "null", 91 | 92 | Serialize.Number = (x) => 93 | let Text.From = (i as number) as text => 94 | if Number.IsNaN(i) then "#nan" else 95 | if i=Number.PositiveInfinity then "#infinity" else 96 | if i=Number.NegativeInfinity then "-#infinity" else 97 | Text.From(i) 98 | in 99 | Text.From(x), 100 | 101 | Serialize.Record = (x) => "[ " & 102 | List.Accumulate( 103 | Record.FieldNames(x), 104 | "", 105 | (seed,item) => 106 | (if seed="" then Serialize.Identifier(item) else seed & ", " & Serialize.Identifier(item)) & " = " & Serialize(Record.Field(x, item)) 107 | ) & 108 | " ] ", 109 | 110 | Serialize.Table = (x) => "#table( type " & 111 | _serialize_table_type(Value.Type(x)) & 112 | ", " & 113 | Serialize(Table.ToRows(x)) & 114 | ") ", 115 | 116 | Serialize.Text = (x) => """" & 117 | _serialize_text_content(x) & 118 | """", 119 | 120 | _serialize_text_content = (x) => let 121 | escapeText = (n as number) as text => "#(#)(" & Text.PadStart(Number.ToText(n, "X", "en-US"), 4, "0") & ")" 122 | in 123 | List.Accumulate( 124 | List.Transform( 125 | Text.ToList(x), 126 | (c) => let n=Character.ToNumber(c) in 127 | if n = 9 then "#(#)(tab)" else 128 | if n = 10 then "#(#)(lf)" else 129 | if n = 13 then "#(#)(cr)" else 130 | if n = 34 then """""" else 131 | if n = 35 then "#(#)(#)" else 132 | if n < 32 then escapeText(n) else 133 | if n < 127 then Character.FromNumber(n) else 134 | escapeText(n) 135 | ), 136 | "", 137 | (s,i)=>s&i 138 | ), 139 | 140 | Serialize.Identifier = (x) => 141 | if _canBeIdentifier(x) then 142 | x 143 | else 144 | "#""" & 145 | _serialize_text_content(x) & 146 | """", 147 | 148 | Serialize.Time = (x) => "#time(" & 149 | Text.From(Time.Hour(x)) & ", " & 150 | Text.From(Time.Minute(x)) & ", " & 151 | Text.From(Time.Second(x)) & ") ", 152 | 153 | Serialize.Type = (x) => "type " & _serialize_typename(x), 154 | 155 | 156 | _serialize_typename = (x, optional funtype as logical) => /* Optional parameter: Is this being used as part of a function signature? */ 157 | let 158 | isFunctionType = (x as type) => try if Type.FunctionReturn(x) is type then true else false otherwise false, 159 | isTableType = (x as type) => try if Type.TableSchema(x) is table then true else false otherwise false, 160 | isRecordType = (x as type) => try if Type.ClosedRecord(x) is type then true else false otherwise false, 161 | isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false 162 | in 163 | 164 | if funtype=null and isTableType(x) then _serialize_table_type(x) else 165 | if funtype=null and isListType(x) then "{ " & @_serialize_typename( Type.ListItem(x) ) & " }" else 166 | if funtype=null and isFunctionType(x) then "function " & _serialize_function_type(x) else 167 | if funtype=null and isRecordType(x) then _serialize_record_type(x) else 168 | 169 | if x = type any then "any" else 170 | let base = Type.NonNullable(x) in 171 | (if Type.IsNullable(x) then "nullable " else "") & 172 | (if base = type anynonnull then "anynonnull" else 173 | if base = type binary then "binary" else 174 | if base = type date then "date" else 175 | if base = type datetime then "datetime" else 176 | if base = type datetimezone then "datetimezone" else 177 | if base = type duration then "duration" else 178 | if base = type logical then "logical" else 179 | if base = type none then "none" else 180 | if base = type null then "null" else 181 | if base = type number then "number" else 182 | if base = type text then "text" else 183 | if base = type time then "time" else 184 | if base = type type then "type" else 185 | 186 | /* Abstract types: */ 187 | if base = type function then "function" else 188 | if base = type table then "table" else 189 | if base = type record then "record" else 190 | if base = type list then "list" else 191 | 192 | "any /*Actually unknown type*/"), 193 | 194 | _serialize_table_type = (x) => 195 | let 196 | schema = Type.TableSchema(x) 197 | in 198 | "table " & 199 | (if Table.IsEmpty(schema) then "" else 200 | "[" & List.Accumulate( 201 | List.Transform( 202 | Table.ToRecords(Table.Sort(schema,"Position")), 203 | each Serialize.Identifier(_[Name]) & " = " & _[Kind]), 204 | "", 205 | (seed,item) => (if seed="" then item else seed & ", " & item ) 206 | ) & "] " ), 207 | 208 | _serialize_record_type = (x) => 209 | let flds = Type.RecordFields(x) 210 | in 211 | if Record.FieldCount(flds)=0 then "record" else 212 | "[" & List.Accumulate( 213 | Record.FieldNames(flds), 214 | "", 215 | (seed,item) => 216 | seed & 217 | (if seed<>"" then ", " else "") & 218 | (Serialize.Identifier(item) & "=" & _serialize_typename(Record.Field(flds,item)[Type]) ) 219 | ) & 220 | (if Type.IsOpenRecord(x) then ",..." else "") & 221 | "]", 222 | 223 | _serialize_function_type = (x) => _serialize_function_param_type( 224 | Type.FunctionParameters(x), 225 | Type.FunctionRequiredParameters(x) ) & 226 | " as " & 227 | _serialize_function_return_type(x), 228 | 229 | _serialize_function_param_type = (t,n) => 230 | let 231 | funsig = Table.ToRecords( 232 | Table.TransformColumns( 233 | Table.AddIndexColumn( Record.ToTable( t ), "isOptional", 1 ), 234 | { "isOptional", (x)=> x>n } ) ) 235 | in 236 | "(" & 237 | List.Accumulate( 238 | funsig, 239 | "", 240 | (seed,item)=> 241 | (if seed="" then "" else seed & ", ") & 242 | (if item[isOptional] then "optional " else "") & 243 | Serialize.Identifier(item[Name]) & " as " & _serialize_typename(item[Value], true) ) 244 | & ")", 245 | 246 | _serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true), 247 | 248 | Serialize = (x) as text => 249 | if x is binary then try Serialize.Binary(x) otherwise "null /*serialize failed*/" else 250 | if x is date then try Serialize.Date(x) otherwise "null /*serialize failed*/" else 251 | if x is datetime then try Serialize.Datetime(x) otherwise "null /*serialize failed*/" else 252 | if x is datetimezone then try Serialize.Datetimezone(x) otherwise "null /*serialize failed*/" else 253 | if x is duration then try Serialize.Duration(x) otherwise "null /*serialize failed*/" else 254 | if x is function then try Serialize.Function(x) otherwise "null /*serialize failed*/" else 255 | if x is list then try Serialize.List(x) otherwise "null /*serialize failed*/" else 256 | if x is logical then try Serialize.Logical(x) otherwise "null /*serialize failed*/" else 257 | if x is null then try Serialize.Null(x) otherwise "null /*serialize failed*/" else 258 | if x is number then try Serialize.Number(x) otherwise "null /*serialize failed*/" else 259 | if x is record then try Serialize.Record(x) otherwise "null /*serialize failed*/" else 260 | if x is table then try Serialize.Table(x) otherwise "null /*serialize failed*/" else 261 | if x is text then try Serialize.Text(x) otherwise "null /*serialize failed*/" else 262 | if x is time then try Serialize.Time(x) otherwise "null /*serialize failed*/" else 263 | if x is type then try Serialize.Type(x) otherwise "null /*serialize failed*/" else 264 | "[#_unable_to_serialize_#]" 265 | in 266 | try Serialize(value) otherwise "" 267 | in 268 | [ 269 | LogValue = Diagnostics.LogValue, 270 | LogValue2 = Diagnostics.LogValue2, 271 | LogFailure = Diagnostics.LogFailure, 272 | WrapFunctionResult = Diagnostics.WrapFunctionResult, 273 | WrapHandlers = Diagnostics.WrapHandlers, 274 | ValueToText = Diagnostics.ValueToText 275 | ] -------------------------------------------------------------------------------- /NASA/NASA.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | NASA 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Content 72 | 73 | 74 | Code 75 | 76 | 77 | Content 78 | 79 | 80 | Content 81 | 82 | 83 | Content 84 | 85 | 86 | Content 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /NASA/NASA.pq: -------------------------------------------------------------------------------- 1 | // Sample connector based on NASA APIs - https://api.nasa.gov/api.html 2 | [Version="1.0.0"] 3 | section NASA; 4 | 5 | // Load the default API key we bundle with the connector 6 | DefaultApiKey = Text.FromBinary(Extension.Contents("apikey")); 7 | 8 | // Endpoint URLs 9 | Endpoint_Fireball = "https://ssd-api.jpl.nasa.gov/fireball.api"; 10 | 11 | // Schemas 12 | FireballResultType = type table [ 13 | date = DateTime.Type, 14 | lat = Number.Type, 15 | #"lat-dir" = Text.Type, 16 | lon = Number.Type, 17 | #"lon-dir" = Text.Type, 18 | alt = Number.Type, 19 | vel = Number.Type, 20 | energy = Number.Type, 21 | #"impact-e" = Number.Type, 22 | vx = Number.Type, 23 | vy = Number.Type, 24 | vz = Number.Type 25 | ]; 26 | 27 | [DataSource.Kind="NASA", Publish="NASA.Publish"] 28 | shared NASA.Contents = ContentsImpl; 29 | 30 | ContentsImpl = () => 31 | let 32 | objects = #table( 33 | {"Name", "Key", "Data", "ItemKind", "ItemName", "IsLeaf"},{ 34 | {"Fireball", "fireball", FireballData(), "Table", "Table", true} 35 | }), 36 | NavTable = Table.ToNavigationTable(objects, {"Key"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf") 37 | in 38 | NavTable; 39 | 40 | FireballData = () as table => 41 | let 42 | result = NasaApiRequest(Endpoint_Fireball, false, [#"vel-comp" = "true"]), 43 | // Transform the data into a tabular format 44 | columns = result[fields], 45 | data = result[data], 46 | toTable = Table.FromList(data, Splitter.SplitByNothing(), {"Data"}), 47 | toRecords = Table.TransformColumns(toTable, {{"Data", each Record.FromList(_, columns)}}), 48 | expanded = Table.ExpandRecordColumn(toRecords, "Data", columns), 49 | // Apply table schema to get the right types 50 | withType = Table.ChangeType(expanded, FireballResultType) 51 | in 52 | withType; 53 | 54 | GetEffectiveApiKey = () as text => 55 | let 56 | credentials = Extension.CurrentCredential(), 57 | keyProvided = credentials[AuthenticationKind]? = "Key" 58 | in 59 | if (keyProvided) then 60 | credentials[Key] 61 | else 62 | DefaultApiKey; 63 | 64 | // Base function used to call the NASA APIs. 65 | NasaApiRequest = (url as text, includeApiKey as logical, optional queryParameters as record) as record => 66 | let 67 | query = if (queryParameters <> null) then queryParameters else [], 68 | queryWithCredentials = query & [ api_key = GetEffectiveApiKey() ], 69 | headers = [ Accept = "application/json" ], 70 | request = Web.Contents(url, [ 71 | Query = if (includeApiKey) then queryWithCredentials else query, 72 | ManualCredentials = true, 73 | Headers = headers ]), 74 | json = Json.Document(request) 75 | in 76 | json; 77 | 78 | // Data Source Kind description 79 | NASA = [ 80 | Authentication = [ 81 | Key = [], 82 | Anonymous = [] 83 | ] 84 | ]; 85 | 86 | // Data Source UI publishing description 87 | NASA.Publish = [ 88 | Beta = true, 89 | Category = "Other", 90 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 91 | LearnMoreUrl = "https://api.nasa.gov/index.html", 92 | SourceImage = NASA.Icons, 93 | SourceTypeImage = NASA.Icons 94 | ]; 95 | 96 | NASA.Icons = [ 97 | Icon16 = { Extension.Contents("NASA16.png"), Extension.Contents("NASA20.png"), Extension.Contents("NASA24.png"), Extension.Contents("NASA32.png") }, 98 | Icon32 = { Extension.Contents("NASA32.png"), Extension.Contents("NASA40.png"), Extension.Contents("NASA48.png"), Extension.Contents("NASA64.png") } 99 | ]; 100 | 101 | // 102 | // Load common library functions 103 | // 104 | // TEMPORARY WORKAROUND until we're able to reference other M modules 105 | Extension.LoadFunction = (name as text) => 106 | let 107 | binary = Extension.Contents(name), 108 | asText = Text.FromBinary(binary) 109 | in 110 | Expression.Evaluate(asText, #shared); 111 | 112 | Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm"); 113 | Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm"); 114 | 115 | // Diagnostics module contains multiple functions. We can take the ones we need. 116 | Diagnostics = Extension.LoadFunction("Diagnostics.pqm"); 117 | Diagnostics.LogValue = Diagnostics[LogValue]; 118 | Diagnostics.LogFailure = Diagnostics[LogFailure]; 119 | Diagnostics.WrapHandlers = Diagnostics[WrapHandlers]; -------------------------------------------------------------------------------- /NASA/NASA.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = NASA.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /NASA/NASA16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA16.png -------------------------------------------------------------------------------- /NASA/NASA20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA20.png -------------------------------------------------------------------------------- /NASA/NASA24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA24.png -------------------------------------------------------------------------------- /NASA/NASA32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA32.png -------------------------------------------------------------------------------- /NASA/NASA40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA40.png -------------------------------------------------------------------------------- /NASA/NASA48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA48.png -------------------------------------------------------------------------------- /NASA/NASA64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA64.png -------------------------------------------------------------------------------- /NASA/NASA80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/NASA/NASA80.png -------------------------------------------------------------------------------- /NASA/README.md: -------------------------------------------------------------------------------- 1 | # Sample connector for NASA APIs 2 | 3 | This is a sample connector APIs listed at the [NASA API Portal](https://api.nasa.gov/index.html). 4 | The sample currently connects to a single API ([Fireball](https://api.nasa.gov/api.html#Fireball)), but may be extended in the future to connect to other APIs. 5 | 6 | Note that while most NASA APIs require the use of an [api_key](https://api.nasa.gov/api.html#authentication), the Fireball API used in this sample does not. 7 | 8 | *Functionality* 9 | - Navigation table 10 | - Multiple authentication types 11 | - API Key based authentication with a default key -------------------------------------------------------------------------------- /NASA/Table.ChangeType.pqm: -------------------------------------------------------------------------------- 1 | let 2 | // table should be an actual Table.Type, or a List.Type of Records 3 | Table.ChangeType = (table, tableType as type) as nullable table => 4 | // we only operate on table types 5 | if (not Type.Is(tableType, type table)) then error "type argument should be a table type" else 6 | // if we have a null value, just return it 7 | if (table = null) then table else 8 | let 9 | columnsForType = Type.RecordFields(Type.TableRow(tableType)), 10 | columnsAsTable = Record.ToTable(columnsForType), 11 | schema = Table.ExpandRecordColumn(columnsAsTable, "Value", {"Type"}, {"Type"}), 12 | previousMeta = Value.Metadata(tableType), 13 | 14 | // make sure we have a table 15 | parameterType = Value.Type(table), 16 | _table = 17 | if (Type.Is(parameterType, type table)) then table 18 | else if (Type.Is(parameterType, type list)) then 19 | let 20 | asTable = Table.FromList(table, Splitter.SplitByNothing(), {"Column1"}), 21 | firstValueType = Value.Type(Table.FirstValue(asTable, null)), 22 | result = 23 | // if the member is a record (as expected), then expand it. 24 | if (Type.Is(firstValueType, type record)) then 25 | Table.ExpandRecordColumn(asTable, "Column1", schema[Name]) 26 | else 27 | error Error.Record("Error.Parameter", "table argument is a list, but not a list of records", [ ValueType = firstValueType ]) 28 | in 29 | if (List.IsEmpty(table)) then 30 | #table({"a"}, {}) 31 | else result 32 | else 33 | error Error.Record("Error.Parameter", "table argument should be a table or list of records", [ValueType = parameterType]), 34 | 35 | reordered = Table.SelectColumns(_table, schema[Name], MissingField.UseNull), 36 | 37 | // process primitive values - this will call Table.TransformColumnTypes 38 | map = (t) => if Type.Is(t, type table) or Type.Is(t, type list) or Type.Is(t, type record) or t = type any then null else t, 39 | mapped = Table.TransformColumns(schema, {"Type", map}), 40 | omitted = Table.SelectRows(mapped, each [Type] <> null), 41 | existingColumns = Table.ColumnNames(reordered), 42 | removeMissing = Table.SelectRows(omitted, each List.Contains(existingColumns, [Name])), 43 | primativeTransforms = Table.ToRows(removeMissing), 44 | changedPrimatives = Table.TransformColumnTypes(reordered, primativeTransforms), 45 | 46 | // Get the list of transforms we'll use for Record types 47 | recordColumns = Table.SelectRows(schema, each Type.Is([Type], type record)), 48 | recordTypeTransformations = Table.AddColumn(recordColumns, "RecordTransformations", each (r) => Record.ChangeType(r, [Type]), type function), 49 | recordChanges = Table.ToRows(Table.SelectColumns(recordTypeTransformations, {"Name", "RecordTransformations"})), 50 | 51 | // Get the list of transforms we'll use for List types 52 | listColumns = Table.SelectRows(schema, each Type.Is([Type], type list)), 53 | listTransforms = Table.AddColumn(listColumns, "ListTransformations", each (t) => List.ChangeType(t, [Type]), Function.Type), 54 | listChanges = Table.ToRows(Table.SelectColumns(listTransforms, {"Name", "ListTransformations"})), 55 | 56 | // Get the list of transforms we'll use for Table types 57 | tableColumns = Table.SelectRows(schema, each Type.Is([Type], type table)), 58 | tableTransforms = Table.AddColumn(tableColumns, "TableTransformations", each (t) => @Table.ChangeType(t, [Type]), Function.Type), 59 | tableChanges = Table.ToRows(Table.SelectColumns(tableTransforms, {"Name", "TableTransformations"})), 60 | 61 | // Perform all of our transformations 62 | allColumnTransforms = recordChanges & listChanges & tableChanges, 63 | changedRecordTypes = if (List.IsEmpty(allColumnTransforms)) then changedPrimatives else Table.TransformColumns(changedPrimatives, allColumnTransforms, null, MissingField.Ignore), 64 | 65 | // set final type 66 | withType = Value.ReplaceType(changedRecordTypes, tableType) 67 | in 68 | if (List.IsEmpty(Record.FieldNames(columnsForType))) then table else withType meta previousMeta, 69 | 70 | // If given a generic record type (no predefined fields), the original record is returned 71 | Record.ChangeType = (record as record, recordType as type) => 72 | let 73 | // record field format is [ fieldName = [ Type = type, Optional = logical], ... ] 74 | fields = try Type.RecordFields(recordType) otherwise error "Record.ChangeType: failed to get record fields. Is this a record type?", 75 | fieldNames = Record.FieldNames(fields), 76 | fieldTable = Record.ToTable(fields), 77 | optionalFields = Table.SelectRows(fieldTable, each [Value][Optional])[Name], 78 | requiredFields = List.Difference(fieldNames, optionalFields), 79 | // make sure all required fields exist 80 | withRequired = Record.SelectFields(record, requiredFields, MissingField.UseNull), 81 | // append optional fields 82 | withOptional = withRequired & Record.SelectFields(record, optionalFields, MissingField.Ignore), 83 | // set types 84 | transforms = GetTransformsForType(recordType), 85 | withTypes = Record.TransformFields(withOptional, transforms, MissingField.Ignore), 86 | // order the same as the record type 87 | reorder = Record.ReorderFields(withTypes, fieldNames, MissingField.Ignore) 88 | in 89 | if (List.IsEmpty(fieldNames)) then record else reorder, 90 | 91 | List.ChangeType = (list as list, listType as type) => 92 | if (not Type.Is(listType, type list)) then error "type argument should be a list type" else 93 | let 94 | listItemType = Type.ListItem(listType), 95 | transform = GetTransformByType(listItemType), 96 | modifiedValues = List.Transform(list, transform), 97 | typed = Value.ReplaceType(modifiedValues, listType) 98 | in 99 | typed, 100 | 101 | // Returns a table type for the provided schema table 102 | Schema.ToTableType = (schema as table) as type => 103 | let 104 | toList = List.Transform(schema[Type], (t) => [Type=t, Optional=false]), 105 | toRecord = Record.FromList(toList, schema[Name]), 106 | toType = Type.ForRecord(toRecord, false), 107 | previousMeta = Value.Metadata(schema) 108 | in 109 | type table (toType) meta previousMeta, 110 | 111 | // Returns a list of transformations that can be passed to Table.TransformColumns, or Record.TransformFields 112 | // Format: {"Column", (f) => ...) .... ex: {"A", Number.From} 113 | GetTransformsForType = (_type as type) as list => 114 | let 115 | fieldsOrColumns = if (Type.Is(_type, type record)) then Type.RecordFields(_type) 116 | else if (Type.Is(_type, type table)) then Type.RecordFields(Type.TableRow(_type)) 117 | else error "GetTransformsForType: record or table type expected", 118 | toTable = Record.ToTable(fieldsOrColumns), 119 | transformColumn = Table.AddColumn(toTable, "Transform", each GetTransformByType([Value][Type]), Function.Type), 120 | transformMap = Table.ToRows(Table.SelectColumns(transformColumn, {"Name", "Transform"})) 121 | in 122 | transformMap, 123 | 124 | GetTransformByType = (_type as type) as function => 125 | if (Type.Is(_type, type number)) then Number.From 126 | else if (Type.Is(_type, type text)) then Text.From 127 | else if (Type.Is(_type, type date)) then Date.From 128 | else if (Type.Is(_type, type datetime)) then DateTime.From 129 | else if (Type.Is(_type, type duration)) then Duration.From 130 | else if (Type.Is(_type, type datetimezone)) then DateTimeZone.From 131 | else if (Type.Is(_type, type logical)) then Logical.From 132 | else if (Type.Is(_type, type time)) then Time.From 133 | else if (Type.Is(_type, type record)) then (t) => if (t <> null) then @Record.ChangeType(t, _type) else t 134 | else if (Type.Is(_type, type table)) then (t) => if (t <> null) then @Table.ChangeType(t, _type) else t 135 | else if (Type.Is(_type, type list)) then (t) => if (t <> null) then @List.ChangeType(t, _type) else t 136 | else (t) => t 137 | in 138 | Table.ChangeType -------------------------------------------------------------------------------- /NASA/Table.ToNavigationTable.pqm: -------------------------------------------------------------------------------- 1 | ( 2 | table as table, 3 | keyColumns as list, 4 | nameColumn as text, 5 | dataColumn as text, 6 | itemKindColumn as text, 7 | itemNameColumn as text, 8 | isLeafColumn as text 9 | ) as table => 10 | let 11 | tableType = Value.Type(table), 12 | newTableType = Type.AddTableKey(tableType, keyColumns, true) meta 13 | [ 14 | NavigationTable.NameColumn = nameColumn, 15 | NavigationTable.DataColumn = dataColumn, 16 | NavigationTable.ItemKindColumn = itemKindColumn, 17 | Preview.DelayColumn = itemNameColumn, 18 | NavigationTable.IsLeafColumn = isLeafColumn 19 | ], 20 | navigationTable = Value.ReplaceType(table, newTableType) 21 | in 22 | navigationTable -------------------------------------------------------------------------------- /NASA/apikey: -------------------------------------------------------------------------------- 1 | DEMO_KEY -------------------------------------------------------------------------------- /NASA/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to NASA 122 | 123 | 124 | NASA 125 | 126 | 127 | NASA 128 | 129 | -------------------------------------------------------------------------------- /Phoenix2018/README.md: -------------------------------------------------------------------------------- 1 | # Custom Connector Samples 2 | 3 | These custom connectors were used in the [Power Summit Phoenix 2018](https://www.powerugsummit.com/home) event. They demonstrate how custom connectors can be used to provide an analyst friendly connector with custom branding a data source. 4 | 5 | The [WideWorldImporters](../Ignite2018/WideWorldImporters) sample enables Direct Query over a local SQL Server database. 6 | 7 | The [Steam](../Steam) sample is a custom connector over a REST API. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerQuery 2 | M scripts, samples, and demos 3 | -------------------------------------------------------------------------------- /Steam/Steam.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | True 15 | False 16 | False 17 | True 18 | False 19 | False 20 | 1000 21 | Yes 22 | Steam 23 | False 24 | 25 | 26 | false 27 | 28 | bin\Debug\ 29 | 30 | 31 | false 32 | bin\Release\ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Code 43 | 44 | 45 | Code 46 | 47 | 48 | Code 49 | 50 | 51 | Code 52 | 53 | 54 | Code 55 | 56 | 57 | Code 58 | 59 | 60 | Code 61 | 62 | 63 | Code 64 | 65 | 66 | Code 67 | 68 | 69 | Code 70 | 71 | 72 | Content 73 | 74 | 75 | Code 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Steam/Steam.pq: -------------------------------------------------------------------------------- 1 | // Overview of the Steam Web API: https://developer.valvesoftware.com/wiki/Steam_Web_API#License_and_further_documentation 2 | section Steam; 3 | 4 | ApiKey = LoadFromResource("apikey"); 5 | 6 | [DataSource.Kind="Steam", Publish="Steam.Publish"] 7 | shared Steam.Contents = Value.ReplaceType(ContentsImpl, SteamContentsType); 8 | 9 | SteamContentsType = type function ( 10 | profile as (type text meta [ 11 | Documentation.FieldCaption = "Profile", 12 | Documentation.FieldDescription = "Steam public profile name", 13 | DataSource.Path = false // don't include in data source path calculation 14 | ]), 15 | optional steamId as (type text meta [ 16 | Documentation.FieldCaption = "Steam ID", 17 | Documentation.FieldDescription = "SteamID64 value. You can provide this value to avoid the additional lookup." 18 | ])) 19 | 20 | as table meta [ 21 | Documentation.Name = "Steam Games", 22 | Documentation.LongDescription = "Steam Games - list all games you own on the steam platform" 23 | ]; 24 | 25 | ContentsImpl = (profile as text, optional steamId as nullable text) => 26 | let 27 | _steamId = if (steamId <> null) then steamId else try LookupSteamIdFromProfile(profile) otherwise error Error.Record("DataSource.Error", "Error retrieving SteamId"), 28 | Source = GetOwnedGames(_steamId), 29 | response = Source[response], 30 | games = response[games], 31 | #"Converted to Table" = Table.FromList(games, Splitter.SplitByNothing(), null, null, ExtraValues.Error), 32 | #"Expanded Column1" = Table.ExpandRecordColumn(#"Converted to Table", "Column1", {"appid", "name", "playtime_forever", "img_icon_url", "img_logo_url", "has_community_visible_stats"}), 33 | #"Cleaned Text" = Table.TransformColumns(#"Expanded Column1",{{"name", Text.Clean}}), 34 | #"Replaced Value" = Table.ReplaceValue(#"Cleaned Text","™","",Replacer.ReplaceText,{"name"}), 35 | #"Replaced Value1" = Table.ReplaceValue(#"Replaced Value","®","",Replacer.ReplaceText,{"name"}), 36 | #"Changed Type" = Table.TransformColumnTypes(#"Replaced Value1",{{"appid", Int64.Type}, {"playtime_forever", Int64.Type}, {"img_icon_url", type text}, {"img_logo_url", type text}, {"has_community_visible_stats", type logical}}), 37 | replaceNull = Table.ReplaceValue(#"Changed Type",null,false,Replacer.ReplaceValue,{"has_community_visible_stats"}), 38 | setIconUrl = Table.AddColumn(replaceNull, "icon_url", each MakeImageUrl([appid], [img_icon_url]), Uri.Type), 39 | setLogoUrl = Table.AddColumn(setIconUrl, "logo_url", each MakeImageUrl([appid], [img_logo_url]), Uri.Type), 40 | remove = Table.RemoveColumns(setLogoUrl, {"img_icon_url", "img_logo_url"})//, 41 | //addScore = Table.AddColumn(remove, "score", each LookupGameScore([name]), type text) 42 | in 43 | remove; 44 | 45 | GetOwnedGames = (steamId as text) => 46 | let 47 | query = [ 48 | key = ApiKey, 49 | steamid = steamId, 50 | format = "json", 51 | include_appinfo = "1" 52 | ], 53 | Source = Json.Document(Web.Contents("https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/", [Query = query])) 54 | in 55 | Source; 56 | 57 | LookupSteamIdFromProfile = (profile as text) as text => 58 | let 59 | _profile = Text.Trim(Text.Clean(profile)), 60 | Source = Xml.Document(Web.Contents("https://steamcommunity.com/id/" & _profile & "?xml=1")), 61 | Value = Source{0}[Value], 62 | GetId = Table.SelectRows(Value, each [Name] = "steamID64"), 63 | id = GetId{0}[Value] 64 | in 65 | id; 66 | 67 | BaseImageUrl = "http://media.steampowered.com/steamcommunity/public/images/apps/"; 68 | 69 | // format is https://media.steampowered.com/steamcommunity/public/images/apps//.jpg 70 | MakeImageUrl = (appId as number, url as nullable text) as nullable text => 71 | if (url = null or url = "") then 72 | null 73 | else 74 | BaseImageUrl & Number.ToText(appId) & "/" & url & ".jpg"; 75 | 76 | LookupGameScore = (title as text) as nullable text => 77 | let 78 | _title = Text.Trim(title), 79 | score = try 80 | let 81 | query = [ search = _title, numrev= "3", site= "pc" ], 82 | contents = Web.Contents("http://www.gamerankings.com/browse.html", [Query = query]), 83 | Source = Table.FromColumns({Lines.FromBinary(contents)}), 84 | Trim = Table.TransformColumns(Source,{},Text.Trim), 85 | Top = Table.FirstN(Trim,400), 86 | Filter = Table.SelectRows(Top, each Text.StartsWith([Column1], "")), 87 | Extract = Table.TransformColumns(Filter, {{"Column1", each Text.BetweenDelimiters(_, "", "", 0, 0), type text}}), 88 | GetValue = Table.First(Extract)[Column1] 89 | in 90 | GetValue otherwise null, 91 | // Sometimes the full title doesn't work. 92 | // If we get back null, try searching for the text after ":". 93 | // If there is nothing after the ":", then give up and return null 94 | return = 95 | if (score = null) then 96 | let 97 | postfix = Text.AfterDelimiter(_title, ":"), 98 | newCall = if (postfix <> "") then @LookupGameScore(postfix) else null 99 | in 100 | newCall 101 | else 102 | score 103 | in 104 | return; 105 | 106 | // Data Source Kind description 107 | Steam = [ 108 | Authentication = [ 109 | Implicit = [] 110 | ] 111 | ]; 112 | 113 | // Data Source UI publishing description 114 | Steam.Publish = [ 115 | Beta = true, 116 | Category = "Other", 117 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 118 | SourceImage = Steam.Icons, 119 | SourceTypeImage = Steam.Icons 120 | ]; 121 | 122 | Steam.Icons = [ 123 | Icon16 = { Extension.Contents("Steam16.png"), Extension.Contents("Steam20.png"), Extension.Contents("Steam24.png"), Extension.Contents("Steam32.png") }, 124 | Icon32 = { Extension.Contents("Steam32.png"), Extension.Contents("Steam40.png"), Extension.Contents("Steam48.png"), Extension.Contents("Steam64.png") } 125 | ]; 126 | 127 | // Common functions 128 | LoadFromResource = (name as text) as text => 129 | try 130 | Text.FromBinary(Extension.Contents(name)) 131 | otherwise 132 | error Error.Unexpected("Resource not found: '" & name & "'. Does the file exist in your project, and is its Build Action set to 'Compile'?"); -------------------------------------------------------------------------------- /Steam/Steam.query.pq: -------------------------------------------------------------------------------- 1 | let 2 | result = Steam.Contents("") 3 | in 4 | result -------------------------------------------------------------------------------- /Steam/Steam16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam16.png -------------------------------------------------------------------------------- /Steam/Steam20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam20.png -------------------------------------------------------------------------------- /Steam/Steam24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam24.png -------------------------------------------------------------------------------- /Steam/Steam32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam32.png -------------------------------------------------------------------------------- /Steam/Steam40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam40.png -------------------------------------------------------------------------------- /Steam/Steam48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam48.png -------------------------------------------------------------------------------- /Steam/Steam64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam64.png -------------------------------------------------------------------------------- /Steam/Steam80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmasson/PowerQuery/f6b1cdb8e4fdbb1f24137c8d4cfcccae60bbb815/Steam/Steam80.png -------------------------------------------------------------------------------- /Steam/apikey: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Steam/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Steam 122 | 123 | 124 | Steam 125 | 126 | 127 | Steam 128 | 129 | --------------------------------------------------------------------------------