@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,413 @@
1
+ # Security Patterns in DAX
2
+
3
+ Row-Level Security (RLS), Object-Level Security (OLS), and dynamic security patterns.
4
+
5
+ ---
6
+
7
+ ## Row-Level Security (RLS)
8
+
9
+ ### Basic Static RLS
10
+
11
+ ```dax
12
+ // Role: Sales Region - West
13
+ // Filter on Region dimension
14
+ [Region] = "West"
15
+ ```
16
+
17
+ ```dax
18
+ // Role: Single Country
19
+ // Filter on Geography
20
+ [Country] = "USA"
21
+ ```
22
+
23
+ ### User-Based Dynamic RLS
24
+
25
+ ```dax
26
+ // Filter by logged-in user's email
27
+ // Requires DimUser table with Email and allowed regions
28
+ VAR _UserEmail = USERPRINCIPALNAME()
29
+ VAR _AllowedRegions =
30
+ CALCULATETABLE(
31
+ VALUES(DimUserRegion[Region]),
32
+ DimUserRegion[UserEmail] = _UserEmail
33
+ )
34
+ RETURN
35
+ [Region] IN _AllowedRegions
36
+ ```
37
+
38
+ ```dax
39
+ // Simplified: User sees their own data only
40
+ [EmployeeEmail] = USERPRINCIPALNAME()
41
+ ```
42
+
43
+ ### Manager Hierarchy RLS
44
+
45
+ ```dax
46
+ // Manager sees their direct reports and all indirect reports
47
+ // Requires parent-child hierarchy in employee table
48
+ VAR _UserEmail = USERPRINCIPALNAME()
49
+ VAR _ManagerEmployeeKey =
50
+ LOOKUPVALUE(
51
+ DimEmployee[EmployeeKey],
52
+ DimEmployee[Email], _UserEmail
53
+ )
54
+ VAR _DirectReports =
55
+ CALCULATETABLE(
56
+ VALUES(DimEmployee[EmployeeKey]),
57
+ PATH(DimEmployee[EmployeeKey], DimEmployee[ManagerKey]),
58
+ PATHCONTAINS(
59
+ PATH(DimEmployee[EmployeeKey], DimEmployee[ManagerKey]),
60
+ _ManagerEmployeeKey
61
+ )
62
+ )
63
+ RETURN
64
+ [EmployeeKey] IN _DirectReports ||
65
+ [EmployeeKey] = _ManagerEmployeeKey
66
+ ```
67
+
68
+ ### Multi-Table RLS
69
+
70
+ ```dax
71
+ // Sales table filtered by user's region
72
+ // Must apply to the dimension table, not fact table
73
+ // In DimRegion table:
74
+ VAR _UserEmail = USERPRINCIPALNAME()
75
+ VAR _UserRegions =
76
+ CALCULATETABLE(
77
+ VALUES(DimUserAccess[RegionKey]),
78
+ DimUserAccess[Email] = _UserEmail
79
+ )
80
+ RETURN
81
+ [RegionKey] IN _UserRegions
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Security Tables Pattern
87
+
88
+ ### User-Region Mapping Table
89
+
90
+ ```
91
+ DimUserAccess
92
+ ├── UserAccessKey (PK)
93
+ ├── UserEmail
94
+ ├── RegionKey (FK)
95
+ ├── AccessLevel (Read/Write)
96
+ └── ValidFrom / ValidTo
97
+ ```
98
+
99
+ ```dax
100
+ // RLS filter expression
101
+ VAR _User = USERPRINCIPALNAME()
102
+ RETURN
103
+ COUNTROWS(
104
+ FILTER(
105
+ DimUserAccess,
106
+ DimUserAccess[UserEmail] = _User &&
107
+ DimUserAccess[RegionKey] = [RegionKey] &&
108
+ DimUserAccess[ValidTo] >= TODAY()
109
+ )
110
+ ) > 0
111
+ ```
112
+
113
+ ### Security with Effective Dating
114
+
115
+ ```dax
116
+ // Time-based security (access expires)
117
+ VAR _User = USERPRINCIPALNAME()
118
+ VAR _Today = TODAY()
119
+ RETURN
120
+ COUNTROWS(
121
+ FILTER(
122
+ DimUserAccess,
123
+ DimUserAccess[UserEmail] = _User &&
124
+ DimUserAccess[RegionKey] = [RegionKey] &&
125
+ DimUserAccess[ValidFrom] <= _Today &&
126
+ (_Today <= DimUserAccess[ValidTo] || ISBLANK(DimUserAccess[ValidTo]))
127
+ )
128
+ ) > 0
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Performance-Optimized RLS
134
+
135
+ ### Pre-filtered Security Table
136
+
137
+ ```dax
138
+ // Instead of complex filter in RLS, use pre-computed table
139
+ // DimUserFilteredRegions: One row per user-region combination
140
+ VAR _User = USERPRINCIPALNAME()
141
+ RETURN
142
+ [RegionKey] IN
143
+ CALCULATETABLE(
144
+ VALUES(DimUserFilteredRegions[RegionKey]),
145
+ DimUserFilteredRegions[UserEmail] = _User
146
+ )
147
+ ```
148
+
149
+ ### Avoid LOOKUPVALUE in RLS
150
+
151
+ ```dax
152
+ // BAD: LOOKUPVALUE in every row evaluation
153
+ [RegionKey] = LOOKUPVALUE(DimUser[RegionKey], DimUser[Email], USERPRINCIPALNAME())
154
+
155
+ // GOOD: Use IN with VALUES
156
+ VAR _User = USERPRINCIPALNAME()
157
+ RETURN
158
+ [RegionKey] IN CALCULATETABLE(
159
+ VALUES(DimUser[RegionKey]),
160
+ DimUser[Email] = _User
161
+ )
162
+ ```
163
+
164
+ ### Bi-directional Filtering Consideration
165
+
166
+ ```dax
167
+ // If relationship is bi-directional, RLS on one table filters both
168
+ // Consider setting relationship to single direction for security tables
169
+ // Then explicitly cross-filter in measures when needed
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Testing RLS in DAX
175
+
176
+ ### Test as Another User
177
+
178
+ ```dax
179
+ // Measure to simulate RLS for testing
180
+ Sales_AsUser =
181
+ VAR _TestUser = "john.smith@company.com" // Replace with test user
182
+ VAR _AllowedRegions =
183
+ CALCULATETABLE(
184
+ VALUES(DimUserAccess[RegionKey]),
185
+ DimUserAccess[UserEmail] = _TestUser
186
+ )
187
+ RETURN
188
+ CALCULATE(
189
+ [Sales],
190
+ DimRegion[RegionKey] IN _AllowedRegions
191
+ )
192
+ ```
193
+
194
+ ### RLS Debug Measure
195
+
196
+ ```dax
197
+ // Shows what user sees for debugging
198
+ _RLS_Debug =
199
+ VAR _User = USERPRINCIPALNAME()
200
+ VAR _AccessCount =
201
+ COUNTROWS(
202
+ FILTER(
203
+ DimUserAccess,
204
+ DimUserAccess[UserEmail] = _User
205
+ )
206
+ )
207
+ RETURN
208
+ "User: " & _User &
209
+ " | Access Rows: " & _AccessCount &
210
+ " | Regions: " & CONCATENATEX(
211
+ FILTER(DimUserAccess, DimUserAccess[UserEmail] = _User),
212
+ DimUserAccess[RegionName],
213
+ ", "
214
+ )
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Object-Level Security (OLS)
220
+
221
+ ### Restricting Column Access
222
+
223
+ OLS is configured in the model (not DAX), but here's how to handle hidden data in measures:
224
+
225
+ ```dax
226
+ // Measure that respects OLS
227
+ // If Salary column is restricted, this returns blank for unauthorized users
228
+ Salary Total =
229
+ VAR _Result = SUM(FactEmployees[Salary])
230
+ RETURN
231
+ IF(ISNUMBER(_Result), _Result, BLANK())
232
+ ```
233
+
234
+ ### Conditional Column Display
235
+
236
+ ```dax
237
+ // Show value only if user has access (via security table)
238
+ Salary Display =
239
+ VAR _User = USERPRINCIPALNAME()
240
+ VAR _HasAccess =
241
+ CALCULATE(
242
+ COUNTROWS(DimUserPermissions),
243
+ DimUserPermissions[UserEmail] = _User,
244
+ DimUserPermissions[Permission] = "ViewSalary"
245
+ ) > 0
246
+ RETURN
247
+ IF(_HasAccess, [Salary Total], BLANK())
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Dynamic Security Patterns
253
+
254
+ ### Role-Based Access
255
+
256
+ ```dax
257
+ // Different data based on user's role
258
+ Sales_RoleBased =
259
+ VAR _User = USERPRINCIPALNAME()
260
+ VAR _Role = LOOKUPVALUE(DimUser[Role], DimUser[Email], _User)
261
+ RETURN
262
+ SWITCH(
263
+ _Role,
264
+ "Admin", [Sales], // See all
265
+ "Manager", CALCULATE([Sales], DimRegion[Region] = LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)),
266
+ "Sales Rep", CALCULATE([Sales], DimSalesRep[Email] = _User),
267
+ BLANK() // No access
268
+ )
269
+ ```
270
+
271
+ ### Attribute-Based Access Control (ABAC)
272
+
273
+ ```dax
274
+ // Access based on multiple attributes
275
+ VAR _User = USERPRINCIPALNAME()
276
+ VAR _UserDept = LOOKUPVALUE(DimUser[Department], DimUser[Email], _User)
277
+ VAR _UserLevel = LOOKUPVALUE(DimUser[Level], DimUser[Email], _User)
278
+ VAR _UserRegion = LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)
279
+ RETURN
280
+ -- Level 5+ sees all
281
+ _UserLevel >= 5 ||
282
+ -- Same department and region
283
+ ([Department] = _UserDept && [Region] = _UserRegion) ||
284
+ -- Specific cross-department access
285
+ [Department] IN {"Shared Services", "Corporate"}
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Common Security Scenarios
291
+
292
+ ### Sales Rep Sees Own Customers
293
+
294
+ ```dax
295
+ // On DimCustomer table
296
+ VAR _User = USERPRINCIPALNAME()
297
+ VAR _SalesRepKey =
298
+ LOOKUPVALUE(DimSalesRep[SalesRepKey], DimSalesRep[Email], _User)
299
+ RETURN
300
+ [AssignedSalesRepKey] = _SalesRepKey ||
301
+ ISBLANK(_SalesRepKey) // Allow if user not found (for admins)
302
+ ```
303
+
304
+ ### Regional Manager Sees Region + Corporate
305
+
306
+ ```dax
307
+ // On DimRegion table
308
+ VAR _User = USERPRINCIPALNAME()
309
+ VAR _UserRegion =
310
+ LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)
311
+ RETURN
312
+ [Region] = _UserRegion ||
313
+ [Region] = "Corporate" ||
314
+ ISBLANK(_UserRegion)
315
+ ```
316
+
317
+ ### Time-Limited Project Access
318
+
319
+ ```dax
320
+ // On DimProject table
321
+ VAR _User = USERPRINCIPALNAME()
322
+ VAR _Today = TODAY()
323
+ RETURN
324
+ COUNTROWS(
325
+ FILTER(
326
+ DimProjectAccess,
327
+ DimProjectAccess[ProjectKey] = [ProjectKey] &&
328
+ DimProjectAccess[UserEmail] = _User &&
329
+ DimProjectAccess[StartDate] <= _Today &&
330
+ DimProjectAccess[EndDate] >= _Today
331
+ )
332
+ ) > 0
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Security Best Practices
338
+
339
+ ### 1. Always Filter on Dimensions
340
+
341
+ ```dax
342
+ // GOOD: RLS on dimension table
343
+ // Fact table inherits filter through relationship
344
+ // On DimRegion:
345
+ [Region] IN {"North", "South"}
346
+
347
+ // BAD: RLS on fact table (slow)
348
+ // On FactSales:
349
+ RELATED(DimRegion[Region]) IN {"North", "South"}
350
+ ```
351
+
352
+ ### 2. Test with Different Users
353
+
354
+ ```dax
355
+ // Create test measure before deploying
356
+ _Test_Security =
357
+ VAR _TestUser = "test.user@company.com"
358
+ RETURN
359
+ CALCULATE(
360
+ [Sales],
361
+ -- Simulate the RLS filter
362
+ DimRegion[Region] IN CALCULATETABLE(
363
+ VALUES(DimUserAccess[Region]),
364
+ DimUserAccess[UserEmail] = _TestUser
365
+ )
366
+ )
367
+ ```
368
+
369
+ ### 3. Provide Fallback for Admins
370
+
371
+ ```dax
372
+ // Always allow admin role
373
+ VAR _User = USERPRINCIPALNAME()
374
+ VAR _IsAdmin = _User IN {"admin@company.com", "power.bi.admin@company.com"}
375
+ VAR _UserRegions = CALCULATETABLE(...)
376
+ RETURN
377
+ _IsAdmin || [RegionKey] IN _UserRegions
378
+ ```
379
+
380
+ ### 4. Log Security Filters
381
+
382
+ ```dax
383
+ // Measure to audit what users see
384
+ _Security_Audit =
385
+ "User: " & USERPRINCIPALNAME() &
386
+ " | Rows Visible: " & COUNTROWS(ALLSELECTED(FactSales)) &
387
+ " | Timestamp: " & NOW()
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Troubleshooting
393
+
394
+ | Issue | Cause | Fix |
395
+ |-------|-------|-----|
396
+ | All data visible | RLS not applied | Check role membership |
397
+ | No data visible | Filter too restrictive | Verify user in security table |
398
+ | Slow with RLS | Complex RLS expression | Simplify, use pre-computed table |
399
+ | Inconsistent results | Bi-directional filters | Review relationship directions |
400
+ | Error with USERPRINCIPALNAME | Running in Desktop | Use "View as Role" feature |
401
+
402
+ ---
403
+
404
+ ## Setup Checklist
405
+
406
+ - [ ] Create security mapping table (user → access)
407
+ - [ ] Define RLS role in model
408
+ - [ ] Write filter expression on dimension table(s)
409
+ - [ ] Test with "View as Role" in Desktop
410
+ - [ ] Test with actual users in Service
411
+ - [ ] Verify totals match expected (no data leakage)
412
+ - [ ] Document security requirements
413
+ - [ ] Set up monitoring for access patterns
@@ -0,0 +1,316 @@
1
+ # Text and Formatting Patterns
2
+
3
+ ## Overview
4
+ DAX patterns for text manipulation, concatenation, and dynamic formatting in Power BI measures.
5
+
6
+ ---
7
+
8
+ ## Text Concatenation
9
+
10
+ ### Basic Concatenation
11
+ ```dax
12
+ -- Using & operator
13
+ FullName = [FirstName] & " " & [LastName]
14
+
15
+ -- Using CONCATENATE (only 2 arguments)
16
+ FullName = CONCATENATE([FirstName], CONCATENATE(" ", [LastName]))
17
+
18
+ -- Using COMBINEVALUES (better for relationships)
19
+ CustomerKey = COMBINEVALUES("-", [Region], [CustomerID])
20
+ ```
21
+
22
+ ### Dynamic Titles
23
+ ```dax
24
+ ChartTitle =
25
+ VAR _SelectedYear = SELECTEDVALUE('Date'[Year], "All Years")
26
+ VAR _SelectedRegion = SELECTEDVALUE(Region[Name], "All Regions")
27
+ RETURN
28
+ "Sales for " & _SelectedRegion & " - " & _SelectedYear
29
+
30
+ -- With metric selection
31
+ DynamicTitle =
32
+ VAR _Metric = SELECTEDVALUE(MetricSelector[Metric], "Sales")
33
+ RETURN
34
+ _Metric & " by Region"
35
+ ```
36
+
37
+ ### Conditional Text
38
+ ```dax
39
+ StatusText =
40
+ VAR _Variance = [Actual] - [Budget]
41
+ RETURN
42
+ IF(_Variance >= 0, "On Target", "Below Target")
43
+
44
+ -- With emoji/symbols
45
+ TrendIndicator =
46
+ VAR _Change = [MoM Growth %]
47
+ RETURN
48
+ SWITCH(
49
+ TRUE(),
50
+ _Change > 0.05, "▲ Strong",
51
+ _Change > 0, "▲ Up",
52
+ _Change = 0, "► Flat",
53
+ _Change > -0.05, "▼ Down",
54
+ "▼ Weak"
55
+ )
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Number Formatting
61
+
62
+ ### FORMAT Function
63
+ ```dax
64
+ -- Currency
65
+ FormattedSales = FORMAT([Total Sales], "$#,##0.00")
66
+
67
+ -- Percentage
68
+ FormattedGrowth = FORMAT([Growth Rate], "0.0%")
69
+
70
+ -- Thousands/Millions
71
+ FormattedShort =
72
+ VAR _Value = [Total Sales]
73
+ RETURN
74
+ SWITCH(
75
+ TRUE(),
76
+ _Value >= 1000000000, FORMAT(_Value / 1000000000, "#,##0.0") & "B",
77
+ _Value >= 1000000, FORMAT(_Value / 1000000, "#,##0.0") & "M",
78
+ _Value >= 1000, FORMAT(_Value / 1000, "#,##0.0") & "K",
79
+ FORMAT(_Value, "#,##0")
80
+ )
81
+
82
+ -- Custom decimal places
83
+ FormattedValue = FORMAT([Value], "#,##0.00")
84
+
85
+ -- Leading zeros
86
+ FormattedID = FORMAT([ID], "00000") -- 00123
87
+ ```
88
+
89
+ ### Date Formatting
90
+ ```dax
91
+ -- Full date
92
+ FormattedDate = FORMAT([Date], "MMMM D, YYYY") -- January 15, 2024
93
+
94
+ -- Short date
95
+ ShortDate = FORMAT([Date], "MM/DD/YYYY")
96
+
97
+ -- Month-Year
98
+ MonthYear = FORMAT([Date], "MMM YYYY") -- Jan 2024
99
+
100
+ -- Day name
101
+ DayName = FORMAT([Date], "DDDD") -- Monday
102
+
103
+ -- ISO format
104
+ ISODate = FORMAT([Date], "YYYY-MM-DD")
105
+
106
+ -- Quarter
107
+ QuarterText = "Q" & FORMAT([Date], "Q YYYY") -- Q1 2024
108
+ ```
109
+
110
+ ### Conditional Formatting Values
111
+ ```dax
112
+ -- For use with conditional formatting
113
+ VarianceColor =
114
+ VAR _Variance = [Actual] - [Budget]
115
+ RETURN
116
+ IF(_Variance >= 0, 1, -1)
117
+
118
+ -- Traffic light (1, 2, 3)
119
+ PerformanceLevel =
120
+ VAR _Achievement = DIVIDE([Actual], [Target])
121
+ RETURN
122
+ SWITCH(
123
+ TRUE(),
124
+ _Achievement >= 1, 1, -- Green
125
+ _Achievement >= 0.8, 2, -- Yellow
126
+ 3 -- Red
127
+ )
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Dynamic Labels
133
+
134
+ ### KPI Card Labels
135
+ ```dax
136
+ SalesWithLabel =
137
+ VAR _Sales = [Total Sales]
138
+ VAR _FormattedSales = FORMAT(_Sales, "$#,##0")
139
+ RETURN
140
+ "Total Sales: " & _FormattedSales
141
+
142
+ -- With comparison
143
+ SalesComparison =
144
+ VAR _Current = [Total Sales]
145
+ VAR _Previous = [Total Sales PY]
146
+ VAR _Change = _Current - _Previous
147
+ VAR _Pct = DIVIDE(_Change, _Previous)
148
+ RETURN
149
+ FORMAT(_Current, "$#,##0") &
150
+ " (" & IF(_Change >= 0, "+", "") & FORMAT(_Pct, "0.0%") & " vs PY)"
151
+ ```
152
+
153
+ ### Selection Labels
154
+ ```dax
155
+ SelectionSummary =
156
+ VAR _SelectedProducts = COUNTROWS(VALUES(Product[Name]))
157
+ VAR _TotalProducts = COUNTROWS(ALL(Product[Name]))
158
+ RETURN
159
+ IF(
160
+ _SelectedProducts = _TotalProducts,
161
+ "All Products",
162
+ _SelectedProducts & " of " & _TotalProducts & " Products"
163
+ )
164
+ ```
165
+
166
+ ### Period Labels
167
+ ```dax
168
+ PeriodLabel =
169
+ VAR _MinDate = MIN('Date'[Date])
170
+ VAR _MaxDate = MAX('Date'[Date])
171
+ RETURN
172
+ FORMAT(_MinDate, "MMM D, YYYY") & " - " & FORMAT(_MaxDate, "MMM D, YYYY")
173
+
174
+ -- Relative period
175
+ RelativePeriod =
176
+ VAR _Days = DATEDIFF(MAX('Date'[Date]), TODAY(), DAY)
177
+ RETURN
178
+ SWITCH(
179
+ TRUE(),
180
+ _Days = 0, "Today",
181
+ _Days = 1, "Yesterday",
182
+ _Days <= 7, _Days & " days ago",
183
+ _Days <= 30, ROUNDUP(_Days / 7, 0) & " weeks ago",
184
+ FORMAT(MAX('Date'[Date]), "MMM D, YYYY")
185
+ )
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Text Extraction and Manipulation
191
+
192
+ ### SEARCH and MID
193
+ ```dax
194
+ -- Extract domain from email
195
+ EmailDomain =
196
+ VAR _Email = [Email]
197
+ VAR _AtPos = SEARCH("@", _Email, 1, 0)
198
+ RETURN
199
+ IF(_AtPos > 0, MID(_Email, _AtPos + 1, 100), BLANK())
200
+
201
+ -- Extract first word
202
+ FirstWord =
203
+ VAR _Text = [FullName]
204
+ VAR _SpacePos = SEARCH(" ", _Text, 1, LEN(_Text) + 1)
205
+ RETURN
206
+ LEFT(_Text, _SpacePos - 1)
207
+ ```
208
+
209
+ ### Text Cleaning
210
+ ```dax
211
+ -- Remove extra spaces (calculated column)
212
+ CleanedName = TRIM([Name])
213
+
214
+ -- Proper case (calculated column)
215
+ ProperName =
216
+ VAR _Text = LOWER([Name])
217
+ RETURN
218
+ UPPER(LEFT(_Text, 1)) & MID(_Text, 2, LEN(_Text))
219
+
220
+ -- Replace characters (calculated column)
221
+ CleanPhone = SUBSTITUTE(SUBSTITUTE([Phone], "-", ""), " ", "")
222
+ ```
223
+
224
+ ### UNICHAR for Symbols
225
+ ```dax
226
+ -- Common symbols
227
+ CheckMark = UNICHAR(10004) -- ✔
228
+ CrossMark = UNICHAR(10006) -- ✖
229
+ UpArrow = UNICHAR(9650) -- ▲
230
+ DownArrow = UNICHAR(9660) -- ▼
231
+ RightArrow = UNICHAR(9658) -- ►
232
+ Star = UNICHAR(9733) -- ★
233
+ Circle = UNICHAR(9679) -- ●
234
+
235
+ -- Usage in measure
236
+ StatusIcon =
237
+ VAR _Status = [Status]
238
+ RETURN
239
+ SWITCH(
240
+ _Status,
241
+ "Complete", UNICHAR(10004),
242
+ "In Progress", UNICHAR(9679),
243
+ "Not Started", UNICHAR(10006),
244
+ ""
245
+ )
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Ranking and Position Text
251
+
252
+ ### Ordinal Numbers
253
+ ```dax
254
+ OrdinalRank =
255
+ VAR _Rank = [Rank]
256
+ VAR _Suffix =
257
+ SWITCH(
258
+ TRUE(),
259
+ MOD(_Rank, 100) IN {11, 12, 13}, "th",
260
+ MOD(_Rank, 10) = 1, "st",
261
+ MOD(_Rank, 10) = 2, "nd",
262
+ MOD(_Rank, 10) = 3, "rd",
263
+ "th"
264
+ )
265
+ RETURN
266
+ _Rank & _Suffix
267
+
268
+ -- Result: 1st, 2nd, 3rd, 4th, 11th, 12th, 21st, etc.
269
+ ```
270
+
271
+ ### Ranking Label
272
+ ```dax
273
+ RankLabel =
274
+ VAR _Rank = [ProductRank]
275
+ VAR _Total = COUNTROWS(ALL(Product))
276
+ RETURN
277
+ "#" & _Rank & " of " & _Total
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Multi-line Text
283
+
284
+ ### Line Breaks
285
+ ```dax
286
+ -- Using UNICHAR(10) for line break
287
+ MultiLineKPI =
288
+ VAR _Sales = FORMAT([Total Sales], "$#,##0")
289
+ VAR _Units = FORMAT([Total Units], "#,##0")
290
+ VAR _Avg = FORMAT([Avg Order Value], "$#,##0.00")
291
+ RETURN
292
+ "Sales: " & _Sales & UNICHAR(10) &
293
+ "Units: " & _Units & UNICHAR(10) &
294
+ "AOV: " & _Avg
295
+
296
+ -- Address formatting
297
+ FullAddress =
298
+ [Street] & UNICHAR(10) &
299
+ [City] & ", " & [State] & " " & [PostalCode]
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Performance Note
305
+
306
+ - FORMAT() returns text - can't be used in further calculations
307
+ - Use separate measures: one for value (number), one for display (text)
308
+ - Text measures increase model size - use sparingly
309
+ - Consider using Power BI's built-in formatting where possible
310
+
311
+ ---
312
+
313
+ ## Related Resources
314
+
315
+ - [KPIs and Metrics](./kpis-and-metrics.md)
316
+ - [DAX Skill](../../skills/dax/SKILL.md)