@jmruthers/pace-core 0.5.188 → 0.5.190

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 (424) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
  4. package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
  5. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  6. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
  7. package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
  8. package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
  9. package/dist/chunk-4QYC5L4K.js.map +1 -0
  10. package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
  11. package/dist/chunk-73HSNNOQ.js.map +1 -0
  12. package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
  13. package/dist/chunk-DZWK57KZ.js.map +1 -0
  14. package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
  15. package/dist/chunk-HQVPB5MZ.js.map +1 -0
  16. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  17. package/dist/chunk-HW3OVDUF.js.map +1 -0
  18. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  19. package/dist/chunk-I7PSE6JW.js.map +1 -0
  20. package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
  21. package/dist/chunk-J2XXC7R5.js.map +1 -0
  22. package/dist/{chunk-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
  23. package/dist/chunk-NIU6J6OX.js.map +1 -0
  24. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  25. package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
  26. package/dist/chunk-RUYZKXOD.js.map +1 -0
  27. package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
  28. package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
  29. package/dist/{chunk-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
  30. package/dist/chunk-STYK4OH2.js.map +1 -0
  31. package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
  32. package/dist/chunk-VVBAW5A5.js.map +1 -0
  33. package/dist/chunk-Y4BUBBHD.js +614 -0
  34. package/dist/chunk-Y4BUBBHD.js.map +1 -0
  35. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  36. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  37. package/dist/components.d.ts +3 -5
  38. package/dist/components.js +19 -23
  39. package/dist/components.js.map +1 -1
  40. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  41. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  42. package/dist/hooks.d.ts +10 -5
  43. package/dist/hooks.js +14 -8
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.d.ts +13 -12
  46. package/dist/index.js +79 -73
  47. package/dist/index.js.map +1 -1
  48. package/dist/providers.d.ts +3 -3
  49. package/dist/providers.js +3 -1
  50. package/dist/rbac/index.d.ts +76 -12
  51. package/dist/rbac/index.js +12 -9
  52. package/dist/types.d.ts +1 -1
  53. package/dist/types.js +1 -1
  54. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
  55. package/dist/utils.js +16 -16
  56. package/docs/README.md +2 -2
  57. package/docs/api/classes/ColumnFactory.md +1 -1
  58. package/docs/api/classes/ErrorBoundary.md +1 -1
  59. package/docs/api/classes/InvalidScopeError.md +2 -2
  60. package/docs/api/classes/Logger.md +1 -1
  61. package/docs/api/classes/MissingUserContextError.md +2 -2
  62. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  63. package/docs/api/classes/PermissionDeniedError.md +1 -1
  64. package/docs/api/classes/RBACAuditManager.md +1 -1
  65. package/docs/api/classes/RBACCache.md +1 -1
  66. package/docs/api/classes/RBACEngine.md +4 -4
  67. package/docs/api/classes/RBACError.md +1 -1
  68. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  69. package/docs/api/classes/SecureSupabaseClient.md +21 -16
  70. package/docs/api/classes/StorageUtils.md +7 -4
  71. package/docs/api/enums/FileCategory.md +1 -1
  72. package/docs/api/enums/LogLevel.md +1 -1
  73. package/docs/api/enums/RBACErrorCode.md +1 -1
  74. package/docs/api/enums/RPCFunction.md +1 -1
  75. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  76. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  77. package/docs/api/interfaces/AggregateConfig.md +1 -1
  78. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  79. package/docs/api/interfaces/AvatarProps.md +128 -0
  80. package/docs/api/interfaces/BadgeProps.md +1 -1
  81. package/docs/api/interfaces/ButtonProps.md +1 -1
  82. package/docs/api/interfaces/CalendarProps.md +20 -6
  83. package/docs/api/interfaces/CardProps.md +1 -1
  84. package/docs/api/interfaces/ColorPalette.md +1 -1
  85. package/docs/api/interfaces/ColorShade.md +1 -1
  86. package/docs/api/interfaces/ComplianceResult.md +1 -1
  87. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  88. package/docs/api/interfaces/DataRecord.md +1 -1
  89. package/docs/api/interfaces/DataTableAction.md +1 -1
  90. package/docs/api/interfaces/DataTableColumn.md +1 -1
  91. package/docs/api/interfaces/DataTableProps.md +1 -1
  92. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  93. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  94. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  95. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  96. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  97. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  98. package/docs/api/interfaces/ExportColumn.md +1 -1
  99. package/docs/api/interfaces/ExportOptions.md +1 -1
  100. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  101. package/docs/api/interfaces/FileMetadata.md +1 -1
  102. package/docs/api/interfaces/FileReference.md +2 -2
  103. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  104. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  105. package/docs/api/interfaces/FileUploadProps.md +30 -19
  106. package/docs/api/interfaces/FooterProps.md +1 -1
  107. package/docs/api/interfaces/FormFieldProps.md +1 -1
  108. package/docs/api/interfaces/FormProps.md +1 -1
  109. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  110. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  111. package/docs/api/interfaces/InputProps.md +1 -1
  112. package/docs/api/interfaces/LabelProps.md +1 -1
  113. package/docs/api/interfaces/LoggerConfig.md +1 -1
  114. package/docs/api/interfaces/LoginFormProps.md +1 -1
  115. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  116. package/docs/api/interfaces/NavigationContextType.md +9 -9
  117. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  118. package/docs/api/interfaces/NavigationItem.md +1 -1
  119. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  120. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  121. package/docs/api/interfaces/Organisation.md +1 -1
  122. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  123. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  124. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  125. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  126. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  127. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  128. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  129. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  130. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  131. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  132. package/docs/api/interfaces/PaletteData.md +1 -1
  133. package/docs/api/interfaces/ParsedAddress.md +1 -1
  134. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  135. package/docs/api/interfaces/ProgressProps.md +3 -11
  136. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  138. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  139. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  140. package/docs/api/interfaces/QuickFix.md +1 -1
  141. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  142. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  143. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  144. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  145. package/docs/api/interfaces/RBACConfig.md +1 -1
  146. package/docs/api/interfaces/RBACContext.md +1 -1
  147. package/docs/api/interfaces/RBACLogger.md +1 -1
  148. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  149. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  151. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  152. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  153. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  154. package/docs/api/interfaces/RBACResult.md +1 -1
  155. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  156. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  157. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  158. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  161. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  162. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  163. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  164. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  165. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  166. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  167. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  168. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  169. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  170. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  171. package/docs/api/interfaces/RouteConfig.md +10 -10
  172. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  173. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  174. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  175. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  176. package/docs/api/interfaces/SetupIssue.md +1 -1
  177. package/docs/api/interfaces/StorageConfig.md +4 -4
  178. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  179. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  180. package/docs/api/interfaces/StorageListOptions.md +22 -9
  181. package/docs/api/interfaces/StorageListResult.md +4 -4
  182. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  183. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  184. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  185. package/docs/api/interfaces/StyleImport.md +1 -1
  186. package/docs/api/interfaces/SwitchProps.md +1 -1
  187. package/docs/api/interfaces/TabsContentProps.md +1 -1
  188. package/docs/api/interfaces/TabsListProps.md +1 -1
  189. package/docs/api/interfaces/TabsProps.md +1 -1
  190. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  191. package/docs/api/interfaces/TextareaProps.md +1 -1
  192. package/docs/api/interfaces/ToastActionElement.md +1 -1
  193. package/docs/api/interfaces/ToastProps.md +1 -1
  194. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  195. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  196. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  197. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  198. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  199. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  201. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  202. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  203. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  205. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  207. package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
  208. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  209. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  210. package/docs/api/interfaces/UserEventAccess.md +11 -11
  211. package/docs/api/interfaces/UserMenuProps.md +1 -1
  212. package/docs/api/interfaces/UserProfile.md +1 -1
  213. package/docs/api/modules.md +155 -135
  214. package/docs/api-reference/components.md +72 -29
  215. package/docs/api-reference/providers.md +2 -2
  216. package/docs/api-reference/rpc-functions.md +1 -0
  217. package/docs/best-practices/README.md +1 -1
  218. package/docs/best-practices/deployment.md +8 -8
  219. package/docs/getting-started/examples/README.md +2 -2
  220. package/docs/getting-started/installation-guide.md +4 -4
  221. package/docs/getting-started/quick-start.md +3 -3
  222. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  223. package/docs/rbac/compliance/compliance-guide.md +2 -2
  224. package/docs/rbac/event-based-apps.md +2 -2
  225. package/docs/rbac/getting-started.md +2 -2
  226. package/docs/rbac/quick-start.md +2 -2
  227. package/docs/security/README.md +4 -4
  228. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  229. package/docs/troubleshooting/README.md +2 -2
  230. package/docs/troubleshooting/migration.md +3 -3
  231. package/package.json +1 -4
  232. package/scripts/check-pace-core-compliance.cjs +1 -1
  233. package/scripts/check-pace-core-compliance.js +1 -1
  234. package/src/__tests__/fixtures/supabase.ts +301 -0
  235. package/src/__tests__/public-recipe-view.test.ts +9 -9
  236. package/src/__tests__/rls-policies.test.ts +197 -61
  237. package/src/components/AddressField/AddressField.test.tsx +42 -0
  238. package/src/components/AddressField/AddressField.tsx +71 -60
  239. package/src/components/AddressField/README.md +1 -0
  240. package/src/components/Alert/Alert.test.tsx +50 -10
  241. package/src/components/Alert/Alert.tsx +5 -3
  242. package/src/components/Avatar/Avatar.test.tsx +252 -226
  243. package/src/components/Avatar/Avatar.tsx +179 -53
  244. package/src/components/Avatar/index.ts +1 -1
  245. package/src/components/Button/Button.test.tsx +2 -1
  246. package/src/components/Button/Button.tsx +3 -3
  247. package/src/components/Calendar/Calendar.test.tsx +53 -37
  248. package/src/components/Calendar/Calendar.tsx +409 -82
  249. package/src/components/Card/Card.test.tsx +7 -4
  250. package/src/components/Card/Card.tsx +3 -6
  251. package/src/components/Checkbox/Checkbox.tsx +2 -2
  252. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  253. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  254. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  255. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  256. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  257. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  258. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  259. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  260. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  261. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  262. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  263. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  264. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  265. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  266. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  267. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  268. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  269. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  270. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  271. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  272. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  273. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  274. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  275. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  276. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  277. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  278. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  279. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  280. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  281. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  282. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  283. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  284. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  285. package/src/components/Dialog/Dialog.tsx +2 -2
  286. package/src/components/EventSelector/EventSelector.tsx +7 -7
  287. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  288. package/src/components/FileUpload/FileUpload.tsx +7 -4
  289. package/src/components/Header/Header.test.tsx +28 -0
  290. package/src/components/Header/Header.tsx +22 -9
  291. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  292. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  293. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  294. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  295. package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
  296. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  297. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  298. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  299. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  300. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  301. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  302. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  303. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  304. package/src/components/Progress/Progress.test.tsx +18 -19
  305. package/src/components/Progress/Progress.tsx +31 -32
  306. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  307. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  308. package/src/components/Select/Select.tsx +5 -5
  309. package/src/components/Switch/Switch.test.tsx +2 -1
  310. package/src/components/Switch/Switch.tsx +1 -1
  311. package/src/components/Toast/Toast.tsx +1 -1
  312. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  313. package/src/components/UserMenu/UserMenu.test.tsx +7 -9
  314. package/src/components/UserMenu/UserMenu.tsx +10 -8
  315. package/src/components/index.ts +2 -1
  316. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  317. package/src/eslint-rules/pace-core-compliance.js +0 -2
  318. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  319. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  320. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  321. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  322. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  323. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  324. package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
  325. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  326. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
  327. package/src/hooks/index.ts +1 -1
  328. package/src/hooks/public/usePublicEvent.ts +2 -2
  329. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  330. package/src/hooks/useAppConfig.ts +24 -5
  331. package/src/hooks/useFileDisplay.ts +297 -34
  332. package/src/hooks/useFileReference.ts +56 -11
  333. package/src/hooks/useFileUrl.ts +1 -1
  334. package/src/hooks/useInactivityTracker.ts +16 -7
  335. package/src/hooks/usePermissionCache.test.ts +85 -8
  336. package/src/hooks/useQueryCache.ts +21 -0
  337. package/src/hooks/useSecureDataAccess.test.ts +80 -35
  338. package/src/hooks/useSecureDataAccess.ts +80 -37
  339. package/src/index.ts +2 -1
  340. package/src/providers/services/EventServiceProvider.tsx +37 -17
  341. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  342. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  343. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  344. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  345. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  346. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  347. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  348. package/src/rbac/api.ts +240 -36
  349. package/src/rbac/cache-invalidation.ts +21 -7
  350. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  351. package/src/rbac/components/NavigationGuard.tsx +23 -63
  352. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  353. package/src/rbac/components/NavigationProvider.tsx +13 -11
  354. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  355. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  356. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  357. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  358. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  359. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  360. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  361. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  362. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  363. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  364. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  365. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  366. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  367. package/src/rbac/engine.ts +4 -2
  368. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  369. package/src/rbac/hooks/index.ts +3 -0
  370. package/src/rbac/hooks/useCan.test.ts +101 -53
  371. package/src/rbac/hooks/usePermissions.ts +108 -41
  372. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  373. package/src/rbac/hooks/useRBAC.ts +83 -40
  374. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  375. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  376. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  377. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  378. package/src/rbac/request-deduplication.ts +1 -1
  379. package/src/rbac/secureClient.ts +72 -12
  380. package/src/rbac/security.ts +29 -23
  381. package/src/rbac/types.ts +10 -0
  382. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  383. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  384. package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
  385. package/src/rbac/utils/contextValidator.ts +288 -0
  386. package/src/rbac/utils/eventContext.ts +48 -2
  387. package/src/services/EventService.ts +165 -21
  388. package/src/services/OrganisationService.ts +37 -2
  389. package/src/services/__tests__/EventService.test.ts +26 -21
  390. package/src/types/file-reference.ts +13 -10
  391. package/src/utils/app/appNameResolver.test.ts +346 -73
  392. package/src/utils/context/superAdminOverride.ts +58 -0
  393. package/src/utils/file-reference/index.ts +61 -33
  394. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  395. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  396. package/src/utils/storage/helpers.test.ts +1 -1
  397. package/src/utils/storage/helpers.ts +38 -19
  398. package/src/utils/storage/types.ts +15 -8
  399. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  400. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  401. package/src/vite-env.d.ts +2 -2
  402. package/dist/chunk-3GOZZZYH.js.map +0 -1
  403. package/dist/chunk-DDM4CCYT.js.map +0 -1
  404. package/dist/chunk-E7UAOUMY.js +0 -75
  405. package/dist/chunk-E7UAOUMY.js.map +0 -1
  406. package/dist/chunk-EFCLXK7F.js.map +0 -1
  407. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  408. package/dist/chunk-HEHYGYOX.js.map +0 -1
  409. package/dist/chunk-IM4QE42D.js.map +0 -1
  410. package/dist/chunk-IPCH26AG.js.map +0 -1
  411. package/dist/chunk-SAUPYVLF.js.map +0 -1
  412. package/dist/chunk-THRPYOFK.js.map +0 -1
  413. package/dist/chunk-UNOTYLQF.js.map +0 -1
  414. package/dist/chunk-VGZZXKBR.js.map +0 -1
  415. package/dist/chunk-YHCN776L.js.map +0 -1
  416. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  417. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  418. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
  419. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  420. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
  421. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
  422. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
  423. /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
  424. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -38,9 +38,10 @@ export class FileReferenceServiceImpl implements FileReferenceService {
38
38
  async createFileReference(options: FileUploadOptions, file: File): Promise<FileReference> {
39
39
  try {
40
40
 
41
- // Validate required options
42
- if (!options.organisation_id) {
43
- throw new Error('organisation_id is required for file upload');
41
+ // organisation_id is optional for user-scoped files (e.g., profile photos)
42
+ const isUserScoped = !options.organisation_id && options.userId;
43
+ if (!isUserScoped && !options.organisation_id) {
44
+ throw new Error('organisation_id is required for file upload, or userId must be provided for user-scoped files');
44
45
  }
45
46
  if (!options.table_name) {
46
47
  throw new Error('table_name is required for file upload');
@@ -52,12 +53,25 @@ export class FileReferenceServiceImpl implements FileReferenceService {
52
53
  throw new Error('folder is required for file upload. The folder prop determines the storage path.');
53
54
  }
54
55
 
56
+ // For user-scoped files, we MUST use auth.uid() for the path to match RLS policies
57
+ // Get the authenticated user ID from the Supabase session
58
+ let authenticatedUserId: string | undefined = undefined;
59
+ if (isUserScoped) {
60
+ const { data: { user: authUser }, error: authError } = await this.supabase.auth.getUser();
61
+ if (authError || !authUser) {
62
+ throw new Error('User must be authenticated to upload user-scoped files');
63
+ }
64
+ authenticatedUserId = authUser.id;
65
+ log.debug('Using authenticated user ID for user-scoped file upload', { userId: authenticatedUserId });
66
+ }
67
+
55
68
  // Step 1: Upload file to storage bucket first
56
- // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename}
69
+ // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename} or users/{auth.uid()}/{folder}/{timestamp-uuid-filename}
57
70
  // Bucket is automatically selected based on is_public flag
58
71
  const uploadResult = await uploadFile(this.supabase, file, {
59
72
  appName: 'file-reference',
60
- orgId: options.organisation_id,
73
+ orgId: options.organisation_id || undefined,
74
+ userId: authenticatedUserId || (isUserScoped ? undefined : options.userId), // Use auth.uid() for user-scoped files
61
75
  isPublic: options.is_public || false,
62
76
  customPath: options.folder // Use folder prop as the custom path segment
63
77
  });
@@ -74,13 +88,16 @@ export class FileReferenceServiceImpl implements FileReferenceService {
74
88
  // Step 2: Extract file metadata (dimensions, hash, etc.)
75
89
  const metadata = await extractFileMetadata(file, {
76
90
  appName: 'file-reference',
77
- orgId: options.organisation_id,
91
+ orgId: options.organisation_id || undefined,
92
+ userId: authenticatedUserId || (isUserScoped ? undefined : options.userId), // Use auth.uid() for user-scoped files
78
93
  isPublic: options.is_public || false
79
94
  }, 'system');
80
95
 
81
96
  // Step 3: Set organisation context in database session before creating file reference
82
- // This ensures RLS policies can check the organisation context
83
- await setOrganisationContext(this.supabase, options.organisation_id);
97
+ // Skip for user-scoped files (no org context needed)
98
+ if (!isUserScoped && options.organisation_id) {
99
+ await setOrganisationContext(this.supabase, options.organisation_id);
100
+ }
84
101
 
85
102
  // Step 4: Create file reference in database using RPC function
86
103
  // This links the storage path to the record in file_references table
@@ -89,7 +106,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
89
106
  p_table_name: options.table_name,
90
107
  p_record_id: options.record_id,
91
108
  p_file_path: filePath, // Storage path from step 1
92
- p_organisation_id: options.organisation_id,
109
+ p_organisation_id: options.organisation_id ?? null,
93
110
  p_app_id: options.app_id,
94
111
  p_page_context: options.pageContext,
95
112
  p_event_id: options.event_id || null, // Pass event_id for event-based apps
@@ -101,7 +118,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
101
118
  ...metadata,
102
119
  ...options.custom_metadata
103
120
  },
104
- p_is_public: options.is_public || false
121
+ p_is_public: options.is_public || false,
122
+ p_user_id: authenticatedUserId || options.userId || null // Pass authenticated user ID for user-scoped files
105
123
  });
106
124
 
107
125
  // Step 5: Rollback - if database insert fails, clean up uploaded file
@@ -131,10 +149,11 @@ export class FileReferenceServiceImpl implements FileReferenceService {
131
149
  }
132
150
 
133
151
  // Invalidate cache for this file display entry so newly uploaded files appear immediately
152
+ // For user-scoped files, pass null for organisation_id
134
153
  invalidateFileDisplayCache(
135
154
  options.table_name,
136
155
  options.record_id,
137
- options.organisation_id,
156
+ options.organisation_id || null,
138
157
  options.category
139
158
  );
140
159
 
@@ -145,15 +164,22 @@ export class FileReferenceServiceImpl implements FileReferenceService {
145
164
  }
146
165
  }
147
166
 
148
- async getFileReference(table_name: string, record_id: string, organisation_id: string): Promise<FileReference | null> {
167
+ async getFileReference(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference | null> {
149
168
  try {
150
- const { data, error } = await this.supabase
169
+ let query = this.supabase
151
170
  .from('file_references')
152
171
  .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
153
172
  .eq('table_name', table_name)
154
- .eq('record_id', record_id)
155
- .eq('organisation_id', organisation_id)
156
- .single();
173
+ .eq('record_id', record_id);
174
+
175
+ // Handle NULL organisation_id for user-owned files
176
+ if (organisation_id === null || organisation_id === undefined) {
177
+ query = query.is('organisation_id', null);
178
+ } else {
179
+ query = query.eq('organisation_id', organisation_id);
180
+ }
181
+
182
+ const { data, error } = await query.single();
157
183
 
158
184
  if (error) {
159
185
  if (error.code === 'PGRST116') {
@@ -169,7 +195,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
169
195
  }
170
196
  }
171
197
 
172
- async getFileUrl(table_name: string, record_id: string, organisation_id: string): Promise<string | null> {
198
+ async getFileUrl(table_name: string, record_id: string, organisation_id?: string): Promise<string | null> {
173
199
  try {
174
200
  // Get file reference to check if it's public
175
201
  const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
@@ -202,14 +228,14 @@ export class FileReferenceServiceImpl implements FileReferenceService {
202
228
  }
203
229
  }
204
230
 
205
- async getSignedUrl(table_name: string, record_id: string, organisation_id: string, expires_in: number = 3600): Promise<string | null> {
231
+ async getSignedUrl(table_name: string, record_id: string, organisation_id?: string, expires_in: number = 3600): Promise<string | null> {
206
232
  try {
207
233
  // Get file path from RPC function
208
234
  const { data: filePath, error } = await this.supabase
209
235
  .rpc('data_file_reference_signed_url_get', {
210
236
  p_table_name: table_name,
211
237
  p_record_id: record_id,
212
- p_organisation_id: organisation_id,
238
+ p_organisation_id: organisation_id ?? null,
213
239
  p_expires_in: expires_in
214
240
  });
215
241
 
@@ -225,6 +251,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
225
251
  const signedUrlResult = await getSignedUrl(this.supabase, filePath, {
226
252
  appName: 'file-reference',
227
253
  orgId: organisation_id,
254
+ userId: organisation_id ? undefined : record_id,
228
255
  expiresIn: expires_in
229
256
  });
230
257
 
@@ -255,7 +282,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
255
282
  }
256
283
  }
257
284
 
258
- async deleteFileReference(table_name: string, record_id: string, organisation_id: string, delete_file: boolean = false): Promise<boolean> {
285
+ async deleteFileReference(table_name: string, record_id: string, organisation_id?: string, delete_file: boolean = false): Promise<boolean> {
259
286
  try {
260
287
  // Get file reference first to determine bucket
261
288
  const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
@@ -264,7 +291,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
264
291
  .rpc('data_file_reference_delete', {
265
292
  p_table_name: table_name,
266
293
  p_record_id: record_id,
267
- p_organisation_id: organisation_id,
294
+ p_organisation_id: organisation_id ?? null,
268
295
  p_delete_file: delete_file
269
296
  });
270
297
 
@@ -284,13 +311,13 @@ export class FileReferenceServiceImpl implements FileReferenceService {
284
311
  }
285
312
  }
286
313
 
287
- async listFileReferences(table_name: string, record_id: string, organisation_id: string): Promise<FileReference[]> {
314
+ async listFileReferences(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference[]> {
288
315
  try {
289
316
  const { data, error } = await this.supabase
290
317
  .rpc('data_file_reference_list', {
291
318
  p_table_name: table_name,
292
319
  p_record_id: record_id,
293
- p_organisation_id: organisation_id
320
+ p_organisation_id: organisation_id ?? null
294
321
  });
295
322
 
296
323
  if (error) {
@@ -331,7 +358,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
331
358
  fileType,
332
359
  ...(item.file_metadata || {}),
333
360
  } as FileMetadata,
334
- organisation_id: organisation_id,
361
+ organisation_id: organisation_id ?? null,
335
362
  app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
336
363
  is_public: item.is_public ?? false,
337
364
  created_at: item.created_at || new Date().toISOString(),
@@ -347,13 +374,13 @@ export class FileReferenceServiceImpl implements FileReferenceService {
347
374
  }
348
375
  }
349
376
 
350
- async getFileCount(table_name: string, record_id: string, organisation_id: string): Promise<number> {
377
+ async getFileCount(table_name: string, record_id: string, organisation_id?: string): Promise<number> {
351
378
  try {
352
379
  const { data, error } = await this.supabase
353
380
  .rpc('data_file_reference_count_get', {
354
381
  p_table_name: table_name,
355
382
  p_record_id: record_id,
356
- p_organisation_id: organisation_id
383
+ p_organisation_id: organisation_id ?? null
357
384
  });
358
385
 
359
386
  if (error) {
@@ -367,12 +394,12 @@ export class FileReferenceServiceImpl implements FileReferenceService {
367
394
  }
368
395
  }
369
396
 
370
- async getFileReferenceById(id: string, organisation_id: string): Promise<FileReference | null> {
397
+ async getFileReferenceById(id: string, organisation_id?: string): Promise<FileReference | null> {
371
398
  try {
372
399
  const { data, error } = await this.supabase
373
400
  .rpc('data_file_reference_get', {
374
401
  p_file_reference_id: id,
375
- p_organisation_id: organisation_id
402
+ p_organisation_id: organisation_id ?? null
376
403
  });
377
404
 
378
405
  if (error) {
@@ -394,7 +421,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
394
421
  table_name: string,
395
422
  record_id: string,
396
423
  category: FileCategory,
397
- organisation_id: string
424
+ organisation_id?: string
398
425
  ): Promise<FileReference[]> {
399
426
  try {
400
427
  // CRITICAL: Use RPC function to get files by category - this correctly filters on file_metadata->>'category'
@@ -405,7 +432,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
405
432
  p_table_name: table_name,
406
433
  p_record_id: record_id,
407
434
  p_category: category,
408
- p_organisation_id: organisation_id
435
+ p_organisation_id: organisation_id ?? null
409
436
  });
410
437
 
411
438
  if (error) {
@@ -471,7 +498,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
471
498
  category: (item.file_metadata?.category as FileCategory) || FileCategory.GENERAL_DOCUMENTS,
472
499
  ...(item.file_metadata || {}),
473
500
  } as FileMetadata,
474
- organisation_id: organisation_id,
501
+ organisation_id: organisation_id ?? null,
475
502
  app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
476
503
  is_public: item.is_public ?? false,
477
504
  created_at: item.created_at || new Date().toISOString(),
@@ -542,11 +569,12 @@ export async function uploadFileWithReference(
542
569
  const service = createFileReferenceService(supabase);
543
570
  const fileReference = await service.createFileReference(options, file);
544
571
 
545
- const fileUrl = options.is_public
572
+ const fileUrl = options.is_public
546
573
  ? getPublicUrl(supabase, fileReference.file_path, true)
547
574
  : await getSignedUrl(supabase, fileReference.file_path, {
548
575
  appName: 'file-reference',
549
- orgId: options.organisation_id,
576
+ orgId: options.organisation_id || undefined,
577
+ userId: options.userId || undefined,
550
578
  expiresIn: 3600
551
579
  });
552
580
 
@@ -14,6 +14,7 @@ import {
14
14
  getAddressByPlaceId,
15
15
  } from './googlePlacesUtils';
16
16
  import { clearInFlightRequests } from '../request-deduplication';
17
+ import { loadGoogleMapsScript, isGoogleMapsLoaded } from './loadGoogleMapsScript';
17
18
  import type { GoogleAddressComponent } from './types';
18
19
 
19
20
  // Mock loadGoogleMapsScript
@@ -62,6 +63,10 @@ const mockPlacesService = {
62
63
  getDetails: vi.fn(),
63
64
  };
64
65
 
66
+ const mockAutocompleteSuggestion = {
67
+ fetchAutocompleteSuggestions: vi.fn(),
68
+ };
69
+
65
70
  // Setup global window.google mock before any tests
66
71
  const setupGoogleMapsMock = () => {
67
72
  const googleMapsMock = {
@@ -69,6 +74,7 @@ const setupGoogleMapsMock = () => {
69
74
  places: {
70
75
  AutocompleteService: vi.fn(() => mockAutocompleteService),
71
76
  PlacesService: vi.fn(() => mockPlacesService),
77
+ AutocompleteSuggestion: undefined as any,
72
78
  PlacesServiceStatus: {
73
79
  OK: 'OK',
74
80
  ZERO_RESULTS: 'ZERO_RESULTS',
@@ -105,9 +111,11 @@ describe('Google Places API Utilities', () => {
105
111
  beforeEach(() => {
106
112
  vi.clearAllMocks();
107
113
  clearInFlightRequests();
114
+ setupGoogleMapsMock();
108
115
  // Reset mocks
109
116
  mockAutocompleteService.getPlacePredictions.mockClear();
110
117
  mockPlacesService.getDetails.mockClear();
118
+ mockAutocompleteSuggestion.fetchAutocompleteSuggestions.mockReset();
111
119
  });
112
120
 
113
121
  afterEach(() => {
@@ -200,6 +208,96 @@ describe('Google Places API Utilities', () => {
200
208
  expect(callArgs.types).toEqual(['address']);
201
209
  expect(callArgs.language).toBe('en');
202
210
  }, { timeout: 5000 });
211
+
212
+ it('uses the new AutocompleteSuggestion API when available', async () => {
213
+ const fetchMock = mockAutocompleteSuggestion.fetchAutocompleteSuggestions;
214
+ (window as any).google.maps.places.AutocompleteSuggestion = mockAutocompleteSuggestion as any;
215
+
216
+ fetchMock.mockResolvedValue({
217
+ suggestions: [
218
+ {
219
+ placePrediction: {
220
+ placeId: 'place-new',
221
+ text: { text: 'Main St', matches: [] },
222
+ structuredFormat: {
223
+ mainText: { text: 'Main St' },
224
+ secondaryText: { text: 'Melbourne' },
225
+ },
226
+ },
227
+ },
228
+ ],
229
+ });
230
+
231
+ const result = await fetchPlaceAutocomplete('Main', mockApiKey);
232
+
233
+ expect(fetchMock).toHaveBeenCalledWith({ input: 'Main' });
234
+ expect(result).toEqual([
235
+ {
236
+ description: 'Main St',
237
+ place_id: 'place-new',
238
+ structured_formatting: {
239
+ main_text: 'Main St',
240
+ secondary_text: 'Melbourne',
241
+ },
242
+ },
243
+ ]);
244
+ expect(mockAutocompleteService.getPlacePredictions).not.toHaveBeenCalled();
245
+ });
246
+
247
+ it('surface errors from the new AutocompleteSuggestion API', async () => {
248
+ (window as any).google.maps.places.AutocompleteSuggestion = mockAutocompleteSuggestion as any;
249
+ mockAutocompleteSuggestion.fetchAutocompleteSuggestions.mockRejectedValue(new Error('network down'));
250
+
251
+ await expect(fetchPlaceAutocomplete('Main', mockApiKey)).rejects.toThrow(
252
+ 'Failed to fetch autocomplete predictions: network down'
253
+ );
254
+ });
255
+
256
+ it('loads Google Maps script when not already available', async () => {
257
+ const mockedLoadScript = vi.mocked(loadGoogleMapsScript);
258
+ const mockedIsLoaded = vi.mocked(isGoogleMapsLoaded);
259
+
260
+ mockedIsLoaded.mockReturnValueOnce(false);
261
+ mockedLoadScript.mockResolvedValueOnce();
262
+
263
+ (window as any).google.maps.places.AutocompleteSuggestion = mockAutocompleteSuggestion as any;
264
+ mockAutocompleteSuggestion.fetchAutocompleteSuggestions.mockResolvedValue({ suggestions: [] });
265
+
266
+ await fetchPlaceAutocomplete('123 Main', mockApiKey);
267
+
268
+ expect(mockedLoadScript).toHaveBeenCalledWith(mockApiKey, 'places');
269
+ });
270
+
271
+ it('deduplicates simultaneous autocomplete requests', async () => {
272
+ vi.useFakeTimers();
273
+
274
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
275
+ setTimeout(() => {
276
+ callback(
277
+ [
278
+ {
279
+ description: 'First',
280
+ place_id: 'place-1',
281
+ structured_formatting: { main_text: 'First', secondary_text: '' },
282
+ },
283
+ ],
284
+ 'OK'
285
+ );
286
+ }, 10);
287
+ });
288
+
289
+ const promise1 = fetchPlaceAutocomplete('duplicate', mockApiKey);
290
+ const promise2 = fetchPlaceAutocomplete('duplicate', mockApiKey);
291
+
292
+ vi.runAllTimers();
293
+
294
+ const [result1, result2] = await Promise.all([promise1, promise2]);
295
+
296
+ expect(mockAutocompleteService.getPlacePredictions).toHaveBeenCalledTimes(1);
297
+ expect(result1).toEqual(result2);
298
+
299
+ vi.useRealTimers();
300
+ });
203
301
  });
204
302
 
205
303
  describe('fetchPlaceDetails', () => {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file Google Maps Script Loader Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces/__tests__
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { isGoogleMapsLoaded, loadGoogleMapsScript } from './loadGoogleMapsScript';
10
+
11
+ describe('loadGoogleMapsScript', () => {
12
+ const originalGoogle = (window as any).google;
13
+
14
+ beforeEach(() => {
15
+ (window as any).google = undefined;
16
+ document.head.innerHTML = '';
17
+ });
18
+
19
+ afterEach(() => {
20
+ (window as any).google = originalGoogle;
21
+ document.head.innerHTML = '';
22
+ });
23
+
24
+ it('detects when Google Maps is loaded', () => {
25
+ expect(isGoogleMapsLoaded()).toBe(false);
26
+
27
+ (window as any).google = { maps: { places: {} } };
28
+
29
+ expect(isGoogleMapsLoaded()).toBe(true);
30
+ });
31
+
32
+ it('resolves immediately when Maps is already available', async () => {
33
+ (window as any).google = { maps: { places: {} } };
34
+ const appendSpy = vi.spyOn(document.head, 'appendChild');
35
+
36
+ await expect(loadGoogleMapsScript('ready-key')).resolves.toBeUndefined();
37
+
38
+ expect(appendSpy).not.toHaveBeenCalled();
39
+ });
40
+
41
+ it('loads the script and resolves after the library initializes', async () => {
42
+ const appendSpy = vi.spyOn(document.head, 'appendChild');
43
+
44
+ const loadPromise = loadGoogleMapsScript('api-key');
45
+
46
+ expect(appendSpy).toHaveBeenCalledTimes(1);
47
+ const scriptEl = appendSpy.mock.calls[0][0] as HTMLScriptElement;
48
+ expect(scriptEl.src).toContain('key=api-key');
49
+
50
+ (window as any).google = { maps: { places: {} } };
51
+ scriptEl.onload?.(new Event('load'));
52
+
53
+ await expect(loadPromise).resolves.toBeUndefined();
54
+ });
55
+
56
+ it('reuses an existing script element', async () => {
57
+ const existingScript = document.createElement('script');
58
+ existingScript.src = 'https://maps.googleapis.com/maps/api/js?key=existing&libraries=places&loading=async';
59
+ document.head.appendChild(existingScript);
60
+
61
+ const appendSpy = vi.spyOn(document.head, 'appendChild');
62
+
63
+ const loadPromise = loadGoogleMapsScript('new-key');
64
+
65
+ (window as any).google = { maps: { places: {} } };
66
+ existingScript.dispatchEvent(new Event('load'));
67
+
68
+ await expect(loadPromise).resolves.toBeUndefined();
69
+ expect(appendSpy).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('rejects when the script fails to load', async () => {
73
+ const appendSpy = vi.spyOn(document.head, 'appendChild');
74
+
75
+ const loadPromise = loadGoogleMapsScript('bad-key');
76
+ const scriptEl = appendSpy.mock.calls[0][0] as HTMLScriptElement;
77
+
78
+ scriptEl.onerror?.(new Event('error'));
79
+
80
+ await expect(loadPromise).rejects.toThrow('Failed to load Google Maps script');
81
+ });
82
+ });
83
+
@@ -60,7 +60,7 @@ describe('[utility] Storage Helpers', () => {
60
60
 
61
61
  it('validates required orgId', () => {
62
62
  expect(() => generateFilePath({ orgId: '' } as any, 'test.jpg'))
63
- .toThrow('orgId is required for file path generation');
63
+ .toThrow('Either orgId or userId is required for file path generation');
64
64
  });
65
65
 
66
66
  it('handles special characters in file names', () => {
@@ -18,33 +18,37 @@ import { createLogger } from '../core/logger';
18
18
  const log = createLogger('StorageHelpers');
19
19
 
20
20
  /**
21
- * Generate a file path based on organization-first structure
21
+ * Generate a file path based on organization-first or user-first structure
22
+ * - If orgId is provided: {orgId}/{folder}/filename
23
+ * - If userId is provided (and orgId is not): users/{userId}/{folder}/filename
22
24
  */
23
25
  export function generateFilePath(options: StorageUploadOptions, fileName: string): string {
24
- const { orgId, isPublic = false, customPath } = options;
26
+ const { orgId, userId, isPublic = false, customPath } = options;
25
27
 
26
-
27
- // Validate required orgId
28
- if (!orgId) {
29
- throw new Error('orgId is required for file path generation');
28
+ // Validate that either orgId or userId is provided
29
+ if (!orgId && !userId) {
30
+ throw new Error('Either orgId or userId is required for file path generation');
30
31
  }
31
32
 
33
+ // Determine base path: organisation-based or user-based
34
+ const basePath = orgId ? orgId : `users/${userId}`;
35
+
32
36
  if (isPublic) {
33
- // Public files go to {orgId}/{folder}/filename
37
+ // Public files go to {basePath}/{folder}/filename
34
38
  if (customPath) {
35
- return `${orgId}/${customPath}/${fileName}`;
39
+ return `${basePath}/${customPath}/${fileName}`;
36
40
  }
37
- return `${orgId}/public/${fileName}`;
41
+ return `${basePath}/public/${fileName}`;
38
42
  }
39
43
 
40
- // Organization-first structure: {orgId}/{folder}/filename
44
+ // Organization-first or user-first structure: {basePath}/{folder}/filename
41
45
  if (customPath) {
42
- return `${orgId}/${customPath}/${fileName}`;
46
+ return `${basePath}/${customPath}/${fileName}`;
43
47
  }
44
48
 
45
49
  // Use customPath if available, otherwise default to files
46
50
  const pathFolder = customPath || 'files';
47
- return `${orgId}/${pathFolder}/${fileName}`;
51
+ return `${basePath}/${pathFolder}/${fileName}`;
48
52
  }
49
53
 
50
54
  /**
@@ -86,7 +90,8 @@ export async function extractFileMetadata(
86
90
  const metadata: StorageFileMetadata = {
87
91
  mimeType: file.type,
88
92
  size: file.size,
89
- orgId: options.orgId,
93
+ ...(options.orgId && { orgId: options.orgId }),
94
+ ...(options.userId && { userId: options.userId }),
90
95
  appName: options.appName || 'pace-core',
91
96
  uploadedBy,
92
97
  uploadedAt: new Date().toISOString(),
@@ -608,8 +613,14 @@ export async function listFiles(
608
613
  // Select bucket based on isPublic flag (default to private files bucket)
609
614
  const bucketName = getBucketName(options.isPublic || false);
610
615
 
611
- // Organization-first structure: {orgId}/{category}/
612
- const pathPrefix = `${options.orgId}/`;
616
+ // Validate that either orgId or userId is provided
617
+ if (!options.orgId && !options.userId) {
618
+ throw new Error('Either orgId or userId is required for listing files');
619
+ }
620
+
621
+ // Organization-first or user-first structure
622
+ const basePath = options.orgId ? options.orgId : `users/${options.userId}`;
623
+ const pathPrefix = `${basePath}/`;
613
624
  const searchPath = options.pathPrefix ? `${pathPrefix}${options.pathPrefix}` : pathPrefix;
614
625
 
615
626
  const { data, error } = await supabase.storage
@@ -634,7 +645,8 @@ export async function listFiles(
634
645
  metadata: {
635
646
  mimeType: item.metadata?.mimetype || 'application/octet-stream',
636
647
  size: item.metadata?.size || 0,
637
- orgId: options.orgId,
648
+ ...(options.orgId && { orgId: options.orgId }),
649
+ ...(options.userId && { userId: options.userId }),
638
650
  appName: options.appName,
639
651
  uploadedBy: 'unknown',
640
652
  uploadedAt: item.created_at || new Date().toISOString(),
@@ -716,13 +728,20 @@ export async function downloadFile(
716
728
  export async function archiveFile(
717
729
  supabase: SupabaseClient,
718
730
  path: string,
719
- options: { appName: string; orgId: string; isPublic?: boolean }
731
+ options: { appName: string; orgId?: string; userId?: string; isPublic?: boolean }
720
732
  ): Promise<{ success: boolean; error?: string }> {
721
733
  try {
722
734
  const bucketName = getBucketName(options.isPublic || false);
723
735
 
724
- // Generate archived path for organization-first structure
725
- const archivedPath = path.replace(`${options.orgId}/`, `archived/${options.orgId}/`);
736
+ // Generate archived path for organization-first or user-first structure
737
+ let archivedPath: string;
738
+ if (options.orgId) {
739
+ archivedPath = path.replace(`${options.orgId}/`, `archived/${options.orgId}/`);
740
+ } else if (options.userId) {
741
+ archivedPath = path.replace(`users/${options.userId}/`, `archived/users/${options.userId}/`);
742
+ } else {
743
+ throw new Error('Either orgId or userId is required for archiving files');
744
+ }
726
745
 
727
746
  // Copy file to archived location
728
747
  const { error: copyError } = await supabase.storage
@@ -5,13 +5,15 @@
5
5
  export interface StorageUploadOptions {
6
6
  /** The app name from rbac_apps */
7
7
  appName: string;
8
- /** Organisation ID for scoping */
9
- orgId: string;
8
+ /** Organisation ID for scoping (required if userId not provided) */
9
+ orgId?: string;
10
+ /** User ID for user-scoped files (required if orgId not provided) */
11
+ userId?: string;
10
12
  /** Whether the file should be publicly accessible */
11
13
  isPublic?: boolean;
12
14
  /** Optional tags for categorisation */
13
15
  tags?: string[];
14
- /** Optional custom path within the app/org structure */
16
+ /** Optional custom path within the app/org/user structure */
15
17
  customPath?: string;
16
18
  /** Optional metadata to store with the file */
17
19
  metadata?: Record<string, any>;
@@ -23,7 +25,8 @@ export interface StorageFileMetadata {
23
25
  width?: number;
24
26
  height?: number;
25
27
  hash?: string;
26
- orgId: string;
28
+ orgId?: string;
29
+ userId?: string;
27
30
  appName: string;
28
31
  uploadedBy: string;
29
32
  uploadedAt: string;
@@ -43,8 +46,10 @@ export interface StorageUploadResult {
43
46
  export interface StorageUrlOptions {
44
47
  /** The app name from rbac_apps */
45
48
  appName: string;
46
- /** Organisation ID for scoping */
47
- orgId: string;
49
+ /** Organisation ID for scoping (required if userId not provided) */
50
+ orgId?: string;
51
+ /** User ID for user-scoped files (required if orgId not provided) */
52
+ userId?: string;
48
53
  /** Expiry time in seconds for signed URLs (default: 3600) */
49
54
  expiresIn?: number;
50
55
  }
@@ -52,8 +57,10 @@ export interface StorageUrlOptions {
52
57
  export interface StorageListOptions {
53
58
  /** The app name from rbac_apps */
54
59
  appName: string;
55
- /** Organisation ID for scoping */
56
- orgId: string;
60
+ /** Organisation ID for scoping (required if userId not provided) */
61
+ orgId?: string;
62
+ /** User ID for user-scoped files (required if orgId not provided) */
63
+ userId?: string;
57
64
  /** Optional path prefix to filter by */
58
65
  pathPrefix?: string;
59
66
  /** Optional tags to filter by */