@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
@@ -19,6 +19,7 @@ vi.mock('../../hooks/useOrganisations', () => ({
19
19
  // Mock the RBAC API
20
20
  vi.mock('../../rbac/api', () => ({
21
21
  isPermitted: vi.fn(),
22
+ isPermittedCached: vi.fn(),
22
23
  getPermissionMap: vi.fn(),
23
24
  emitAuditEvent: vi.fn()
24
25
  }));
@@ -451,8 +452,8 @@ describe('useOrganisationSecurity', () => {
451
452
  const mockOrg = { id: 'org-123' };
452
453
 
453
454
  // Mock the RBAC API
454
- const { isPermitted } = await import('../../rbac/api');
455
- vi.mocked(isPermitted).mockResolvedValue(true);
455
+ const { isPermittedCached } = await import('../../rbac/api');
456
+ vi.mocked(isPermittedCached).mockResolvedValue(true);
456
457
 
457
458
  vi.mocked(useUnifiedAuth).mockReturnValue({
458
459
  ...mockUseUnifiedAuth,
@@ -469,7 +470,7 @@ describe('useOrganisationSecurity', () => {
469
470
  const hasPermission = await result.current.hasPermission('view_basic');
470
471
 
471
472
  expect(hasPermission).toBe(true);
472
- expect(isPermitted).toHaveBeenCalledWith({
473
+ expect(isPermittedCached).toHaveBeenCalledWith({
473
474
  userId: 'user-123',
474
475
  scope: {
475
476
  organisationId: 'org-123',
@@ -485,8 +486,8 @@ describe('useOrganisationSecurity', () => {
485
486
  const mockOrg = { id: 'org-123' };
486
487
 
487
488
  // Mock the RBAC API to throw an error
488
- const { isPermitted } = await import('../../rbac/api');
489
- vi.mocked(isPermitted).mockRejectedValue(new Error('RBAC API error'));
489
+ const { isPermittedCached } = await import('../../rbac/api');
490
+ vi.mocked(isPermittedCached).mockRejectedValue(new Error('RBAC API error'));
490
491
 
491
492
  vi.mocked(useUnifiedAuth).mockReturnValue({
492
493
  ...mockUseUnifiedAuth,
@@ -510,8 +511,8 @@ describe('useOrganisationSecurity', () => {
510
511
  const mockOrg = { id: 'org-123' };
511
512
 
512
513
  // Mock the RBAC API to throw an error
513
- const { isPermitted } = await import('../../rbac/api');
514
- vi.mocked(isPermitted).mockImplementation(() => {
514
+ const { isPermittedCached } = await import('../../rbac/api');
515
+ vi.mocked(isPermittedCached).mockImplementation(() => {
515
516
  throw new Error('RBAC API exception');
516
517
  });
517
518
 
@@ -536,8 +537,8 @@ describe('useOrganisationSecurity', () => {
536
537
  const mockUser = { id: 'user-123', user_metadata: { eventId: 'event-123', appId: 'app-123' } };
537
538
 
538
539
  // Mock the RBAC API
539
- const { isPermitted } = await import('../../rbac/api');
540
- vi.mocked(isPermitted).mockResolvedValue(true);
540
+ const { isPermittedCached } = await import('../../rbac/api');
541
+ vi.mocked(isPermittedCached).mockResolvedValue(true);
541
542
 
542
543
  vi.mocked(useUnifiedAuth).mockReturnValue({
543
544
  ...mockUseUnifiedAuth,
@@ -549,7 +550,7 @@ describe('useOrganisationSecurity', () => {
549
550
 
550
551
  await result.current.hasPermission('view_basic', 'org-456');
551
552
 
552
- expect(isPermitted).toHaveBeenCalledWith({
553
+ expect(isPermittedCached).toHaveBeenCalledWith({
553
554
  userId: 'user-123',
554
555
  scope: {
555
556
  organisationId: 'org-456',
@@ -21,9 +21,20 @@ import type { Database } from '../../types/database';
21
21
  import type { FileCategory } from '../../types/file-reference';
22
22
  import { FileCategory as FileCategoryEnum } from '../../types/file-reference';
23
23
 
24
- // Mock getPublicUrl
24
+ // Mock getPublicUrl and generateFileUrlsBatch
25
25
  vi.mock('../../utils/storage/helpers', () => ({
26
- getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`)
26
+ getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`),
27
+ generateFileUrlsBatch: vi.fn().mockImplementation(async (supabase, files) => {
28
+ const urlMap = new Map<string, string>();
29
+ for (const file of files) {
30
+ if (file.is_public) {
31
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
32
+ } else {
33
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
34
+ }
35
+ }
36
+ return urlMap;
37
+ })
27
38
  }));
28
39
 
29
40
  // Mock logger
@@ -36,7 +47,7 @@ vi.mock('../../utils/core/logger', () => ({
36
47
  },
37
48
  }));
38
49
 
39
- import { getPublicUrl } from '../../utils/storage/helpers';
50
+ import { getPublicUrl, generateFileUrlsBatch } from '../../utils/storage/helpers';
40
51
 
41
52
  describe('usePublicFileDisplay Hook', () => {
42
53
  let mockSupabase: SupabaseClient<Database>;
@@ -61,6 +72,19 @@ describe('usePublicFileDisplay Hook', () => {
61
72
  vi.clearAllMocks();
62
73
  clearPublicFileDisplayCache();
63
74
  mockSupabase = createMockSupabaseClient() as any;
75
+
76
+ // Reset generateFileUrlsBatch mock to ensure it returns a Map
77
+ vi.mocked(generateFileUrlsBatch).mockImplementation(async (supabase, files) => {
78
+ const urlMap = new Map<string, string>();
79
+ for (const file of files) {
80
+ if (file.is_public) {
81
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
82
+ } else {
83
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
84
+ }
85
+ }
86
+ return urlMap;
87
+ });
64
88
  });
65
89
 
66
90
  afterEach(() => {
@@ -256,13 +280,14 @@ describe('usePublicFileDisplay Hook', () => {
256
280
  await waitFor(
257
281
  () => {
258
282
  expect(result.current.isLoading).toBe(false);
283
+ expect(result.current.fileCount).toBe(2);
284
+ expect(result.current.fileReferences.length).toBe(2);
285
+ expect(result.current.fileUrls).toBeDefined();
286
+ expect(result.current.fileUrls.size).toBe(2);
259
287
  },
260
288
  { timeout: 2000 }
261
289
  );
262
290
 
263
- expect(result.current.fileCount).toBe(2);
264
- expect(result.current.fileReferences.length).toBe(2);
265
- expect(result.current.fileUrls.size).toBe(2);
266
291
  expect(result.current.fileUrls.get('file-123')).toBe('https://example.com/org-123/logos/logo.png');
267
292
  expect(result.current.fileUrls.get('file-456')).toBe('https://example.com/org-123/logos/logo2.png');
268
293
  expect(result.current.fileUrl).toBe(null); // No single file URL in multiple mode
@@ -28,6 +28,8 @@ export { useEventTheme } from './useEventTheme';
28
28
  // === DATA & STATE HOOKS ===
29
29
  export { useDebounce } from './useDebounce';
30
30
  export { useDataTableState } from './useDataTableState';
31
+ export { useAddressAutocomplete } from './useAddressAutocomplete';
32
+ export type { UseAddressAutocompleteOptions, UseAddressAutocompleteReturn } from './useAddressAutocomplete';
31
33
 
32
34
  // === ORGANISATION HOOKS ===
33
35
  export { useOrganisationPermissions } from './useOrganisationPermissions';
@@ -48,6 +50,8 @@ export { useComponentPerformance } from './useComponentPerformance';
48
50
  export { useAppConfig } from './useAppConfig';
49
51
  export type { UseAppConfigReturn } from './useAppConfig';
50
52
  export { usePerformanceMonitor } from './usePerformanceMonitor';
53
+ export { useQueryCache, queryCacheHelpers } from './useQueryCache';
54
+ export type { UseQueryCacheReturn, UseQueryCacheOptions } from './useQueryCache';
51
55
 
52
56
  // DataTable performance hook
53
57
  export { useDataTablePerformance } from './useDataTablePerformance';
@@ -56,6 +60,8 @@ export type { UseDataTablePerformanceOptions, UseDataTablePerformanceReturn } fr
56
60
  // === FILE DISPLAY HOOKS ===
57
61
  export { useFileDisplay, clearFileDisplayCache, getFileDisplayCacheStats, invalidateFileDisplayCache } from './useFileDisplay';
58
62
  export type { UseFileDisplayReturn, UseFileDisplayOptions } from './useFileDisplay';
63
+ export { useFileUrlCache } from './useFileUrlCache';
64
+ export type { UseFileUrlCacheReturn } from './useFileUrlCache';
59
65
 
60
66
  // === STORAGE HOOKS ===
61
67
  export { useStorage, useFileUpload } from './useStorage';
@@ -40,7 +40,7 @@ import { useState, useEffect, useCallback } from 'react';
40
40
  import type { SupabaseClient } from '@supabase/supabase-js';
41
41
  import type { Database } from '../../types/database';
42
42
  import { FileReference, FileCategory } from '../../types/file-reference';
43
- import { getPublicUrl } from '../../utils/storage/helpers';
43
+ import { getPublicUrl, generateFileUrlsBatch } from '../../utils/storage/helpers';
44
44
  import { logger } from '../../utils/core/logger';
45
45
 
46
46
  // Simple in-memory cache for public file data
@@ -236,7 +236,7 @@ export function usePublicFileDisplay(
236
236
  const ids = fileIds.map((item: any) => item.id);
237
237
  const { data: fullData, error: fetchError } = await supabase
238
238
  .from('file_references')
239
- .select('*')
239
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
240
240
  .in('id', ids)
241
241
  .eq('is_public', true); // Only public files in public context
242
242
 
@@ -295,14 +295,12 @@ export function usePublicFileDisplay(
295
295
  const url = getPublicUrl(supabase, firstFile.file_path, true);
296
296
  setFileUrl(url);
297
297
  } else {
298
- // Multiple files mode - generate URLs for all files
299
- const urlMap = new Map<string, string>();
300
- for (const fileRef of fileRefs) {
301
- const url = getPublicUrl(supabase, fileRef.file_path, true);
302
- if (url) {
303
- urlMap.set(fileRef.id, url);
304
- }
305
- }
298
+ // Multiple files mode - generate URLs for all files in batch
299
+ const urlMap = await generateFileUrlsBatch(supabase, fileRefs, {
300
+ appName: 'pace-core',
301
+ orgId: organisation_id,
302
+ expiresIn: 3600
303
+ });
306
304
  setFileUrls(urlMap);
307
305
  setFileReference(null);
308
306
  setFileUrl(null);
@@ -0,0 +1,318 @@
1
+ /**
2
+ * @file Address Autocomplete Hook Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/__tests__
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+ import { renderHook, waitFor } from '@testing-library/react';
10
+ import { useAddressAutocomplete } from './useAddressAutocomplete';
11
+ import * as googlePlacesUtils from '../utils/google-places';
12
+
13
+ // Mock the Google Places utilities
14
+ vi.mock('../utils/google-places', () => ({
15
+ fetchPlaceAutocomplete: vi.fn(),
16
+ fetchPlaceDetails: vi.fn(),
17
+ createAddressFromPlaceResult: vi.fn(),
18
+ getAddressByPlaceId: vi.fn(),
19
+ }));
20
+
21
+ // Mock useQueryCache
22
+ vi.mock('./useQueryCache', () => ({
23
+ useQueryCache: () => ({
24
+ getCachedQuery: async <T,>(_table: string, _filterKey: string, _filterValue: string, fetchFn: () => Promise<T>) => {
25
+ return fetchFn();
26
+ },
27
+ }),
28
+ }));
29
+
30
+ // Mock useDebounce to return value immediately for tests
31
+ vi.mock('./useDebounce', () => ({
32
+ useDebounce: (value: any, _delay: number) => value,
33
+ }));
34
+
35
+ describe('useAddressAutocomplete', () => {
36
+ const mockApiKey = 'test-api-key';
37
+
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ afterEach(() => {
43
+ vi.clearAllMocks();
44
+ vi.useRealTimers();
45
+ });
46
+
47
+ describe('Initial state', () => {
48
+ it('returns initial state with empty suggestions', () => {
49
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
50
+
51
+ expect(result.current.suggestions).toEqual([]);
52
+ expect(result.current.isLoading).toBe(false);
53
+ expect(result.current.error).toBeNull();
54
+ }, { timeout: 5000 });
55
+ });
56
+
57
+ describe('Autocomplete suggestions', () => {
58
+ it('fetches suggestions when input value changes', async () => {
59
+ const mockPredictions = [
60
+ {
61
+ description: '123 Main St, Melbourne VIC, Australia',
62
+ place_id: 'ChIJ123',
63
+ structured_formatting: {
64
+ main_text: '123 Main St',
65
+ secondary_text: 'Melbourne VIC, Australia',
66
+ },
67
+ },
68
+ ];
69
+
70
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
71
+
72
+ const { result } = renderHook(() =>
73
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
74
+ );
75
+
76
+ await waitFor(
77
+ () => {
78
+ expect(result.current.suggestions).toHaveLength(1);
79
+ },
80
+ { timeout: 1000 }
81
+ );
82
+
83
+ expect(result.current.suggestions[0].place_id).toBe('ChIJ123');
84
+ }, { timeout: 5000 });
85
+
86
+ it('debounces input value', async () => {
87
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValue([]);
88
+
89
+ // Since we're mocking useDebounce to return immediately, this test verifies
90
+ // that the hook still works correctly with rapid input changes
91
+ const { result, rerender } = renderHook(
92
+ ({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
93
+ { initialProps: { inputValue: '123' } }
94
+ );
95
+
96
+ // Change input quickly - with mocked debounce, each change triggers immediately
97
+ rerender({ inputValue: '123 M' });
98
+ rerender({ inputValue: '123 Ma' });
99
+ rerender({ inputValue: '123 Main' });
100
+
101
+ // Wait for the last call
102
+ await waitFor(
103
+ () => {
104
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
105
+ },
106
+ { timeout: 1000 }
107
+ );
108
+
109
+ // With mocked debounce, it will be called for each change
110
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
111
+ }, { timeout: 5000 });
112
+
113
+ it('clears suggestions when input is empty', async () => {
114
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
115
+ { description: '123 Main St', place_id: 'ChIJ123' },
116
+ ]);
117
+
118
+ const { result, rerender } = renderHook(
119
+ ({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
120
+ { initialProps: { inputValue: '123 Main' } }
121
+ );
122
+
123
+ await waitFor(
124
+ () => {
125
+ expect(result.current.suggestions).toBeDefined();
126
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
127
+ },
128
+ { timeout: 1000 }
129
+ );
130
+
131
+ rerender({ inputValue: '' });
132
+
133
+ await waitFor(
134
+ () => {
135
+ expect(result.current.suggestions).toBeDefined();
136
+ expect(result.current.suggestions).toEqual([]);
137
+ },
138
+ { timeout: 1000 }
139
+ );
140
+ }, { timeout: 5000 });
141
+
142
+ it('handles API errors', async () => {
143
+ const error = new Error('API request denied');
144
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockRejectedValueOnce(error);
145
+
146
+ const { result } = renderHook(() =>
147
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
148
+ );
149
+
150
+ await waitFor(
151
+ () => {
152
+ expect(result.current.error).not.toBeNull();
153
+ },
154
+ { timeout: 1000 }
155
+ );
156
+
157
+ expect(result.current.error?.message).toBe('API request denied');
158
+ expect(result.current.suggestions).toEqual([]);
159
+ }, { timeout: 5000 });
160
+ });
161
+
162
+ describe('Address selection', () => {
163
+ it('selects address by place_id', async () => {
164
+ const mockPlaceDetails = {
165
+ place_id: 'ChIJ123',
166
+ formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
167
+ address_components: [],
168
+ geometry: { location: { lat: -37.8136, lng: 144.9631 } },
169
+ };
170
+
171
+ const mockParsedAddress = {
172
+ place_id: 'ChIJ123',
173
+ full_address: '123 Main St, Melbourne VIC 3000, Australia',
174
+ street_number: null,
175
+ route: null,
176
+ suburb: null,
177
+ state: null,
178
+ postcode: null,
179
+ country: null,
180
+ lat: -37.8136,
181
+ lng: 144.9631,
182
+ };
183
+
184
+ vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockResolvedValueOnce(mockPlaceDetails);
185
+ vi.mocked(googlePlacesUtils.createAddressFromPlaceResult).mockReturnValueOnce(mockParsedAddress);
186
+
187
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
188
+
189
+ const address = await result.current.selectAddress('ChIJ123');
190
+
191
+ expect(address).not.toBeNull();
192
+ expect(address?.place_id).toBe('ChIJ123');
193
+ expect(googlePlacesUtils.fetchPlaceDetails).toHaveBeenCalledWith('ChIJ123', mockApiKey);
194
+ }, { timeout: 5000 });
195
+
196
+ it('returns null when place_id is invalid', async () => {
197
+ vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockRejectedValueOnce(new Error('Place not found'));
198
+
199
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
200
+
201
+ const address = await result.current.selectAddress('invalid');
202
+
203
+ expect(address).toBeNull();
204
+ });
205
+ });
206
+
207
+ describe('getAddressByPlaceId', () => {
208
+ it('retrieves address by place_id', async () => {
209
+ const mockAddress = {
210
+ place_id: 'ChIJ123',
211
+ full_address: '123 Main St',
212
+ street_number: null,
213
+ route: null,
214
+ suburb: null,
215
+ state: null,
216
+ postcode: null,
217
+ country: null,
218
+ lat: null,
219
+ lng: null,
220
+ };
221
+
222
+ vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(mockAddress);
223
+
224
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
225
+
226
+ const address = await result.current.getAddressByPlaceId('ChIJ123');
227
+
228
+ expect(address).not.toBeNull();
229
+ expect(address?.place_id).toBe('ChIJ123');
230
+ }, { timeout: 5000 });
231
+
232
+ it('returns null on error', async () => {
233
+ vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(null);
234
+
235
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
236
+
237
+ const address = await result.current.getAddressByPlaceId('invalid');
238
+
239
+ expect(address).toBeNull();
240
+ }, { timeout: 5000 });
241
+ });
242
+
243
+ describe('clearSuggestions', () => {
244
+ it('clears suggestions', async () => {
245
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
246
+ { description: '123 Main St', place_id: 'ChIJ123' },
247
+ ]);
248
+
249
+ const { result } = renderHook(() =>
250
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
251
+ );
252
+
253
+ await waitFor(
254
+ () => {
255
+ expect(result.current.suggestions).toBeDefined();
256
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
257
+ },
258
+ { timeout: 1000 }
259
+ );
260
+
261
+ result.current.clearSuggestions();
262
+
263
+ // Wait for state update
264
+ await waitFor(
265
+ () => {
266
+ expect(result.current.suggestions).toEqual([]);
267
+ expect(result.current.error).toBeNull();
268
+ },
269
+ { timeout: 1000 }
270
+ );
271
+ }, { timeout: 5000 });
272
+ });
273
+
274
+ describe('Caching', () => {
275
+ it('uses cache when enabled', async () => {
276
+ const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
277
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
278
+
279
+ const { result } = renderHook(() =>
280
+ useAddressAutocomplete(mockApiKey, '123 Main', {
281
+ debounceDelay: 0,
282
+ cacheEnabled: true,
283
+ })
284
+ );
285
+
286
+ await waitFor(
287
+ () => {
288
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
289
+ },
290
+ { timeout: 1000 }
291
+ );
292
+
293
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
294
+ });
295
+
296
+ it('skips cache when disabled', async () => {
297
+ const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
298
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
299
+
300
+ const { result } = renderHook(() =>
301
+ useAddressAutocomplete(mockApiKey, '123 Main', {
302
+ debounceDelay: 0,
303
+ cacheEnabled: false,
304
+ })
305
+ );
306
+
307
+ await waitFor(
308
+ () => {
309
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
310
+ },
311
+ { timeout: 1000 }
312
+ );
313
+
314
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
315
+ }, { timeout: 5000 });
316
+ });
317
+ });
318
+