@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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +25 -0
  3. package/AGENTS.md +244 -0
  4. package/CHANGELOG.md +265 -0
  5. package/LICENSE +21 -0
  6. package/README.md +211 -0
  7. package/bin/build-plugin.js +30 -0
  8. package/bin/cli.js +1064 -0
  9. package/bin/commands/add.js +533 -0
  10. package/bin/commands/add.test.js +77 -0
  11. package/bin/commands/build-desktop.js +166 -0
  12. package/bin/commands/changelog.js +443 -0
  13. package/bin/commands/diff.js +325 -0
  14. package/bin/commands/lint.js +419 -0
  15. package/bin/commands/lint.test.js +103 -0
  16. package/bin/commands/mcp-setup.js +246 -0
  17. package/bin/commands/pull.js +287 -0
  18. package/bin/commands/pull.test.js +36 -0
  19. package/bin/commands/push.js +231 -0
  20. package/bin/commands/push.test.js +14 -0
  21. package/bin/commands/search.js +344 -0
  22. package/bin/commands/search.test.js +115 -0
  23. package/bin/commands/setup.js +545 -0
  24. package/bin/commands/setup.test.js +46 -0
  25. package/bin/commands/sync-profile.js +405 -0
  26. package/bin/commands/sync-profile.test.js +14 -0
  27. package/bin/commands/sync-source.js +418 -0
  28. package/bin/commands/sync-source.test.js +14 -0
  29. package/bin/commands/watch.js +206 -0
  30. package/bin/lib/generators/claude-plugin.js +266 -0
  31. package/bin/lib/generators/claude-plugin.test.js +110 -0
  32. package/bin/lib/generators/index.js +116 -0
  33. package/bin/lib/generators/shared.js +282 -0
  34. package/bin/lib/licensing/index.js +35 -0
  35. package/bin/lib/licensing/storage.js +364 -0
  36. package/bin/lib/licensing/storage.test.js +55 -0
  37. package/bin/lib/licensing/validator.js +213 -0
  38. package/bin/lib/licensing/validator.test.js +137 -0
  39. package/bin/lib/microsoft-mcp.js +176 -0
  40. package/bin/lib/microsoft-mcp.test.js +106 -0
  41. package/bin/lib/skills.js +84 -0
  42. package/bin/mcp/powerbi-modeling-launcher.js +38 -0
  43. package/bin/postinstall.js +44 -0
  44. package/bin/utils/errors.js +159 -0
  45. package/bin/utils/git.js +298 -0
  46. package/bin/utils/logger.js +142 -0
  47. package/bin/utils/mcp-detect.js +274 -0
  48. package/bin/utils/mcp-detect.test.js +105 -0
  49. package/bin/utils/pbix.js +305 -0
  50. package/bin/utils/pbix.test.js +37 -0
  51. package/bin/utils/profiles.js +312 -0
  52. package/bin/utils/projects.js +168 -0
  53. package/bin/utils/readline.js +206 -0
  54. package/bin/utils/readline.test.js +47 -0
  55. package/bin/utils/tui.js +314 -0
  56. package/bin/utils/tui.test.js +127 -0
  57. package/commands/contributions.md +265 -0
  58. package/commands/data-model-design.md +468 -0
  59. package/commands/dax-doctor.md +248 -0
  60. package/commands/fabric-scripts.md +452 -0
  61. package/commands/migration-assistant.md +290 -0
  62. package/commands/model-documenter.md +242 -0
  63. package/commands/pbi-connect.md +239 -0
  64. package/commands/project-kickoff.md +905 -0
  65. package/commands/report-layout.md +296 -0
  66. package/commands/rls-design.md +533 -0
  67. package/commands/theme-tweaker.md +624 -0
  68. package/config.example.json +23 -0
  69. package/config.json +23 -0
  70. package/desktop-extension/manifest.json +37 -0
  71. package/desktop-extension/package.json +10 -0
  72. package/desktop-extension/server.js +95 -0
  73. package/docs/openrouter-free-models.md +92 -0
  74. package/library/examples/README.md +151 -0
  75. package/library/examples/finance-reporting/README.md +351 -0
  76. package/library/examples/finance-reporting/data-model.md +267 -0
  77. package/library/examples/finance-reporting/measures.dax +557 -0
  78. package/library/examples/hr-analytics/README.md +371 -0
  79. package/library/examples/hr-analytics/data-model.md +315 -0
  80. package/library/examples/hr-analytics/measures.dax +460 -0
  81. package/library/examples/marketing-analytics/README.md +37 -0
  82. package/library/examples/marketing-analytics/data-model.md +62 -0
  83. package/library/examples/marketing-analytics/measures.dax +110 -0
  84. package/library/examples/retail-analytics/README.md +439 -0
  85. package/library/examples/retail-analytics/data-model.md +288 -0
  86. package/library/examples/retail-analytics/measures.dax +481 -0
  87. package/library/examples/supply-chain/README.md +37 -0
  88. package/library/examples/supply-chain/data-model.md +69 -0
  89. package/library/examples/supply-chain/measures.dax +77 -0
  90. package/library/examples/udf-library/README.md +228 -0
  91. package/library/examples/udf-library/functions.dax +571 -0
  92. package/library/snippets/dax/README.md +292 -0
  93. package/library/snippets/dax/business-domains.md +576 -0
  94. package/library/snippets/dax/calculate-patterns.md +276 -0
  95. package/library/snippets/dax/calculation-groups.md +489 -0
  96. package/library/snippets/dax/error-handling.md +495 -0
  97. package/library/snippets/dax/iterators-and-aggregations.md +474 -0
  98. package/library/snippets/dax/kpis-and-metrics.md +293 -0
  99. package/library/snippets/dax/rankings-and-topn.md +235 -0
  100. package/library/snippets/dax/security-patterns.md +413 -0
  101. package/library/snippets/dax/text-and-formatting.md +316 -0
  102. package/library/snippets/dax/time-intelligence.md +196 -0
  103. package/library/snippets/dax/user-defined-functions.md +477 -0
  104. package/library/snippets/dax/virtual-tables.md +546 -0
  105. package/library/snippets/excel-formulas/README.md +84 -0
  106. package/library/snippets/excel-formulas/aggregations.md +330 -0
  107. package/library/snippets/excel-formulas/dates-and-times.md +361 -0
  108. package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
  109. package/library/snippets/excel-formulas/lookups.md +169 -0
  110. package/library/snippets/excel-formulas/text-functions.md +363 -0
  111. package/library/snippets/governance/naming-conventions.md +97 -0
  112. package/library/snippets/governance/review-checklists.md +107 -0
  113. package/library/snippets/power-query/README.md +389 -0
  114. package/library/snippets/power-query/api-integration.md +707 -0
  115. package/library/snippets/power-query/connections.md +434 -0
  116. package/library/snippets/power-query/data-cleaning.md +298 -0
  117. package/library/snippets/power-query/error-handling.md +526 -0
  118. package/library/snippets/power-query/parameters.md +350 -0
  119. package/library/snippets/power-query/performance.md +506 -0
  120. package/library/snippets/power-query/transformations.md +330 -0
  121. package/library/snippets/report-design/accessibility.md +78 -0
  122. package/library/snippets/report-design/chart-selection.md +54 -0
  123. package/library/snippets/report-design/layout-patterns.md +87 -0
  124. package/library/templates/data-models/README.md +93 -0
  125. package/library/templates/data-models/finance-model.md +627 -0
  126. package/library/templates/data-models/retail-star-schema.md +473 -0
  127. package/library/templates/excel/README.md +83 -0
  128. package/library/templates/excel/budget-tracker.md +432 -0
  129. package/library/templates/excel/data-entry-form.md +533 -0
  130. package/library/templates/power-bi/README.md +72 -0
  131. package/library/templates/power-bi/finance-report.md +449 -0
  132. package/library/templates/power-bi/kpi-scorecard.md +461 -0
  133. package/library/templates/power-bi/sales-dashboard.md +281 -0
  134. package/library/themes/excel/README.md +436 -0
  135. package/library/themes/power-bi/README.md +271 -0
  136. package/library/themes/power-bi/accessible.json +307 -0
  137. package/library/themes/power-bi/bi-superpowers-default.json +858 -0
  138. package/library/themes/power-bi/corporate-blue.json +291 -0
  139. package/library/themes/power-bi/dark-mode.json +291 -0
  140. package/library/themes/power-bi/minimal.json +292 -0
  141. package/library/themes/power-bi/print-friendly.json +309 -0
  142. package/package.json +93 -0
  143. package/skills/contributions/SKILL.md +267 -0
  144. package/skills/data-model-design/SKILL.md +470 -0
  145. package/skills/data-modeling/SKILL.md +254 -0
  146. package/skills/data-quality/SKILL.md +664 -0
  147. package/skills/dax/SKILL.md +708 -0
  148. package/skills/dax-doctor/SKILL.md +250 -0
  149. package/skills/dax-udf/SKILL.md +489 -0
  150. package/skills/deployment/SKILL.md +320 -0
  151. package/skills/excel-formulas/SKILL.md +463 -0
  152. package/skills/fabric-scripts/SKILL.md +454 -0
  153. package/skills/fast-standard/SKILL.md +509 -0
  154. package/skills/governance/SKILL.md +205 -0
  155. package/skills/migration-assistant/SKILL.md +292 -0
  156. package/skills/model-documenter/SKILL.md +244 -0
  157. package/skills/pbi-connect/SKILL.md +241 -0
  158. package/skills/power-query/SKILL.md +406 -0
  159. package/skills/project-kickoff/SKILL.md +907 -0
  160. package/skills/query-performance/SKILL.md +480 -0
  161. package/skills/report-design/SKILL.md +207 -0
  162. package/skills/report-layout/SKILL.md +298 -0
  163. package/skills/rls-design/SKILL.md +535 -0
  164. package/skills/semantic-model/SKILL.md +237 -0
  165. package/skills/testing-validation/SKILL.md +643 -0
  166. package/skills/theme-tweaker/SKILL.md +626 -0
  167. package/src/content/base.md +237 -0
  168. package/src/content/mcp-requirements.json +69 -0
  169. package/src/content/routing.md +203 -0
  170. package/src/content/skills/contributions.md +259 -0
  171. package/src/content/skills/data-model-design.md +462 -0
  172. package/src/content/skills/data-modeling.md +246 -0
  173. package/src/content/skills/data-quality.md +656 -0
  174. package/src/content/skills/dax-doctor.md +242 -0
  175. package/src/content/skills/dax-udf.md +481 -0
  176. package/src/content/skills/dax.md +700 -0
  177. package/src/content/skills/deployment.md +312 -0
  178. package/src/content/skills/excel-formulas.md +455 -0
  179. package/src/content/skills/fabric-scripts.md +446 -0
  180. package/src/content/skills/fast-standard.md +501 -0
  181. package/src/content/skills/governance.md +197 -0
  182. package/src/content/skills/migration-assistant.md +284 -0
  183. package/src/content/skills/model-documenter.md +236 -0
  184. package/src/content/skills/pbi-connect.md +233 -0
  185. package/src/content/skills/power-query.md +398 -0
  186. package/src/content/skills/project-kickoff.md +899 -0
  187. package/src/content/skills/query-performance.md +472 -0
  188. package/src/content/skills/report-design.md +199 -0
  189. package/src/content/skills/report-layout.md +290 -0
  190. package/src/content/skills/rls-design.md +527 -0
  191. package/src/content/skills/semantic-model.md +229 -0
  192. package/src/content/skills/testing-validation.md +635 -0
  193. 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