@luquimbo/bi-superpowers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +25 -0
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/bin/build-plugin.js +30 -0
- package/bin/cli.js +1064 -0
- package/bin/commands/add.js +533 -0
- package/bin/commands/add.test.js +77 -0
- package/bin/commands/build-desktop.js +166 -0
- package/bin/commands/changelog.js +443 -0
- package/bin/commands/diff.js +325 -0
- package/bin/commands/lint.js +419 -0
- package/bin/commands/lint.test.js +103 -0
- package/bin/commands/mcp-setup.js +246 -0
- package/bin/commands/pull.js +287 -0
- package/bin/commands/pull.test.js +36 -0
- package/bin/commands/push.js +231 -0
- package/bin/commands/push.test.js +14 -0
- package/bin/commands/search.js +344 -0
- package/bin/commands/search.test.js +115 -0
- package/bin/commands/setup.js +545 -0
- package/bin/commands/setup.test.js +46 -0
- package/bin/commands/sync-profile.js +405 -0
- package/bin/commands/sync-profile.test.js +14 -0
- package/bin/commands/sync-source.js +418 -0
- package/bin/commands/sync-source.test.js +14 -0
- package/bin/commands/watch.js +206 -0
- package/bin/lib/generators/claude-plugin.js +266 -0
- package/bin/lib/generators/claude-plugin.test.js +110 -0
- package/bin/lib/generators/index.js +116 -0
- package/bin/lib/generators/shared.js +282 -0
- package/bin/lib/licensing/index.js +35 -0
- package/bin/lib/licensing/storage.js +364 -0
- package/bin/lib/licensing/storage.test.js +55 -0
- package/bin/lib/licensing/validator.js +213 -0
- package/bin/lib/licensing/validator.test.js +137 -0
- package/bin/lib/microsoft-mcp.js +176 -0
- package/bin/lib/microsoft-mcp.test.js +106 -0
- package/bin/lib/skills.js +84 -0
- package/bin/mcp/powerbi-modeling-launcher.js +38 -0
- package/bin/postinstall.js +44 -0
- package/bin/utils/errors.js +159 -0
- package/bin/utils/git.js +298 -0
- package/bin/utils/logger.js +142 -0
- package/bin/utils/mcp-detect.js +274 -0
- package/bin/utils/mcp-detect.test.js +105 -0
- package/bin/utils/pbix.js +305 -0
- package/bin/utils/pbix.test.js +37 -0
- package/bin/utils/profiles.js +312 -0
- package/bin/utils/projects.js +168 -0
- package/bin/utils/readline.js +206 -0
- package/bin/utils/readline.test.js +47 -0
- package/bin/utils/tui.js +314 -0
- package/bin/utils/tui.test.js +127 -0
- package/commands/contributions.md +265 -0
- package/commands/data-model-design.md +468 -0
- package/commands/dax-doctor.md +248 -0
- package/commands/fabric-scripts.md +452 -0
- package/commands/migration-assistant.md +290 -0
- package/commands/model-documenter.md +242 -0
- package/commands/pbi-connect.md +239 -0
- package/commands/project-kickoff.md +905 -0
- package/commands/report-layout.md +296 -0
- package/commands/rls-design.md +533 -0
- package/commands/theme-tweaker.md +624 -0
- package/config.example.json +23 -0
- package/config.json +23 -0
- package/desktop-extension/manifest.json +37 -0
- package/desktop-extension/package.json +10 -0
- package/desktop-extension/server.js +95 -0
- package/docs/openrouter-free-models.md +92 -0
- package/library/examples/README.md +151 -0
- package/library/examples/finance-reporting/README.md +351 -0
- package/library/examples/finance-reporting/data-model.md +267 -0
- package/library/examples/finance-reporting/measures.dax +557 -0
- package/library/examples/hr-analytics/README.md +371 -0
- package/library/examples/hr-analytics/data-model.md +315 -0
- package/library/examples/hr-analytics/measures.dax +460 -0
- package/library/examples/marketing-analytics/README.md +37 -0
- package/library/examples/marketing-analytics/data-model.md +62 -0
- package/library/examples/marketing-analytics/measures.dax +110 -0
- package/library/examples/retail-analytics/README.md +439 -0
- package/library/examples/retail-analytics/data-model.md +288 -0
- package/library/examples/retail-analytics/measures.dax +481 -0
- package/library/examples/supply-chain/README.md +37 -0
- package/library/examples/supply-chain/data-model.md +69 -0
- package/library/examples/supply-chain/measures.dax +77 -0
- package/library/examples/udf-library/README.md +228 -0
- package/library/examples/udf-library/functions.dax +571 -0
- package/library/snippets/dax/README.md +292 -0
- package/library/snippets/dax/business-domains.md +576 -0
- package/library/snippets/dax/calculate-patterns.md +276 -0
- package/library/snippets/dax/calculation-groups.md +489 -0
- package/library/snippets/dax/error-handling.md +495 -0
- package/library/snippets/dax/iterators-and-aggregations.md +474 -0
- package/library/snippets/dax/kpis-and-metrics.md +293 -0
- package/library/snippets/dax/rankings-and-topn.md +235 -0
- package/library/snippets/dax/security-patterns.md +413 -0
- package/library/snippets/dax/text-and-formatting.md +316 -0
- package/library/snippets/dax/time-intelligence.md +196 -0
- package/library/snippets/dax/user-defined-functions.md +477 -0
- package/library/snippets/dax/virtual-tables.md +546 -0
- package/library/snippets/excel-formulas/README.md +84 -0
- package/library/snippets/excel-formulas/aggregations.md +330 -0
- package/library/snippets/excel-formulas/dates-and-times.md +361 -0
- package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
- package/library/snippets/excel-formulas/lookups.md +169 -0
- package/library/snippets/excel-formulas/text-functions.md +363 -0
- package/library/snippets/governance/naming-conventions.md +97 -0
- package/library/snippets/governance/review-checklists.md +107 -0
- package/library/snippets/power-query/README.md +389 -0
- package/library/snippets/power-query/api-integration.md +707 -0
- package/library/snippets/power-query/connections.md +434 -0
- package/library/snippets/power-query/data-cleaning.md +298 -0
- package/library/snippets/power-query/error-handling.md +526 -0
- package/library/snippets/power-query/parameters.md +350 -0
- package/library/snippets/power-query/performance.md +506 -0
- package/library/snippets/power-query/transformations.md +330 -0
- package/library/snippets/report-design/accessibility.md +78 -0
- package/library/snippets/report-design/chart-selection.md +54 -0
- package/library/snippets/report-design/layout-patterns.md +87 -0
- package/library/templates/data-models/README.md +93 -0
- package/library/templates/data-models/finance-model.md +627 -0
- package/library/templates/data-models/retail-star-schema.md +473 -0
- package/library/templates/excel/README.md +83 -0
- package/library/templates/excel/budget-tracker.md +432 -0
- package/library/templates/excel/data-entry-form.md +533 -0
- package/library/templates/power-bi/README.md +72 -0
- package/library/templates/power-bi/finance-report.md +449 -0
- package/library/templates/power-bi/kpi-scorecard.md +461 -0
- package/library/templates/power-bi/sales-dashboard.md +281 -0
- package/library/themes/excel/README.md +436 -0
- package/library/themes/power-bi/README.md +271 -0
- package/library/themes/power-bi/accessible.json +307 -0
- package/library/themes/power-bi/bi-superpowers-default.json +858 -0
- package/library/themes/power-bi/corporate-blue.json +291 -0
- package/library/themes/power-bi/dark-mode.json +291 -0
- package/library/themes/power-bi/minimal.json +292 -0
- package/library/themes/power-bi/print-friendly.json +309 -0
- package/package.json +93 -0
- package/skills/contributions/SKILL.md +267 -0
- package/skills/data-model-design/SKILL.md +470 -0
- package/skills/data-modeling/SKILL.md +254 -0
- package/skills/data-quality/SKILL.md +664 -0
- package/skills/dax/SKILL.md +708 -0
- package/skills/dax-doctor/SKILL.md +250 -0
- package/skills/dax-udf/SKILL.md +489 -0
- package/skills/deployment/SKILL.md +320 -0
- package/skills/excel-formulas/SKILL.md +463 -0
- package/skills/fabric-scripts/SKILL.md +454 -0
- package/skills/fast-standard/SKILL.md +509 -0
- package/skills/governance/SKILL.md +205 -0
- package/skills/migration-assistant/SKILL.md +292 -0
- package/skills/model-documenter/SKILL.md +244 -0
- package/skills/pbi-connect/SKILL.md +241 -0
- package/skills/power-query/SKILL.md +406 -0
- package/skills/project-kickoff/SKILL.md +907 -0
- package/skills/query-performance/SKILL.md +480 -0
- package/skills/report-design/SKILL.md +207 -0
- package/skills/report-layout/SKILL.md +298 -0
- package/skills/rls-design/SKILL.md +535 -0
- package/skills/semantic-model/SKILL.md +237 -0
- package/skills/testing-validation/SKILL.md +643 -0
- package/skills/theme-tweaker/SKILL.md +626 -0
- package/src/content/base.md +237 -0
- package/src/content/mcp-requirements.json +69 -0
- package/src/content/routing.md +203 -0
- package/src/content/skills/contributions.md +259 -0
- package/src/content/skills/data-model-design.md +462 -0
- package/src/content/skills/data-modeling.md +246 -0
- package/src/content/skills/data-quality.md +656 -0
- package/src/content/skills/dax-doctor.md +242 -0
- package/src/content/skills/dax-udf.md +481 -0
- package/src/content/skills/dax.md +700 -0
- package/src/content/skills/deployment.md +312 -0
- package/src/content/skills/excel-formulas.md +455 -0
- package/src/content/skills/fabric-scripts.md +446 -0
- package/src/content/skills/fast-standard.md +501 -0
- package/src/content/skills/governance.md +197 -0
- package/src/content/skills/migration-assistant.md +284 -0
- package/src/content/skills/model-documenter.md +236 -0
- package/src/content/skills/pbi-connect.md +233 -0
- package/src/content/skills/power-query.md +398 -0
- package/src/content/skills/project-kickoff.md +899 -0
- package/src/content/skills/query-performance.md +472 -0
- package/src/content/skills/report-design.md +199 -0
- package/src/content/skills/report-layout.md +290 -0
- package/src/content/skills/rls-design.md +527 -0
- package/src/content/skills/semantic-model.md +229 -0
- package/src/content/skills/testing-validation.md +635 -0
- package/src/content/skills/theme-tweaker.md +618 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
# API Integration in Power Query
|
|
2
|
+
|
|
3
|
+
Patterns for connecting to REST APIs, handling pagination, authentication, and error handling.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Basic REST API Calls
|
|
8
|
+
|
|
9
|
+
### Simple GET Request
|
|
10
|
+
|
|
11
|
+
```m
|
|
12
|
+
let
|
|
13
|
+
// Basic API call
|
|
14
|
+
Source = Json.Document(
|
|
15
|
+
Web.Contents("https://api.example.com/data")
|
|
16
|
+
),
|
|
17
|
+
|
|
18
|
+
// Convert to table
|
|
19
|
+
ToTable = Table.FromRecords(Source)
|
|
20
|
+
in
|
|
21
|
+
ToTable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### GET with Query Parameters
|
|
25
|
+
|
|
26
|
+
```m
|
|
27
|
+
let
|
|
28
|
+
BaseUrl = "https://api.example.com/data",
|
|
29
|
+
QueryParams = [
|
|
30
|
+
startDate = "2024-01-01",
|
|
31
|
+
endDate = "2024-12-31",
|
|
32
|
+
limit = "1000"
|
|
33
|
+
],
|
|
34
|
+
|
|
35
|
+
Source = Json.Document(
|
|
36
|
+
Web.Contents(BaseUrl, [Query = QueryParams])
|
|
37
|
+
)
|
|
38
|
+
in
|
|
39
|
+
Source
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### GET with Headers
|
|
43
|
+
|
|
44
|
+
```m
|
|
45
|
+
let
|
|
46
|
+
Source = Json.Document(
|
|
47
|
+
Web.Contents(
|
|
48
|
+
"https://api.example.com/data",
|
|
49
|
+
[
|
|
50
|
+
Headers = [
|
|
51
|
+
#"Content-Type" = "application/json",
|
|
52
|
+
#"Accept" = "application/json",
|
|
53
|
+
#"X-Custom-Header" = "value"
|
|
54
|
+
]
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
in
|
|
59
|
+
Source
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Authentication Patterns
|
|
65
|
+
|
|
66
|
+
### API Key in Header
|
|
67
|
+
|
|
68
|
+
```m
|
|
69
|
+
let
|
|
70
|
+
ApiKey = "your-api-key-here", // Use parameter in production
|
|
71
|
+
|
|
72
|
+
Source = Json.Document(
|
|
73
|
+
Web.Contents(
|
|
74
|
+
"https://api.example.com/data",
|
|
75
|
+
[
|
|
76
|
+
Headers = [
|
|
77
|
+
#"Authorization" = "Bearer " & ApiKey,
|
|
78
|
+
#"Content-Type" = "application/json"
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
in
|
|
84
|
+
Source
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### API Key in Query String
|
|
88
|
+
|
|
89
|
+
```m
|
|
90
|
+
let
|
|
91
|
+
ApiKey = "your-api-key-here",
|
|
92
|
+
|
|
93
|
+
Source = Json.Document(
|
|
94
|
+
Web.Contents(
|
|
95
|
+
"https://api.example.com/data",
|
|
96
|
+
[
|
|
97
|
+
Query = [
|
|
98
|
+
api_key = ApiKey,
|
|
99
|
+
format = "json"
|
|
100
|
+
]
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
in
|
|
105
|
+
Source
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Basic Authentication
|
|
109
|
+
|
|
110
|
+
```m
|
|
111
|
+
let
|
|
112
|
+
Username = "user",
|
|
113
|
+
Password = "pass",
|
|
114
|
+
|
|
115
|
+
// Encode credentials
|
|
116
|
+
Credentials = Binary.ToText(
|
|
117
|
+
Text.ToBinary(Username & ":" & Password),
|
|
118
|
+
BinaryEncoding.Base64
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
Source = Json.Document(
|
|
122
|
+
Web.Contents(
|
|
123
|
+
"https://api.example.com/data",
|
|
124
|
+
[
|
|
125
|
+
Headers = [
|
|
126
|
+
#"Authorization" = "Basic " & Credentials
|
|
127
|
+
]
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
in
|
|
132
|
+
Source
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### OAuth Token (Pre-obtained)
|
|
136
|
+
|
|
137
|
+
```m
|
|
138
|
+
let
|
|
139
|
+
// Token obtained separately (e.g., via Azure AD)
|
|
140
|
+
AccessToken = "eyJhbGciOiJSUzI1NiIs...",
|
|
141
|
+
|
|
142
|
+
Source = Json.Document(
|
|
143
|
+
Web.Contents(
|
|
144
|
+
"https://api.example.com/data",
|
|
145
|
+
[
|
|
146
|
+
Headers = [
|
|
147
|
+
#"Authorization" = "Bearer " & AccessToken
|
|
148
|
+
]
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
in
|
|
153
|
+
Source
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Pagination Patterns
|
|
159
|
+
|
|
160
|
+
### Offset-Based Pagination
|
|
161
|
+
|
|
162
|
+
```m
|
|
163
|
+
let
|
|
164
|
+
BaseUrl = "https://api.example.com/data",
|
|
165
|
+
PageSize = 100,
|
|
166
|
+
|
|
167
|
+
// Function to get one page
|
|
168
|
+
GetPage = (offset as number) =>
|
|
169
|
+
let
|
|
170
|
+
Response = Json.Document(
|
|
171
|
+
Web.Contents(
|
|
172
|
+
BaseUrl,
|
|
173
|
+
[Query = [
|
|
174
|
+
offset = Text.From(offset),
|
|
175
|
+
limit = Text.From(PageSize)
|
|
176
|
+
]]
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
in
|
|
180
|
+
Response[data],
|
|
181
|
+
|
|
182
|
+
// Get first page to check total
|
|
183
|
+
FirstPage = GetPage(0),
|
|
184
|
+
TotalCount = Json.Document(
|
|
185
|
+
Web.Contents(BaseUrl, [Query = [limit = "1"]])
|
|
186
|
+
)[total],
|
|
187
|
+
|
|
188
|
+
// Generate list of offsets
|
|
189
|
+
Offsets = List.Generate(
|
|
190
|
+
() => 0,
|
|
191
|
+
each _ < TotalCount,
|
|
192
|
+
each _ + PageSize
|
|
193
|
+
),
|
|
194
|
+
|
|
195
|
+
// Get all pages
|
|
196
|
+
AllPages = List.Transform(Offsets, each GetPage(_)),
|
|
197
|
+
|
|
198
|
+
// Combine into single table
|
|
199
|
+
Combined = Table.FromList(
|
|
200
|
+
List.Combine(AllPages),
|
|
201
|
+
Splitter.SplitByNothing(),
|
|
202
|
+
{"Column1"}
|
|
203
|
+
),
|
|
204
|
+
|
|
205
|
+
Expanded = Table.ExpandRecordColumn(Combined, "Column1", Record.FieldNames(AllPages{0}{0}))
|
|
206
|
+
in
|
|
207
|
+
Expanded
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Page Number Pagination
|
|
211
|
+
|
|
212
|
+
```m
|
|
213
|
+
let
|
|
214
|
+
BaseUrl = "https://api.example.com/data",
|
|
215
|
+
|
|
216
|
+
// Function to get one page
|
|
217
|
+
GetPage = (pageNum as number) =>
|
|
218
|
+
let
|
|
219
|
+
Response = Json.Document(
|
|
220
|
+
Web.Contents(
|
|
221
|
+
BaseUrl,
|
|
222
|
+
[Query = [page = Text.From(pageNum)]]
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
in
|
|
226
|
+
Response,
|
|
227
|
+
|
|
228
|
+
// Get first page
|
|
229
|
+
FirstPage = GetPage(1),
|
|
230
|
+
TotalPages = FirstPage[totalPages],
|
|
231
|
+
|
|
232
|
+
// Generate page numbers
|
|
233
|
+
PageNumbers = {1..TotalPages},
|
|
234
|
+
|
|
235
|
+
// Get all pages
|
|
236
|
+
AllResponses = List.Transform(PageNumbers, each GetPage(_)),
|
|
237
|
+
|
|
238
|
+
// Extract data from each response
|
|
239
|
+
AllData = List.Transform(AllResponses, each _[data]),
|
|
240
|
+
|
|
241
|
+
// Combine
|
|
242
|
+
Combined = List.Combine(AllData),
|
|
243
|
+
ToTable = Table.FromRecords(Combined)
|
|
244
|
+
in
|
|
245
|
+
ToTable
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Cursor-Based Pagination
|
|
249
|
+
|
|
250
|
+
```m
|
|
251
|
+
let
|
|
252
|
+
BaseUrl = "https://api.example.com/data",
|
|
253
|
+
|
|
254
|
+
// Recursive function for cursor pagination
|
|
255
|
+
GetAllPages = (cursor as nullable text, accumulator as list) =>
|
|
256
|
+
let
|
|
257
|
+
QueryParams = if cursor = null then [] else [cursor = cursor],
|
|
258
|
+
Response = Json.Document(
|
|
259
|
+
Web.Contents(BaseUrl, [Query = QueryParams])
|
|
260
|
+
),
|
|
261
|
+
CurrentData = Response[data],
|
|
262
|
+
NextCursor = try Response[next_cursor] otherwise null,
|
|
263
|
+
NewAccumulator = accumulator & CurrentData,
|
|
264
|
+
|
|
265
|
+
Result = if NextCursor = null then
|
|
266
|
+
NewAccumulator
|
|
267
|
+
else
|
|
268
|
+
@GetAllPages(NextCursor, NewAccumulator)
|
|
269
|
+
in
|
|
270
|
+
Result,
|
|
271
|
+
|
|
272
|
+
AllData = GetAllPages(null, {}),
|
|
273
|
+
ToTable = Table.FromRecords(AllData)
|
|
274
|
+
in
|
|
275
|
+
ToTable
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Link-Based Pagination (HATEOAS)
|
|
279
|
+
|
|
280
|
+
```m
|
|
281
|
+
let
|
|
282
|
+
// Function to follow next links
|
|
283
|
+
GetAllPages = (url as text, accumulator as list) =>
|
|
284
|
+
let
|
|
285
|
+
Response = Json.Document(Web.Contents(url)),
|
|
286
|
+
CurrentData = Response[results],
|
|
287
|
+
NextUrl = try Response[next] otherwise null,
|
|
288
|
+
NewAccumulator = accumulator & CurrentData,
|
|
289
|
+
|
|
290
|
+
Result = if NextUrl = null then
|
|
291
|
+
NewAccumulator
|
|
292
|
+
else
|
|
293
|
+
@GetAllPages(NextUrl, NewAccumulator)
|
|
294
|
+
in
|
|
295
|
+
Result,
|
|
296
|
+
|
|
297
|
+
StartUrl = "https://api.example.com/data",
|
|
298
|
+
AllData = GetAllPages(StartUrl, {}),
|
|
299
|
+
ToTable = Table.FromRecords(AllData)
|
|
300
|
+
in
|
|
301
|
+
ToTable
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## POST Requests
|
|
307
|
+
|
|
308
|
+
### POST with JSON Body
|
|
309
|
+
|
|
310
|
+
```m
|
|
311
|
+
let
|
|
312
|
+
Url = "https://api.example.com/query",
|
|
313
|
+
|
|
314
|
+
// Request body
|
|
315
|
+
Body = Json.FromValue([
|
|
316
|
+
startDate = "2024-01-01",
|
|
317
|
+
endDate = "2024-12-31",
|
|
318
|
+
filters = [
|
|
319
|
+
status = "active",
|
|
320
|
+
region = "US"
|
|
321
|
+
]
|
|
322
|
+
]),
|
|
323
|
+
|
|
324
|
+
Source = Json.Document(
|
|
325
|
+
Web.Contents(
|
|
326
|
+
Url,
|
|
327
|
+
[
|
|
328
|
+
Headers = [
|
|
329
|
+
#"Content-Type" = "application/json"
|
|
330
|
+
],
|
|
331
|
+
Content = Body
|
|
332
|
+
]
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
in
|
|
336
|
+
Source
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### POST with Form Data
|
|
340
|
+
|
|
341
|
+
```m
|
|
342
|
+
let
|
|
343
|
+
Url = "https://api.example.com/token",
|
|
344
|
+
|
|
345
|
+
// URL-encoded form data
|
|
346
|
+
FormData = Text.ToBinary(
|
|
347
|
+
Uri.BuildQueryString([
|
|
348
|
+
grant_type = "client_credentials",
|
|
349
|
+
client_id = "your-client-id",
|
|
350
|
+
client_secret = "your-client-secret"
|
|
351
|
+
])
|
|
352
|
+
),
|
|
353
|
+
|
|
354
|
+
Source = Json.Document(
|
|
355
|
+
Web.Contents(
|
|
356
|
+
Url,
|
|
357
|
+
[
|
|
358
|
+
Headers = [
|
|
359
|
+
#"Content-Type" = "application/x-www-form-urlencoded"
|
|
360
|
+
],
|
|
361
|
+
Content = FormData
|
|
362
|
+
]
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
in
|
|
366
|
+
Source
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Error Handling
|
|
372
|
+
|
|
373
|
+
### Basic Error Handling
|
|
374
|
+
|
|
375
|
+
```m
|
|
376
|
+
let
|
|
377
|
+
SafeApiCall = (url as text) =>
|
|
378
|
+
let
|
|
379
|
+
Response = try Json.Document(Web.Contents(url))
|
|
380
|
+
in
|
|
381
|
+
if Response[HasError] then
|
|
382
|
+
[
|
|
383
|
+
success = false,
|
|
384
|
+
error = Response[Error][Message],
|
|
385
|
+
data = null
|
|
386
|
+
]
|
|
387
|
+
else
|
|
388
|
+
[
|
|
389
|
+
success = true,
|
|
390
|
+
error = null,
|
|
391
|
+
data = Response[Value]
|
|
392
|
+
],
|
|
393
|
+
|
|
394
|
+
Result = SafeApiCall("https://api.example.com/data")
|
|
395
|
+
in
|
|
396
|
+
Result
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Retry with Backoff
|
|
400
|
+
|
|
401
|
+
```m
|
|
402
|
+
let
|
|
403
|
+
// Retry function with exponential backoff
|
|
404
|
+
RetryRequest = (url as text, maxRetries as number, retryCount as number) =>
|
|
405
|
+
let
|
|
406
|
+
Response = try Json.Document(Web.Contents(url))
|
|
407
|
+
in
|
|
408
|
+
if Response[HasError] then
|
|
409
|
+
if retryCount >= maxRetries then
|
|
410
|
+
error Response[Error]
|
|
411
|
+
else
|
|
412
|
+
let
|
|
413
|
+
// Wait before retry (simplified - actual delay not supported in M)
|
|
414
|
+
_ = Function.InvokeAfter(
|
|
415
|
+
() => null,
|
|
416
|
+
#duration(0, 0, 0, Number.Power(2, retryCount))
|
|
417
|
+
)
|
|
418
|
+
in
|
|
419
|
+
@RetryRequest(url, maxRetries, retryCount + 1)
|
|
420
|
+
else
|
|
421
|
+
Response[Value],
|
|
422
|
+
|
|
423
|
+
Result = RetryRequest("https://api.example.com/data", 3, 0)
|
|
424
|
+
in
|
|
425
|
+
Result
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Handle HTTP Status Codes
|
|
429
|
+
|
|
430
|
+
```m
|
|
431
|
+
let
|
|
432
|
+
Url = "https://api.example.com/data",
|
|
433
|
+
|
|
434
|
+
Response = Web.Contents(
|
|
435
|
+
Url,
|
|
436
|
+
[
|
|
437
|
+
ManualStatusHandling = {400, 401, 403, 404, 500}
|
|
438
|
+
]
|
|
439
|
+
),
|
|
440
|
+
|
|
441
|
+
Metadata = Value.Metadata(Response),
|
|
442
|
+
StatusCode = Metadata[Response.Status],
|
|
443
|
+
|
|
444
|
+
Result = if StatusCode = 200 then
|
|
445
|
+
Json.Document(Response)
|
|
446
|
+
else if StatusCode = 401 then
|
|
447
|
+
error Error.Record("Authentication Error", "Invalid credentials")
|
|
448
|
+
else if StatusCode = 404 then
|
|
449
|
+
error Error.Record("Not Found", "Resource not found")
|
|
450
|
+
else if StatusCode >= 500 then
|
|
451
|
+
error Error.Record("Server Error", "API server error: " & Text.From(StatusCode))
|
|
452
|
+
else
|
|
453
|
+
error Error.Record("API Error", "Unexpected status: " & Text.From(StatusCode))
|
|
454
|
+
in
|
|
455
|
+
Result
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Rate Limiting
|
|
461
|
+
|
|
462
|
+
### Delay Between Requests
|
|
463
|
+
|
|
464
|
+
```m
|
|
465
|
+
let
|
|
466
|
+
BaseUrl = "https://api.example.com/data",
|
|
467
|
+
Endpoints = {"users", "orders", "products"},
|
|
468
|
+
|
|
469
|
+
// Function with delay
|
|
470
|
+
GetWithDelay = (endpoint as text, index as number) =>
|
|
471
|
+
let
|
|
472
|
+
// Delay increases with index
|
|
473
|
+
_ = Function.InvokeAfter(
|
|
474
|
+
() => null,
|
|
475
|
+
#duration(0, 0, 0, index * 0.5) // 0.5 second delay per request
|
|
476
|
+
),
|
|
477
|
+
Response = Json.Document(
|
|
478
|
+
Web.Contents(BaseUrl & "/" & endpoint)
|
|
479
|
+
)
|
|
480
|
+
in
|
|
481
|
+
Response,
|
|
482
|
+
|
|
483
|
+
// Process with index for delay
|
|
484
|
+
Results = List.Transform(
|
|
485
|
+
List.Zip({Endpoints, {0..List.Count(Endpoints)-1}}),
|
|
486
|
+
each GetWithDelay(_{0}, _{1})
|
|
487
|
+
)
|
|
488
|
+
in
|
|
489
|
+
Results
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Respect Retry-After Header
|
|
493
|
+
|
|
494
|
+
```m
|
|
495
|
+
let
|
|
496
|
+
Url = "https://api.example.com/data",
|
|
497
|
+
|
|
498
|
+
Response = Web.Contents(
|
|
499
|
+
Url,
|
|
500
|
+
[ManualStatusHandling = {429}]
|
|
501
|
+
),
|
|
502
|
+
|
|
503
|
+
Metadata = Value.Metadata(Response),
|
|
504
|
+
StatusCode = Metadata[Response.Status],
|
|
505
|
+
|
|
506
|
+
Result = if StatusCode = 429 then
|
|
507
|
+
let
|
|
508
|
+
RetryAfter = try Number.From(Metadata[Headers][#"Retry-After"]) otherwise 60,
|
|
509
|
+
_ = Function.InvokeAfter(() => null, #duration(0, 0, 0, RetryAfter))
|
|
510
|
+
in
|
|
511
|
+
Json.Document(Web.Contents(Url)) // Retry
|
|
512
|
+
else
|
|
513
|
+
Json.Document(Response)
|
|
514
|
+
in
|
|
515
|
+
Result
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Common API Patterns
|
|
521
|
+
|
|
522
|
+
### REST API with Nested JSON
|
|
523
|
+
|
|
524
|
+
```m
|
|
525
|
+
let
|
|
526
|
+
Source = Json.Document(
|
|
527
|
+
Web.Contents("https://api.example.com/orders")
|
|
528
|
+
),
|
|
529
|
+
|
|
530
|
+
// Expand nested records
|
|
531
|
+
Orders = Source[data],
|
|
532
|
+
ToTable = Table.FromRecords(Orders),
|
|
533
|
+
|
|
534
|
+
// Expand nested customer object
|
|
535
|
+
ExpandedCustomer = Table.ExpandRecordColumn(
|
|
536
|
+
ToTable, "customer",
|
|
537
|
+
{"id", "name", "email"},
|
|
538
|
+
{"customer_id", "customer_name", "customer_email"}
|
|
539
|
+
),
|
|
540
|
+
|
|
541
|
+
// Expand nested line items list
|
|
542
|
+
ExpandedItems = Table.ExpandListColumn(ExpandedCustomer, "line_items"),
|
|
543
|
+
ExpandedItemDetails = Table.ExpandRecordColumn(
|
|
544
|
+
ExpandedItems, "line_items",
|
|
545
|
+
{"product_id", "quantity", "price"},
|
|
546
|
+
{"item_product_id", "item_quantity", "item_price"}
|
|
547
|
+
)
|
|
548
|
+
in
|
|
549
|
+
ExpandedItemDetails
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### GraphQL API
|
|
553
|
+
|
|
554
|
+
```m
|
|
555
|
+
let
|
|
556
|
+
Url = "https://api.example.com/graphql",
|
|
557
|
+
|
|
558
|
+
Query = "
|
|
559
|
+
query GetOrders($startDate: String!) {
|
|
560
|
+
orders(startDate: $startDate) {
|
|
561
|
+
id
|
|
562
|
+
date
|
|
563
|
+
total
|
|
564
|
+
customer {
|
|
565
|
+
name
|
|
566
|
+
email
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
",
|
|
571
|
+
|
|
572
|
+
Variables = [startDate = "2024-01-01"],
|
|
573
|
+
|
|
574
|
+
Body = Json.FromValue([
|
|
575
|
+
query = Query,
|
|
576
|
+
variables = Variables
|
|
577
|
+
]),
|
|
578
|
+
|
|
579
|
+
Response = Json.Document(
|
|
580
|
+
Web.Contents(
|
|
581
|
+
Url,
|
|
582
|
+
[
|
|
583
|
+
Headers = [#"Content-Type" = "application/json"],
|
|
584
|
+
Content = Body
|
|
585
|
+
]
|
|
586
|
+
)
|
|
587
|
+
),
|
|
588
|
+
|
|
589
|
+
Data = Response[data][orders],
|
|
590
|
+
ToTable = Table.FromRecords(Data)
|
|
591
|
+
in
|
|
592
|
+
ToTable
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### OData API
|
|
596
|
+
|
|
597
|
+
```m
|
|
598
|
+
let
|
|
599
|
+
// OData is well-supported natively
|
|
600
|
+
Source = OData.Feed(
|
|
601
|
+
"https://services.odata.org/V4/Northwind/Northwind.svc/",
|
|
602
|
+
null,
|
|
603
|
+
[
|
|
604
|
+
Query = [
|
|
605
|
+
#"$filter" = "Country eq 'USA'",
|
|
606
|
+
#"$select" = "CustomerID,CompanyName,City",
|
|
607
|
+
#"$top" = "100"
|
|
608
|
+
]
|
|
609
|
+
]
|
|
610
|
+
)
|
|
611
|
+
in
|
|
612
|
+
Source
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Best Practices
|
|
618
|
+
|
|
619
|
+
### 1. Use Parameters for URLs and Keys
|
|
620
|
+
|
|
621
|
+
```m
|
|
622
|
+
let
|
|
623
|
+
// Reference parameters instead of hardcoding
|
|
624
|
+
BaseUrl = BaseUrlParameter,
|
|
625
|
+
ApiKey = ApiKeyParameter,
|
|
626
|
+
|
|
627
|
+
Source = Json.Document(
|
|
628
|
+
Web.Contents(
|
|
629
|
+
BaseUrl,
|
|
630
|
+
[Headers = [#"Authorization" = "Bearer " & ApiKey]]
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
in
|
|
634
|
+
Source
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### 2. Enable Query Folding Where Possible
|
|
638
|
+
|
|
639
|
+
```m
|
|
640
|
+
let
|
|
641
|
+
// Use RelativePath for consistent base URL
|
|
642
|
+
// This helps with caching and query folding
|
|
643
|
+
Source = Json.Document(
|
|
644
|
+
Web.Contents(
|
|
645
|
+
"https://api.example.com",
|
|
646
|
+
[RelativePath = "/data/orders"]
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
in
|
|
650
|
+
Source
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 3. Handle Empty Responses
|
|
654
|
+
|
|
655
|
+
```m
|
|
656
|
+
let
|
|
657
|
+
Response = Json.Document(Web.Contents(Url)),
|
|
658
|
+
Data = try Response[data] otherwise {},
|
|
659
|
+
ToTable = if Data = {} or Data = null then
|
|
660
|
+
#table({"Column1"}, {}) // Empty table with schema
|
|
661
|
+
else
|
|
662
|
+
Table.FromRecords(Data)
|
|
663
|
+
in
|
|
664
|
+
ToTable
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### 4. Log API Calls (Debug)
|
|
668
|
+
|
|
669
|
+
```m
|
|
670
|
+
let
|
|
671
|
+
Url = "https://api.example.com/data",
|
|
672
|
+
|
|
673
|
+
// Log the URL being called
|
|
674
|
+
_ = Diagnostics.Trace(
|
|
675
|
+
TraceLevel.Information,
|
|
676
|
+
"Calling API: " & Url,
|
|
677
|
+
() => true,
|
|
678
|
+
true
|
|
679
|
+
),
|
|
680
|
+
|
|
681
|
+
Response = Json.Document(Web.Contents(Url))
|
|
682
|
+
in
|
|
683
|
+
Response
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## Troubleshooting
|
|
689
|
+
|
|
690
|
+
| Issue | Cause | Solution |
|
|
691
|
+
|-------|-------|----------|
|
|
692
|
+
| "Access denied" | CORS or auth issue | Check credentials, use Web.Contents options |
|
|
693
|
+
| "Formula firewall" | Privacy levels | Set privacy to Public/Organizational |
|
|
694
|
+
| Timeout | Slow API | Increase timeout, paginate smaller |
|
|
695
|
+
| "Invalid JSON" | Not JSON response | Check response content-type |
|
|
696
|
+
| Rate limited | Too many requests | Add delays, implement retry |
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## Security Checklist
|
|
701
|
+
|
|
702
|
+
- [ ] Never hardcode credentials in queries
|
|
703
|
+
- [ ] Use parameters for API keys
|
|
704
|
+
- [ ] Store secrets in Azure Key Vault or parameter
|
|
705
|
+
- [ ] Review data gateway requirements
|
|
706
|
+
- [ ] Test with minimum required permissions
|
|
707
|
+
- [ ] Document API rate limits
|