@jmruthers/pace-core 0.5.186 → 0.5.188

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 (290) hide show
  1. package/dist/{DataTable-IX2NBUTP.js → DataTable-GUFUNZ3N.js} +7 -7
  2. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  3. package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-HGPQUCBC.js → chunk-2UUZZJFT.js} +3 -3
  8. package/dist/{chunk-445GEP27.js → chunk-3GOZZZYH.js} +33 -8
  9. package/dist/chunk-3GOZZZYH.js.map +1 -0
  10. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  11. package/dist/chunk-63FOKYGO.js.map +1 -0
  12. package/dist/{chunk-DAGICKHT.js → chunk-DDM4CCYT.js} +3 -3
  13. package/dist/{chunk-XAUHJD3L.js → chunk-E7UAOUMY.js} +2 -2
  14. package/dist/{chunk-HDCUMOOI.js → chunk-EFCLXK7F.js} +792 -559
  15. package/dist/chunk-EFCLXK7F.js.map +1 -0
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-GRIQLQ52.js → chunk-IM4QE42D.js} +27 -23
  19. package/dist/chunk-IM4QE42D.js.map +1 -0
  20. package/dist/{chunk-OALXJH4Y.js → chunk-IPCH26AG.js} +8 -8
  21. package/dist/chunk-IPCH26AG.js.map +1 -0
  22. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  23. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  24. package/dist/{chunk-TC7D3CR3.js → chunk-UNOTYLQF.js} +556 -101
  25. package/dist/chunk-UNOTYLQF.js.map +1 -0
  26. package/dist/{chunk-FXFJRTKI.js → chunk-VGZZXKBR.js} +5 -5
  27. package/dist/chunk-VGZZXKBR.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  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/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  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/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/getting-started/examples/README.md +2 -2
  208. package/docs/implementation-guides/file-upload-storage.md +29 -0
  209. package/docs/implementation-guides/public-pages.md +140 -1230
  210. package/docs/rbac/README.md +2 -1
  211. package/docs/rbac/api-reference.md +11 -0
  212. package/docs/rbac/performance.md +320 -0
  213. package/docs/standards/01-architecture-standard.md +5 -0
  214. package/docs/standards/05-security-standard.md +14 -0
  215. package/docs/standards/07-rbac-and-rls-standard.md +356 -0
  216. package/package.json +1 -1
  217. package/src/__tests__/public-recipe-view.test.ts +199 -0
  218. package/src/__tests__/rls-policies.test.ts +333 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/index.ts +2 -0
  227. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  228. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  229. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  230. package/src/hooks/index.ts +6 -0
  231. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  232. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  233. package/src/hooks/useAddressAutocomplete.ts +268 -0
  234. package/src/hooks/useFileDisplay.ts +3 -15
  235. package/src/hooks/useFileReference.test.ts +20 -3
  236. package/src/hooks/useFileReference.ts +3 -24
  237. package/src/hooks/useFileUrlCache.ts +246 -0
  238. package/src/hooks/useInactivityTracker.ts +31 -20
  239. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  240. package/src/hooks/useOrganisationSecurity.ts +3 -3
  241. package/src/hooks/useQueryCache.ts +315 -0
  242. package/src/index.ts +2 -0
  243. package/src/providers/services/EventServiceProvider.tsx +4 -1
  244. package/src/rbac/api.test.ts +21 -6
  245. package/src/rbac/api.ts +32 -11
  246. package/src/rbac/audit-batched.ts +223 -0
  247. package/src/rbac/audit-enhanced.ts +2 -2
  248. package/src/rbac/audit.test.ts +6 -5
  249. package/src/rbac/audit.ts +34 -6
  250. package/src/rbac/cache-invalidation.ts +63 -12
  251. package/src/rbac/cache.test.ts +2 -2
  252. package/src/rbac/cache.ts +61 -14
  253. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  254. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  255. package/src/rbac/config.ts +9 -0
  256. package/src/rbac/engine.ts +2 -21
  257. package/src/rbac/hooks/usePermissions.ts +21 -5
  258. package/src/rbac/index.ts +19 -0
  259. package/src/rbac/performance.ts +210 -0
  260. package/src/rbac/request-deduplication.ts +87 -0
  261. package/src/rbac/utils/deep-equal.ts +93 -0
  262. package/src/services/OrganisationService.ts +5 -4
  263. package/src/types/file-reference.ts +0 -1
  264. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  265. package/src/utils/file-reference/index.ts +44 -15
  266. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  267. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  268. package/src/utils/google-places/index.ts +26 -0
  269. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  270. package/src/utils/google-places/types.ts +94 -0
  271. package/src/utils/index.ts +23 -0
  272. package/src/utils/request-deduplication.ts +165 -0
  273. package/src/utils/storage/helpers.ts +143 -4
  274. package/dist/chunk-445GEP27.js.map +0 -1
  275. package/dist/chunk-FMUCXFII.js +0 -76
  276. package/dist/chunk-FMUCXFII.js.map +0 -1
  277. package/dist/chunk-FSFQFJCU.js.map +0 -1
  278. package/dist/chunk-FXFJRTKI.js.map +0 -1
  279. package/dist/chunk-GRIQLQ52.js.map +0 -1
  280. package/dist/chunk-HDCUMOOI.js.map +0 -1
  281. package/dist/chunk-OALXJH4Y.js.map +0 -1
  282. package/dist/chunk-TC7D3CR3.js.map +0 -1
  283. package/dist/chunk-U6WNSFX5.js.map +0 -1
  284. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
  285. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
  286. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  287. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  288. /package/dist/{chunk-HGPQUCBC.js.map → chunk-2UUZZJFT.js.map} +0 -0
  289. /package/dist/{chunk-DAGICKHT.js.map → chunk-DDM4CCYT.js.map} +0 -0
  290. /package/dist/{chunk-XAUHJD3L.js.map → chunk-E7UAOUMY.js.map} +0 -0
@@ -73,6 +73,7 @@ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
73
73
  import { UUID, Permission, Scope } from '../types';
74
74
  import { createScopeFromEvent } from '../utils/eventContext';
75
75
  import { getRBACLogger } from '../config';
76
+ import { scopeEqual } from '../utils/deep-equal';
76
77
 
77
78
  export interface PagePermissionGuardProps {
78
79
  /** Name of the page being protected */
@@ -333,17 +334,24 @@ const PagePermissionGuardComponent = ({
333
334
 
334
335
  // Create a stable scope that only includes valid values
335
336
  // OrganisationId is required - use undefined if not available, useCan will handle loading state
337
+ // Use ref to track previous scope for deep equality comparison
338
+ const prevScopeRef = useRef<Scope | null>(null);
336
339
  const stableScope = useMemo(() => {
337
- if (resolvedScope && resolvedScope.organisationId) {
338
- return {
339
- organisationId: resolvedScope.organisationId,
340
- appId: resolvedScope.appId || undefined,
341
- eventId: resolvedScope.eventId || undefined
342
- };
340
+ const newScope: Scope = resolvedScope && resolvedScope.organisationId
341
+ ? {
342
+ organisationId: resolvedScope.organisationId,
343
+ appId: resolvedScope.appId || undefined,
344
+ eventId: resolvedScope.eventId || undefined
345
+ }
346
+ : { organisationId: undefined, appId: undefined, eventId: undefined };
347
+
348
+ // Only return new object if scope actually changed (deep equality check)
349
+ if (scopeEqual(prevScopeRef.current, newScope)) {
350
+ return prevScopeRef.current!;
343
351
  }
344
- // Return scope without organisationId - useCan will keep loading state until resolved
345
- // Scope.organisationId is optional, so undefined is valid
346
- return { organisationId: undefined, appId: undefined, eventId: undefined };
352
+
353
+ prevScopeRef.current = newScope;
354
+ return newScope;
347
355
  }, [resolvedScope]);
348
356
 
349
357
  // Check if user has permission - only call useCan when we have a resolved scope with valid organisationId
@@ -487,5 +495,6 @@ function DefaultLoading() {
487
495
  );
488
496
  };
489
497
 
490
- export const PagePermissionGuard = PagePermissionGuardComponent;
498
+ // Memoize component to prevent unnecessary re-renders when props haven't changed
499
+ export const PagePermissionGuard = React.memo(PagePermissionGuardComponent);
491
500
  export default PagePermissionGuard;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Performance tests for PagePermissionGuard
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Components/PagePermissionGuard/Performance
5
+ * @since 2.0.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
+ import { render, screen, waitFor } from '@testing-library/react';
10
+ import React from 'react';
11
+ import { PagePermissionGuard } from '../PagePermissionGuard';
12
+ import { useCan } from '../../hooks';
13
+ import { useUnifiedAuth } from '../../../providers/services/UnifiedAuthProvider';
14
+ import { clearInFlightRequests, getInFlightRequestCount } from '../../request-deduplication';
15
+ import { rbacCache } from '../../cache';
16
+
17
+ // Mock dependencies - use same path as component
18
+ vi.mock('../../hooks', () => ({
19
+ useCan: vi.fn()
20
+ }));
21
+
22
+ // Mock the auth provider
23
+ const mockUseUnifiedAuthFn = vi.fn();
24
+ vi.mock('../../../providers/services/UnifiedAuthProvider', () => ({
25
+ useUnifiedAuth: () => mockUseUnifiedAuthFn(),
26
+ UnifiedAuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
27
+ }));
28
+
29
+ // Mock the event context utility
30
+ vi.mock('../../utils/eventContext', () => ({
31
+ createScopeFromEvent: vi.fn()
32
+ }));
33
+
34
+ // Mock the app name resolver
35
+ vi.mock('../../../utils/app/appNameResolver', () => ({
36
+ getCurrentAppName: vi.fn(() => 'test-app')
37
+ }));
38
+
39
+ import { createScopeFromEvent } from '../../utils/eventContext';
40
+ import { getCurrentAppName } from '../../../utils/app/appNameResolver';
41
+
42
+ const mockUseCan = vi.mocked(useCan);
43
+ const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
44
+ const mockGetCurrentAppName = vi.mocked(getCurrentAppName);
45
+
46
+ describe('PagePermissionGuard Performance', () => {
47
+ const mockUser = {
48
+ id: 'user-123',
49
+ email: 'test@example.com',
50
+ };
51
+
52
+ const mockOrganisation = {
53
+ id: 'org-123',
54
+ name: 'Test Org',
55
+ };
56
+
57
+ const mockEvent = {
58
+ event_id: 'event-123',
59
+ name: 'Test Event',
60
+ };
61
+
62
+ const mockScope = {
63
+ organisationId: 'org-123',
64
+ eventId: 'event-123',
65
+ appId: 'app-123',
66
+ };
67
+
68
+ beforeEach(() => {
69
+ vi.clearAllMocks();
70
+ clearInFlightRequests();
71
+ rbacCache.clear();
72
+
73
+ // Set up mocks to match working test exactly
74
+ mockUseUnifiedAuthFn.mockReturnValue({
75
+ user: mockUser,
76
+ selectedOrganisation: { id: 'org-123' },
77
+ selectedEvent: { event_id: 'event-123' },
78
+ appId: 'app-123', // Required for scope resolution
79
+ supabase: {
80
+ from: vi.fn().mockReturnValue({
81
+ select: vi.fn().mockReturnValue({
82
+ eq: vi.fn().mockReturnValue({
83
+ eq: vi.fn().mockReturnValue({
84
+ single: vi.fn().mockResolvedValue({
85
+ data: { id: 'app-123', name: 'test-app', is_active: true },
86
+ error: null
87
+ })
88
+ })
89
+ })
90
+ })
91
+ })
92
+ } as any
93
+ });
94
+
95
+ mockGetCurrentAppName.mockReturnValue('test-app');
96
+
97
+ // Mock useCan to return permission granted
98
+ // Use mockImplementation to handle all calls, including initial calls with undefined scope
99
+ mockUseCan.mockImplementation(() => ({
100
+ can: true,
101
+ isLoading: false,
102
+ error: null,
103
+ refetch: vi.fn(),
104
+ }));
105
+ });
106
+
107
+ describe('Request Deduplication', () => {
108
+ it('should deduplicate requests when multiple instances check the same permission', async () => {
109
+ // Ensure useCan returns permission granted
110
+ mockUseCan.mockImplementation(() => ({
111
+ can: true,
112
+ isLoading: false,
113
+ error: null,
114
+ refetch: vi.fn(),
115
+ }));
116
+
117
+ render(
118
+ <>
119
+ <PagePermissionGuard pageName="dashboard" operation="read">
120
+ <div data-testid="content-1">Content 1</div>
121
+ </PagePermissionGuard>
122
+ <PagePermissionGuard pageName="dashboard" operation="read">
123
+ <div data-testid="content-2">Content 2</div>
124
+ </PagePermissionGuard>
125
+ <PagePermissionGuard pageName="dashboard" operation="read">
126
+ <div data-testid="content-3">Content 3</div>
127
+ </PagePermissionGuard>
128
+ </>
129
+ );
130
+
131
+ // Wait for scope resolution and permission check to complete
132
+ // The component resolves scope asynchronously, so we need to wait
133
+ await waitFor(() => {
134
+ expect(screen.getByTestId('content-1')).toBeInTheDocument();
135
+ expect(screen.getByTestId('content-2')).toBeInTheDocument();
136
+ expect(screen.getByTestId('content-3')).toBeInTheDocument();
137
+ }, { interval: 10 });
138
+
139
+ // Verify useCan was called (deduplication happens at API level, not hook level)
140
+ expect(mockUseCan).toHaveBeenCalled();
141
+ });
142
+
143
+ it('should track in-flight requests correctly', () => {
144
+ expect(getInFlightRequestCount()).toBe(0);
145
+ });
146
+ });
147
+
148
+ describe('Cache Performance', () => {
149
+ it('should use cached results on subsequent renders', async () => {
150
+ let checkCount = 0;
151
+
152
+ mockUseCan.mockImplementation(() => {
153
+ checkCount++;
154
+ return {
155
+ can: true,
156
+ isLoading: false,
157
+ error: null,
158
+ refetch: vi.fn(),
159
+ };
160
+ });
161
+
162
+ const { rerender } = render(
163
+ <PagePermissionGuard pageName="dashboard" operation="read">
164
+ <div data-testid="content">Content</div>
165
+ </PagePermissionGuard>
166
+ );
167
+
168
+ await waitFor(() => {
169
+ expect(screen.getByTestId('content')).toBeInTheDocument();
170
+ }, { interval: 10 });
171
+
172
+ const initialCheckCount = checkCount;
173
+
174
+ // Rerender with same props
175
+ rerender(
176
+ <PagePermissionGuard pageName="dashboard" operation="read">
177
+ <div data-testid="content">Content</div>
178
+ </PagePermissionGuard>
179
+ );
180
+
181
+ await waitFor(() => {
182
+ expect(screen.getByTestId('content')).toBeInTheDocument();
183
+ }, { interval: 10 });
184
+
185
+ // Should not increase check count significantly due to memoization
186
+ // Allow some margin for React's rendering behavior
187
+ expect(checkCount).toBeLessThanOrEqual(initialCheckCount + 2);
188
+ });
189
+ });
190
+
191
+ describe('Render Performance', () => {
192
+ it('should not cause excessive re-renders', () => {
193
+ let renderCount = 0;
194
+
195
+ const TestComponent = () => {
196
+ renderCount++;
197
+ return (
198
+ <PagePermissionGuard pageName="dashboard" operation="read">
199
+ <div data-testid="content">Content</div>
200
+ </PagePermissionGuard>
201
+ );
202
+ };
203
+
204
+ render(<TestComponent />);
205
+
206
+ // Should not have excessive re-renders
207
+ expect(renderCount).toBeLessThan(10);
208
+ });
209
+
210
+ it('should memoize scope objects to prevent unnecessary re-renders', async () => {
211
+ // Ensure useCan returns permission granted
212
+ mockUseCan.mockImplementation(() => ({
213
+ can: true,
214
+ isLoading: false,
215
+ error: null,
216
+ refetch: vi.fn(),
217
+ }));
218
+
219
+ const { rerender } = render(
220
+ <PagePermissionGuard pageName="dashboard" operation="read">
221
+ <div data-testid="content">Content</div>
222
+ </PagePermissionGuard>
223
+ );
224
+
225
+ await waitFor(() => {
226
+ expect(screen.getByTestId('content')).toBeInTheDocument();
227
+ }, { interval: 10 });
228
+
229
+ const initialCallCount = mockUseCan.mock.calls.length;
230
+
231
+ // Rerender with same props
232
+ rerender(
233
+ <PagePermissionGuard pageName="dashboard" operation="read">
234
+ <div data-testid="content">Content</div>
235
+ </PagePermissionGuard>
236
+ );
237
+
238
+ await waitFor(() => {
239
+ expect(screen.getByTestId('content')).toBeInTheDocument();
240
+ }, { interval: 10 });
241
+
242
+ // useCan should be called, but scope memoization prevents excessive calls
243
+ // Allow some margin for React's rendering behavior
244
+ expect(mockUseCan.mock.calls.length).toBeLessThanOrEqual(initialCallCount + 2);
245
+ });
246
+ });
247
+ });
248
+
@@ -22,12 +22,21 @@ export interface RBACConfig {
22
22
  cache?: {
23
23
  ttl?: number;
24
24
  enabled?: boolean;
25
+ sessionTtl?: number; // Session cache TTL in milliseconds (default: 5 minutes)
25
26
  };
26
27
  audit?: {
27
28
  enabled?: boolean;
28
29
  logLevel?: LogLevel;
30
+ batched?: boolean; // Enable batched audit logging (default: true)
31
+ batchWindow?: number; // Time window in milliseconds (default: 100ms)
32
+ batchSize?: number; // Maximum batch size (default: 10)
29
33
  };
30
34
  security?: Partial<RBACSecurityConfig>;
35
+ performance?: {
36
+ enableRequestDeduplication?: boolean; // Enable request deduplication (default: true)
37
+ enableBatchedAuditLogging?: boolean; // Enable batched audit logging (default: true)
38
+ enablePerformanceTracking?: boolean; // Enable performance tracking (default: false in production)
39
+ };
31
40
  }
32
41
 
33
42
  export interface RBACLogger {
@@ -156,27 +156,8 @@ export class RBACEngine {
156
156
  cacheHit = true;
157
157
  cacheSource = 'memory';
158
158
 
159
- const duration = Date.now() - startTime;
160
-
161
- // Audit cache hit (if organisation context exists)
162
- if (scope.organisationId) {
163
- const resolvedPageId = await this.resolvePageId(pageId, scope.appId);
164
- await emitAuditEvent({
165
- type: 'permission_check',
166
- userId,
167
- organisationId: scope.organisationId,
168
- eventId: scope.eventId,
169
- appId: scope.appId,
170
- pageId: resolvedPageId,
171
- permission,
172
- decision: cached,
173
- source: 'api',
174
- duration_ms: duration,
175
- cache_hit: true,
176
- cache_source: 'memory',
177
- });
178
- }
179
-
159
+ // Skip audit logging for cached checks (only log on cache miss)
160
+ // This significantly reduces network requests
180
161
  return cached;
181
162
  }
182
163
 
@@ -22,6 +22,7 @@ import {
22
22
  isPermittedCached
23
23
  } from '../api';
24
24
  import { getRBACLogger } from '../config';
25
+ import { scopeEqual } from '../utils/deep-equal';
25
26
 
26
27
  /**
27
28
  * Hook to get user's permissions in a scope
@@ -337,20 +338,35 @@ export function useCan(
337
338
  const lastPageIdRef = useRef<UUID | undefined | null>(null);
338
339
  const lastUseCacheRef = useRef<boolean | null>(null);
339
340
 
341
+ // Create a stable scope object for comparison
342
+ const stableScope = useMemo(() => {
343
+ if (!isValidScope) {
344
+ return null;
345
+ }
346
+ return {
347
+ organisationId,
348
+ eventId,
349
+ appId,
350
+ };
351
+ }, [isValidScope, organisationId, eventId, appId]);
352
+
353
+ // Track previous scope for deep equality comparison
354
+ const prevScopeRef = useRef<Scope | null>(null);
355
+
340
356
  useEffect(() => {
341
- // Create a scope key to track changes - use safe property access
342
- const scopeKey = isValidScope ? `${organisationId}-${eventId}-${appId}` : 'invalid-scope';
357
+ // Use deep equality check for scope to prevent unnecessary re-runs
358
+ const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
343
359
 
344
360
  // Only run if something has actually changed
345
361
  if (
346
362
  lastUserIdRef.current !== userId ||
347
- lastScopeRef.current !== scopeKey ||
363
+ scopeChanged ||
348
364
  lastPermissionRef.current !== permission ||
349
365
  lastPageIdRef.current !== pageId ||
350
366
  lastUseCacheRef.current !== useCache
351
367
  ) {
352
368
  lastUserIdRef.current = userId;
353
- lastScopeRef.current = scopeKey;
369
+ prevScopeRef.current = stableScope;
354
370
  lastPermissionRef.current = permission;
355
371
  lastPageIdRef.current = pageId;
356
372
  lastUseCacheRef.current = useCache;
@@ -410,7 +426,7 @@ export function useCan(
410
426
 
411
427
  checkPermission();
412
428
  }
413
- }, [userId, isValidScope, organisationId, eventId, appId, permission, pageId, useCache]);
429
+ }, [userId, stableScope, permission, pageId, useCache]);
414
430
 
415
431
  const refetch = useCallback(async () => {
416
432
  if (!userId) {
package/src/rbac/index.ts CHANGED
@@ -59,6 +59,25 @@ export {
59
59
  CACHE_PATTERNS,
60
60
  } from './cache';
61
61
 
62
+ // Performance monitoring
63
+ export {
64
+ enablePerformanceMonitoring,
65
+ disablePerformanceMonitoring,
66
+ isPerformanceMonitoringEnabled,
67
+ recordPermissionCheck,
68
+ recordAuditEvent,
69
+ getPerformanceMetrics,
70
+ resetPerformanceMetrics,
71
+ getPerformanceSummary,
72
+ type RBACPerformanceMetrics,
73
+ } from './performance';
74
+
75
+ // Request deduplication
76
+ export {
77
+ clearInFlightRequests,
78
+ getInFlightRequestCount,
79
+ } from './request-deduplication';
80
+
62
81
  // Audit
63
82
  export {
64
83
  RBACAuditManager,
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Performance Monitoring for RBAC
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Performance
5
+ * @since 2.0.0
6
+ *
7
+ * This module provides performance monitoring and metrics tracking for RBAC operations.
8
+ */
9
+
10
+ export interface RBACPerformanceMetrics {
11
+ /** Total number of permission checks */
12
+ totalChecks: number;
13
+ /** Number of cache hits */
14
+ cacheHits: number;
15
+ /** Number of cache misses */
16
+ cacheMisses: number;
17
+ /** Cache hit rate (0-1) */
18
+ cacheHitRate: number;
19
+ /** Number of deduplicated requests */
20
+ deduplicatedRequests: number;
21
+ /** Total number of network requests made */
22
+ networkRequests: number;
23
+ /** Average response time in milliseconds */
24
+ averageResponseTime: number;
25
+ /** Total response time in milliseconds */
26
+ totalResponseTime: number;
27
+ /** Number of batched audit events */
28
+ batchedAuditEvents: number;
29
+ /** Number of individual audit events */
30
+ individualAuditEvents: number;
31
+ }
32
+
33
+ class RBACPerformanceMonitor {
34
+ private metrics: RBACPerformanceMetrics = {
35
+ totalChecks: 0,
36
+ cacheHits: 0,
37
+ cacheMisses: 0,
38
+ cacheHitRate: 0,
39
+ deduplicatedRequests: 0,
40
+ networkRequests: 0,
41
+ averageResponseTime: 0,
42
+ totalResponseTime: 0,
43
+ batchedAuditEvents: 0,
44
+ individualAuditEvents: 0,
45
+ };
46
+
47
+ private enabled: boolean = false;
48
+
49
+ /**
50
+ * Enable or disable performance monitoring
51
+ */
52
+ setEnabled(enabled: boolean): void {
53
+ this.enabled = enabled;
54
+ }
55
+
56
+ /**
57
+ * Check if performance monitoring is enabled
58
+ */
59
+ isEnabled(): boolean {
60
+ return this.enabled;
61
+ }
62
+
63
+ /**
64
+ * Record a permission check
65
+ */
66
+ recordCheck(cacheHit: boolean, responseTime: number, wasDeduplicated: boolean = false): void {
67
+ if (!this.enabled) {
68
+ return;
69
+ }
70
+
71
+ this.metrics.totalChecks++;
72
+
73
+ if (cacheHit) {
74
+ this.metrics.cacheHits++;
75
+ } else {
76
+ this.metrics.cacheMisses++;
77
+ this.metrics.networkRequests++;
78
+ }
79
+
80
+ if (wasDeduplicated) {
81
+ this.metrics.deduplicatedRequests++;
82
+ }
83
+
84
+ this.metrics.totalResponseTime += responseTime;
85
+ this.metrics.averageResponseTime = this.metrics.totalResponseTime / this.metrics.totalChecks;
86
+ this.metrics.cacheHitRate = this.metrics.cacheHits / this.metrics.totalChecks;
87
+ }
88
+
89
+ /**
90
+ * Record an audit event
91
+ */
92
+ recordAuditEvent(batched: boolean): void {
93
+ if (!this.enabled) {
94
+ return;
95
+ }
96
+
97
+ if (batched) {
98
+ this.metrics.batchedAuditEvents++;
99
+ } else {
100
+ this.metrics.individualAuditEvents++;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get current metrics
106
+ */
107
+ getMetrics(): RBACPerformanceMetrics {
108
+ return { ...this.metrics };
109
+ }
110
+
111
+ /**
112
+ * Reset all metrics
113
+ */
114
+ reset(): void {
115
+ this.metrics = {
116
+ totalChecks: 0,
117
+ cacheHits: 0,
118
+ cacheMisses: 0,
119
+ cacheHitRate: 0,
120
+ deduplicatedRequests: 0,
121
+ networkRequests: 0,
122
+ averageResponseTime: 0,
123
+ totalResponseTime: 0,
124
+ batchedAuditEvents: 0,
125
+ individualAuditEvents: 0,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Get metrics summary as a formatted string
131
+ */
132
+ getSummary(): string {
133
+ const m = this.metrics;
134
+ return `
135
+ RBAC Performance Metrics:
136
+ Total Checks: ${m.totalChecks}
137
+ Cache Hits: ${m.cacheHits} (${(m.cacheHitRate * 100).toFixed(1)}%)
138
+ Cache Misses: ${m.cacheMisses}
139
+ Deduplicated Requests: ${m.deduplicatedRequests}
140
+ Network Requests: ${m.networkRequests}
141
+ Average Response Time: ${m.averageResponseTime.toFixed(2)}ms
142
+ Batched Audit Events: ${m.batchedAuditEvents}
143
+ Individual Audit Events: ${m.individualAuditEvents}
144
+ `;
145
+ }
146
+ }
147
+
148
+ // Global performance monitor instance
149
+ const performanceMonitor = new RBACPerformanceMonitor();
150
+
151
+ /**
152
+ * Enable performance monitoring
153
+ */
154
+ export function enablePerformanceMonitoring(): void {
155
+ performanceMonitor.setEnabled(true);
156
+ }
157
+
158
+ /**
159
+ * Disable performance monitoring
160
+ */
161
+ export function disablePerformanceMonitoring(): void {
162
+ performanceMonitor.setEnabled(false);
163
+ }
164
+
165
+ /**
166
+ * Check if performance monitoring is enabled
167
+ */
168
+ export function isPerformanceMonitoringEnabled(): boolean {
169
+ return performanceMonitor.isEnabled();
170
+ }
171
+
172
+ /**
173
+ * Record a permission check
174
+ */
175
+ export function recordPermissionCheck(
176
+ cacheHit: boolean,
177
+ responseTime: number,
178
+ wasDeduplicated: boolean = false
179
+ ): void {
180
+ performanceMonitor.recordCheck(cacheHit, responseTime, wasDeduplicated);
181
+ }
182
+
183
+ /**
184
+ * Record an audit event
185
+ */
186
+ export function recordAuditEvent(batched: boolean): void {
187
+ performanceMonitor.recordAuditEvent(batched);
188
+ }
189
+
190
+ /**
191
+ * Get current performance metrics
192
+ */
193
+ export function getPerformanceMetrics(): RBACPerformanceMetrics {
194
+ return performanceMonitor.getMetrics();
195
+ }
196
+
197
+ /**
198
+ * Reset performance metrics
199
+ */
200
+ export function resetPerformanceMetrics(): void {
201
+ performanceMonitor.reset();
202
+ }
203
+
204
+ /**
205
+ * Get performance metrics summary
206
+ */
207
+ export function getPerformanceSummary(): string {
208
+ return performanceMonitor.getSummary();
209
+ }
210
+