@jmruthers/pace-core 0.5.115 → 0.5.116

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 (234) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
  28. package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +41 -14
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +29 -2
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  163. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  164. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  165. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  166. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  167. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  168. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  169. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  170. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  171. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  172. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  173. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  174. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  175. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  176. package/src/components/EventSelector/EventSelector.tsx +5 -25
  177. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  178. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  179. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  184. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  185. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  186. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  187. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  188. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  189. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  190. package/src/components/Select/Select.tsx +8 -0
  191. package/src/components/Toast/Toast.tsx +1 -1
  192. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  193. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  194. package/src/hooks/useEventTheme.ts +49 -18
  195. package/src/hooks/usePermissionCache.ts +5 -3
  196. package/src/hooks/useSecureDataAccess.ts +11 -1
  197. package/src/hooks/useToast.ts +1 -1
  198. package/src/providers/services/EventServiceProvider.tsx +15 -8
  199. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  200. package/src/rbac/audit.test.ts +206 -0
  201. package/src/rbac/audit.ts +37 -2
  202. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  203. package/src/rbac/errors.test.ts +340 -0
  204. package/src/rbac/hooks/index.ts +9 -0
  205. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  206. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  207. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  208. package/src/services/AuthService.ts +10 -0
  209. package/src/services/EventService.ts +111 -50
  210. package/src/services/__tests__/AuthService.test.ts +1 -1
  211. package/src/services/__tests__/EventService.test.ts +60 -45
  212. package/src/services/interfaces/IEventService.ts +1 -1
  213. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  214. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  215. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  216. package/src/utils/file-reference.test.ts +214 -0
  217. package/dist/chunk-3OGQLOJM.js.map +0 -1
  218. package/dist/chunk-5CDJCTOO.js +0 -190
  219. package/dist/chunk-F6QB26OS.js.map +0 -1
  220. package/dist/chunk-KTHLNIMA.js.map +0 -1
  221. package/dist/chunk-OO3V7W4H.js.map +0 -1
  222. package/dist/chunk-SYXOZQ4P.js.map +0 -1
  223. package/dist/chunk-XYRZV7R5.js.map +0 -1
  224. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  225. package/src/rbac/audit-enhanced.ts +0 -351
  226. /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  227. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  228. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  229. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  230. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  231. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  232. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  233. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  234. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -26,6 +26,50 @@ import { useEvents } from './useEvents';
26
26
  import { applyPalette, clearPalette } from '../theming/runtime';
27
27
  import type { PaletteData, ColorPalette } from '../theming/runtime';
28
28
 
29
+ /**
30
+ * Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
31
+ * This matches the logic in EventService.parseAndNormalizeEventColours()
32
+ */
33
+ function parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
34
+ try {
35
+ if (!input) return null;
36
+ let obj: any = input;
37
+ if (typeof input === 'string') {
38
+ try {
39
+ obj = JSON.parse(input);
40
+ } catch {
41
+ return null;
42
+ }
43
+ } else if (typeof input !== 'object') {
44
+ return null;
45
+ }
46
+
47
+ const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
48
+ const main = pick(obj, 'ev-main', 'main');
49
+ const sec = pick(obj, 'ev-sec', 'sec');
50
+ const acc = pick(obj, 'ev-acc', 'acc');
51
+ if (!main && !sec && !acc) return null;
52
+
53
+ // Fill helper: return palette as-is, only return empty object if null/undefined/empty
54
+ const fill = (p: any) => {
55
+ if (!p) return {};
56
+ // If object is empty or has no actual color values, return empty object
57
+ const keys = Object.keys(p);
58
+ if (keys.length === 0) return {};
59
+ // Check if any values are truthy (not null/undefined)
60
+ const hasValues = keys.some(k => p[k] != null);
61
+ if (!hasValues) return {};
62
+ // Return the object as-is (don't fill missing shades)
63
+ return p;
64
+ };
65
+
66
+ return { main: fill(main), sec: fill(sec), acc: fill(acc) };
67
+ } catch (error) {
68
+ console.warn('[useEventTheme] Failed to parse/normalize event colours:', error);
69
+ return null;
70
+ }
71
+ }
72
+
29
73
  /**
30
74
  * Hook that automatically applies event-specific theming
31
75
  *
@@ -62,30 +106,17 @@ export function useEventTheme(): void {
62
106
  // Check if the event has theme colors
63
107
  const eventColours = selectedEvent.event_colours;
64
108
 
65
- if (!eventColours || typeof eventColours !== 'object') {
66
- clearPalette();
67
- return;
68
- }
69
-
70
- // Validate that event_colours has the expected structure
71
- const palette = eventColours as Partial<PaletteData>;
109
+ // Parse and normalize event_colours (same logic as EventService)
110
+ const normalized = parseAndNormalizeEventColours(eventColours);
72
111
 
73
- // Check if we have at least one valid palette (main, sec, or acc)
74
- if (!palette.main && !palette.sec && !palette.acc) {
112
+ if (!normalized) {
75
113
  clearPalette();
76
114
  return;
77
115
  }
78
116
 
79
- // Apply the palette
80
- // The system expects main, sec, and acc, so we ensure all are present (empty if needed)
81
- const fullPalette: PaletteData = {
82
- main: (palette.main as ColorPalette) || {},
83
- sec: (palette.sec as ColorPalette) || {},
84
- acc: (palette.acc as ColorPalette) || {},
85
- };
86
-
117
+ // Apply the normalized palette
87
118
  try {
88
- applyPalette(fullPalette);
119
+ applyPalette(normalized);
89
120
  } catch (error) {
90
121
  console.error('[useEventTheme] Failed to apply event palette:', error);
91
122
  }
@@ -322,7 +322,8 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
322
322
  }));
323
323
  }
324
324
 
325
- const results: PermissionResult[] = [];
325
+ // Pre-size results array to maintain order and length
326
+ const results: PermissionResult[] = new Array(permissions.length);
326
327
  const startTime = Date.now();
327
328
 
328
329
  // Check cache for all permissions first
@@ -334,13 +335,14 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
334
335
  const entry = cache.current.get(cacheKey);
335
336
 
336
337
  if (entry && isCacheValid(entry)) {
337
- results.push({
338
+ // Use index assignment to maintain order
339
+ results[i] = {
338
340
  operation,
339
341
  pageId,
340
342
  hasPermission: entry.result,
341
343
  cached: true,
342
344
  timestamp: entry.timestamp
343
- });
345
+ };
344
346
  stats.current.cacheHits++;
345
347
  } else {
346
348
  uncachedPermissions.push([operation, pageId, i]);
@@ -459,9 +459,19 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
459
459
  await setOrganisationContextInSession(organisationId);
460
460
 
461
461
  // Include organisation_id in RPC parameters
462
+ // Some functions use p_organisation_id instead of organisation_id (to avoid conflicts with RETURNS TABLE columns)
463
+ const functionsWithPOrganisationId = [
464
+ 'data_cake_diners_list',
465
+ 'data_cake_mealplans_list'
466
+ ];
467
+
468
+ const paramName = functionsWithPOrganisationId.includes(functionName)
469
+ ? 'p_organisation_id'
470
+ : 'organisation_id';
471
+
462
472
  const secureParams = {
463
473
  ...params,
464
- organisation_id: organisationId
474
+ [paramName]: organisationId
465
475
  };
466
476
 
467
477
  const { data, error } = await supabase!.rpc(functionName, secureParams);
@@ -34,7 +34,7 @@ type ToasterToast = ToastProps & {
34
34
  /** Unique identifier for the toast */
35
35
  id: string
36
36
  /** Duration before automatic dismissal in milliseconds */
37
- duration: number
37
+ duration?: number
38
38
  /** Optional title content */
39
39
  title?: React.ReactNode
40
40
  /** Optional description content */
@@ -52,16 +52,23 @@ export function EventServiceProvider({
52
52
 
53
53
  // Update service dependencies and initialize when dependencies change
54
54
  useEffect(() => {
55
- eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
56
-
57
- // Re-initialize service when dependencies change
58
55
  let isMounted = true;
59
56
 
60
- eventService.initialize().catch(error => {
61
- if (isMounted) {
62
- console.error('[EventServiceProvider] Failed to initialize event service:', error);
63
- }
64
- });
57
+ const updateAndInitialize = async () => {
58
+ // Update dependencies (now async to handle user change cleanup)
59
+ await eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
60
+
61
+ if (!isMounted) return;
62
+
63
+ // Re-initialize service when dependencies change
64
+ await eventService.initialize().catch(error => {
65
+ if (isMounted) {
66
+ console.error('[EventServiceProvider] Failed to initialize event service:', error);
67
+ }
68
+ });
69
+ };
70
+
71
+ updateAndInitialize();
65
72
 
66
73
  return () => {
67
74
  isMounted = false;
@@ -0,0 +1,385 @@
1
+ /**
2
+ * @file RBAC Cache Invalidation Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/CacheInvalidation
5
+ * @since 1.0.0
6
+ *
7
+ * Comprehensive tests for the RBAC cache invalidation system.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import { createMockSupabaseClient } from '@test/helpers';
12
+ import type { SupabaseClient } from '@supabase/supabase-js';
13
+ import type { Database } from '../types/database';
14
+ import {
15
+ RBACCacheInvalidationManager,
16
+ INVALIDATION_PATTERNS,
17
+ initializeCacheInvalidation,
18
+ getCacheInvalidationManager,
19
+ } from '../cache-invalidation';
20
+ import { rbacCache, CACHE_PATTERNS } from '../cache';
21
+ import { emitAuditEvent } from '../audit';
22
+
23
+ // Mock the cache module
24
+ vi.mock('../cache', () => ({
25
+ rbacCache: {
26
+ invalidate: vi.fn(),
27
+ clear: vi.fn(),
28
+ },
29
+ CACHE_PATTERNS: {
30
+ USER: (userId: string) => `user:${userId}`,
31
+ ORGANISATION: (orgId: string) => `org:${orgId}`,
32
+ EVENT: (eventId: string) => `event:${eventId}`,
33
+ APP: (appId: string) => `app:${appId}`,
34
+ },
35
+ }));
36
+
37
+ // Mock the audit module
38
+ vi.mock('../audit', () => ({
39
+ emitAuditEvent: vi.fn(() => Promise.resolve(undefined)),
40
+ }));
41
+
42
+ describe('RBAC Cache Invalidation', () => {
43
+ let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
44
+ let manager: RBACCacheInvalidationManager;
45
+
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ mockSupabase = createMockSupabaseClient();
49
+ manager = new RBACCacheInvalidationManager(mockSupabase as unknown as SupabaseClient<Database>);
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.clearAllMocks();
54
+ });
55
+
56
+ describe('INVALIDATION_PATTERNS', () => {
57
+ describe('USER_ROLES_CHANGED', () => {
58
+ it('generates correct patterns for user invalidation', () => {
59
+ const userId = 'user-123' as const;
60
+ const patterns = INVALIDATION_PATTERNS.USER_ROLES_CHANGED(userId);
61
+
62
+ expect(patterns).toContain('user:user-123');
63
+ expect(patterns).toContain('perm:user-123:*');
64
+ expect(patterns).toContain('access:user-123:*');
65
+ expect(patterns).toContain('map:user-123:*');
66
+ });
67
+ });
68
+
69
+ describe('ORGANISATION_PERMISSIONS_CHANGED', () => {
70
+ it('generates correct patterns for organisation invalidation', () => {
71
+ const orgId = 'org-456' as const;
72
+ const patterns = INVALIDATION_PATTERNS.ORGANISATION_PERMISSIONS_CHANGED(orgId);
73
+
74
+ expect(patterns).toContain('org:org-456');
75
+ expect(patterns).toContain('perm:*:org-456:*');
76
+ expect(patterns).toContain('access:*:org-456:*');
77
+ expect(patterns).toContain('map:*:org-456:*');
78
+ });
79
+ });
80
+
81
+ describe('EVENT_PERMISSIONS_CHANGED', () => {
82
+ it('generates correct patterns for event invalidation', () => {
83
+ const eventId = 'event-789';
84
+ const patterns = INVALIDATION_PATTERNS.EVENT_PERMISSIONS_CHANGED(eventId);
85
+
86
+ expect(patterns).toContain('event:event-789');
87
+ expect(patterns).toContain('perm:*:*:event-789:*');
88
+ expect(patterns).toContain('access:*:*:event-789:*');
89
+ expect(patterns).toContain('map:*:*:event-789:*');
90
+ });
91
+ });
92
+
93
+ describe('APP_PERMISSIONS_CHANGED', () => {
94
+ it('generates correct patterns for app invalidation', () => {
95
+ const appId = 'app-abc' as const;
96
+ const patterns = INVALIDATION_PATTERNS.APP_PERMISSIONS_CHANGED(appId);
97
+
98
+ expect(patterns).toContain('app:app-abc');
99
+ expect(patterns).toContain('perm:*:*:*:app-abc:*');
100
+ expect(patterns).toContain('access:*:*:*:app-abc');
101
+ expect(patterns).toContain('map:*:*:*:app-abc');
102
+ });
103
+ });
104
+
105
+ describe('PAGE_PERMISSIONS_CHANGED', () => {
106
+ it('generates correct patterns for page invalidation', () => {
107
+ const pageId = 'page-xyz' as const;
108
+ const patterns = INVALIDATION_PATTERNS.PAGE_PERMISSIONS_CHANGED(pageId);
109
+
110
+ expect(patterns).toContain('perm:*:*:*:*:page-xyz');
111
+ expect(patterns).toContain('map:*:*:*:*');
112
+ });
113
+ });
114
+ });
115
+
116
+ describe('RBACCacheInvalidationManager', () => {
117
+ describe('onInvalidation', () => {
118
+ it('adds callback and returns unsubscribe function', () => {
119
+ const callback = vi.fn();
120
+ const unsubscribe = manager.onInvalidation(callback);
121
+
122
+ expect(typeof unsubscribe).toBe('function');
123
+
124
+ // Trigger invalidation
125
+ manager.invalidateUser('user-123' as const, 'test');
126
+
127
+ expect(callback).toHaveBeenCalled();
128
+
129
+ // Unsubscribe
130
+ unsubscribe();
131
+
132
+ // Clear mocks
133
+ vi.clearAllMocks();
134
+
135
+ // Trigger again - callback should not be called
136
+ manager.invalidateUser('user-123' as const, 'test');
137
+ expect(callback).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('handles multiple callbacks', () => {
141
+ const callback1 = vi.fn();
142
+ const callback2 = vi.fn();
143
+
144
+ manager.onInvalidation(callback1);
145
+ manager.onInvalidation(callback2);
146
+
147
+ manager.invalidateUser('user-123' as const, 'test');
148
+
149
+ expect(callback1).toHaveBeenCalled();
150
+ expect(callback2).toHaveBeenCalled();
151
+ });
152
+ });
153
+
154
+ describe('invalidateUser', () => {
155
+ it('invalidates cache for user with correct patterns', () => {
156
+ const userId = 'user-123' as const;
157
+ manager.invalidateUser(userId, 'test-reason');
158
+
159
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-123');
160
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:*');
161
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:*');
162
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:*');
163
+ });
164
+
165
+ it('emits audit event for user invalidation', async () => {
166
+ const userId = 'user-123' as const;
167
+ manager.invalidateUser(userId, 'test-reason');
168
+
169
+ await vi.waitFor(() => {
170
+ expect(emitAuditEvent).toHaveBeenCalled();
171
+ });
172
+
173
+ const auditCall = vi.mocked(emitAuditEvent).mock.calls[0][0];
174
+ expect(auditCall.type).toBe('permission_check');
175
+ expect(auditCall.permission).toBe('cache:invalidate');
176
+ expect(auditCall.metadata?.reason).toBe('test-reason');
177
+ });
178
+ });
179
+
180
+ describe('invalidateOrganisation', () => {
181
+ it('invalidates cache for organisation with correct patterns', () => {
182
+ const orgId = 'org-456' as const;
183
+ manager.invalidateOrganisation(orgId, 'test-reason');
184
+
185
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('org:org-456');
186
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:org-456:*');
187
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:org-456:*');
188
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:org-456:*');
189
+ });
190
+ });
191
+
192
+ describe('invalidateEvent', () => {
193
+ it('invalidates cache for event with correct patterns', () => {
194
+ const eventId = 'event-789';
195
+ manager.invalidateEvent(eventId, 'test-reason');
196
+
197
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('event:event-789');
198
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:event-789:*');
199
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:event-789:*');
200
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:event-789:*');
201
+ });
202
+ });
203
+
204
+ describe('invalidateApp', () => {
205
+ it('invalidates cache for app with correct patterns', () => {
206
+ const appId = 'app-abc' as const;
207
+ manager.invalidateApp(appId, 'test-reason');
208
+
209
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('app:app-abc');
210
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:app-abc:*');
211
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:*:app-abc');
212
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:app-abc');
213
+ });
214
+ });
215
+
216
+ describe('invalidatePage', () => {
217
+ it('invalidates cache for page with correct patterns', () => {
218
+ const pageId = 'page-xyz' as const;
219
+ manager.invalidatePage(pageId, 'test-reason');
220
+
221
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:*:page-xyz');
222
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:*');
223
+ });
224
+ });
225
+
226
+ describe('invalidateAllUsersInOrganisation', () => {
227
+ it('invalidates cache for all users in organisation', async () => {
228
+ const orgId = 'org-456' as const;
229
+ const mockUsers = [
230
+ { user_id: 'user-1' as const },
231
+ { user_id: 'user-2' as const },
232
+ ];
233
+
234
+ mockSupabase.from.mockReturnValue({
235
+ select: vi.fn().mockReturnValue({
236
+ eq: vi.fn().mockReturnValue({
237
+ eq: vi.fn().mockResolvedValue({
238
+ data: mockUsers,
239
+ error: null,
240
+ }),
241
+ }),
242
+ }),
243
+ } as any);
244
+
245
+ await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
246
+
247
+ // Should invalidate each user
248
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-1');
249
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-2');
250
+ });
251
+
252
+ it('handles empty user list gracefully', async () => {
253
+ const orgId = 'org-456' as const;
254
+
255
+ mockSupabase.from.mockReturnValue({
256
+ select: vi.fn().mockReturnValue({
257
+ eq: vi.fn().mockReturnValue({
258
+ eq: vi.fn().mockResolvedValue({
259
+ data: [],
260
+ error: null,
261
+ }),
262
+ }),
263
+ }),
264
+ } as any);
265
+
266
+ await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
267
+
268
+ // Should not throw, but should not call invalidate for users
269
+ expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
270
+ });
271
+
272
+ it('handles null data gracefully', async () => {
273
+ const orgId = 'org-456' as const;
274
+
275
+ mockSupabase.from.mockReturnValue({
276
+ select: vi.fn().mockReturnValue({
277
+ eq: vi.fn().mockReturnValue({
278
+ eq: vi.fn().mockResolvedValue({
279
+ data: null,
280
+ error: null,
281
+ }),
282
+ }),
283
+ }),
284
+ } as any);
285
+
286
+ await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
287
+
288
+ // Should not throw
289
+ expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
290
+ });
291
+ });
292
+
293
+ describe('clearAllCache', () => {
294
+ it('clears all cache entries', () => {
295
+ manager.clearAllCache();
296
+ expect(rbacCache.clear).toHaveBeenCalled();
297
+ });
298
+ });
299
+
300
+ describe('setupRealtimeSubscriptions', () => {
301
+ it('skips subscriptions when realtime is not available', () => {
302
+ const mockSupabaseNoRealtime = createMockSupabaseClient();
303
+ mockSupabaseNoRealtime.channel = undefined;
304
+
305
+ const managerNoRealtime = new RBACCacheInvalidationManager(
306
+ mockSupabaseNoRealtime as unknown as SupabaseClient<Database>
307
+ );
308
+
309
+ // Should not throw and should log that realtime is not available
310
+ expect(managerNoRealtime).toBeDefined();
311
+ });
312
+
313
+ it('sets up subscriptions when realtime is available', () => {
314
+ const mockChannel = {
315
+ on: vi.fn().mockReturnThis(),
316
+ subscribe: vi.fn(),
317
+ };
318
+
319
+ const mockSupabaseWithRealtime = createMockSupabaseClient();
320
+ mockSupabaseWithRealtime.channel = vi.fn(() => mockChannel);
321
+
322
+ const managerWithRealtime = new RBACCacheInvalidationManager(
323
+ mockSupabaseWithRealtime as unknown as SupabaseClient<Database>
324
+ );
325
+
326
+ expect(mockSupabaseWithRealtime.channel).toHaveBeenCalled();
327
+ expect(mockChannel.on).toHaveBeenCalled();
328
+ expect(mockChannel.subscribe).toHaveBeenCalled();
329
+ });
330
+ });
331
+ });
332
+
333
+ describe('Global Functions', () => {
334
+ describe('initializeCacheInvalidation', () => {
335
+ it('initializes and returns cache invalidation manager', () => {
336
+ const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
337
+
338
+ expect(manager).toBeInstanceOf(RBACCacheInvalidationManager);
339
+ });
340
+
341
+ it('sets global manager instance', () => {
342
+ initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
343
+
344
+ const globalManager = getCacheInvalidationManager();
345
+ expect(globalManager).toBeInstanceOf(RBACCacheInvalidationManager);
346
+ });
347
+ });
348
+
349
+ describe('getCacheInvalidationManager', () => {
350
+ it('returns null when manager is not initialized', () => {
351
+ // Reset global state by creating new instance without initialize
352
+ const manager = getCacheInvalidationManager();
353
+ // May be null or the previous instance
354
+ expect(manager === null || manager instanceof RBACCacheInvalidationManager).toBe(true);
355
+ });
356
+
357
+ it('returns manager instance after initialization', () => {
358
+ const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
359
+ const retrieved = getCacheInvalidationManager();
360
+
361
+ expect(retrieved).toBe(manager);
362
+ });
363
+ });
364
+ });
365
+
366
+ describe('Error Handling', () => {
367
+ it('handles audit event errors gracefully', async () => {
368
+ vi.mocked(emitAuditEvent).mockRejectedValue(new Error('Audit error'));
369
+
370
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
371
+
372
+ manager.invalidateUser('user-123' as const, 'test');
373
+
374
+ await vi.waitFor(() => {
375
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
376
+ expect.stringContaining('[RBAC Cache] Failed to log cache invalidation audit event'),
377
+ expect.any(Error)
378
+ );
379
+ });
380
+
381
+ consoleWarnSpy.mockRestore();
382
+ });
383
+ });
384
+ });
385
+