@jmruthers/pace-core 0.5.189 → 0.5.191

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 (438) 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-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  4. package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
  5. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
  6. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  7. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
  8. package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
  9. package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
  10. package/dist/chunk-6LTQQAT6.js.map +1 -0
  11. package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
  12. package/dist/chunk-6TQDD426.js.map +1 -0
  13. package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
  14. package/dist/chunk-G37KK66H.js.map +1 -0
  15. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  16. package/dist/chunk-HW3OVDUF.js.map +1 -0
  17. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  18. package/dist/chunk-I7PSE6JW.js.map +1 -0
  19. package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
  20. package/dist/chunk-LOMZXPSN.js.map +1 -0
  21. package/dist/chunk-OETXORNB.js +614 -0
  22. package/dist/chunk-OETXORNB.js.map +1 -0
  23. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  24. package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
  25. package/dist/chunk-ROXMHMY2.js.map +1 -0
  26. package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
  27. package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
  28. package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
  29. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  30. package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
  31. package/dist/chunk-VRGWKHDB.js.map +1 -0
  32. package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
  33. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  34. package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
  35. package/dist/chunk-XYXSXPUK.js.map +1 -0
  36. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  37. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  38. package/dist/components.d.ts +5 -6
  39. package/dist/components.js +19 -19
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  42. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  43. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  44. package/dist/hooks.d.ts +20 -15
  45. package/dist/hooks.js +14 -8
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/index.d.ts +17 -15
  48. package/dist/index.js +86 -81
  49. package/dist/index.js.map +1 -1
  50. package/dist/providers.d.ts +3 -3
  51. package/dist/providers.js +3 -1
  52. package/dist/rbac/index.d.ts +77 -13
  53. package/dist/rbac/index.js +12 -9
  54. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  55. package/dist/types.d.ts +3 -3
  56. package/dist/types.js +1 -1
  57. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
  58. package/dist/utils.d.ts +8 -8
  59. package/dist/utils.js +16 -16
  60. package/docs/README.md +2 -2
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +2 -2
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +2 -2
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +1 -1
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +5 -5
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  73. package/docs/api/classes/SecureSupabaseClient.md +25 -20
  74. package/docs/api/classes/StorageUtils.md +7 -4
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +20 -6
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +2 -2
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  109. package/docs/api/interfaces/FileUploadProps.md +30 -19
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  120. package/docs/api/interfaces/NavigationContextType.md +9 -9
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  133. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +2 -2
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  139. package/docs/api/interfaces/ProgressProps.md +3 -11
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  175. package/docs/api/interfaces/RouteConfig.md +10 -10
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  178. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +4 -4
  182. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  183. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  184. package/docs/api/interfaces/StorageListOptions.md +22 -9
  185. package/docs/api/interfaces/StorageListResult.md +4 -4
  186. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  187. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  188. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +11 -11
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +165 -106
  218. package/docs/api-reference/components.md +15 -7
  219. package/docs/api-reference/providers.md +2 -2
  220. package/docs/api-reference/rpc-functions.md +1 -0
  221. package/docs/best-practices/README.md +1 -1
  222. package/docs/best-practices/deployment.md +8 -8
  223. package/docs/getting-started/examples/README.md +2 -2
  224. package/docs/getting-started/installation-guide.md +4 -4
  225. package/docs/getting-started/quick-start.md +3 -3
  226. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  227. package/docs/migration/README.md +18 -0
  228. package/docs/migration/database-changes-december-2025.md +767 -0
  229. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  230. package/docs/rbac/compliance/compliance-guide.md +2 -2
  231. package/docs/rbac/event-based-apps.md +2 -2
  232. package/docs/rbac/getting-started.md +2 -2
  233. package/docs/rbac/quick-start.md +2 -2
  234. package/docs/security/README.md +4 -4
  235. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  236. package/docs/troubleshooting/README.md +2 -2
  237. package/docs/troubleshooting/migration.md +3 -3
  238. package/package.json +1 -3
  239. package/scripts/check-pace-core-compliance.cjs +1 -1
  240. package/scripts/check-pace-core-compliance.js +1 -1
  241. package/src/__tests__/fixtures/supabase.ts +301 -0
  242. package/src/__tests__/public-recipe-view.test.ts +19 -19
  243. package/src/__tests__/rls-policies.test.ts +210 -74
  244. package/src/components/AddressField/AddressField.test.tsx +42 -0
  245. package/src/components/AddressField/AddressField.tsx +71 -60
  246. package/src/components/AddressField/README.md +7 -6
  247. package/src/components/Alert/Alert.test.tsx +50 -10
  248. package/src/components/Alert/Alert.tsx +5 -3
  249. package/src/components/Avatar/Avatar.test.tsx +95 -43
  250. package/src/components/Avatar/Avatar.tsx +16 -16
  251. package/src/components/Button/Button.test.tsx +2 -1
  252. package/src/components/Button/Button.tsx +3 -3
  253. package/src/components/Calendar/Calendar.test.tsx +53 -37
  254. package/src/components/Calendar/Calendar.tsx +409 -82
  255. package/src/components/Card/Card.test.tsx +7 -4
  256. package/src/components/Card/Card.tsx +3 -6
  257. package/src/components/Checkbox/Checkbox.tsx +2 -2
  258. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  259. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  260. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  261. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  262. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  263. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  264. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  265. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  266. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  267. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  268. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  269. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  270. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  271. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  272. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  273. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  274. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  275. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  276. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  277. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  278. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  279. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  280. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  281. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  282. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  284. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  285. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  286. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  287. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  288. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  289. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  290. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  291. package/src/components/Dialog/Dialog.tsx +2 -2
  292. package/src/components/EventSelector/EventSelector.tsx +7 -7
  293. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  294. package/src/components/FileUpload/FileUpload.tsx +7 -4
  295. package/src/components/Header/Header.test.tsx +28 -0
  296. package/src/components/Header/Header.tsx +22 -9
  297. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  298. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  299. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  301. package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
  302. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  303. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  304. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  305. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  306. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  307. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  308. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  309. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  310. package/src/components/Progress/Progress.test.tsx +18 -19
  311. package/src/components/Progress/Progress.tsx +31 -32
  312. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  313. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  314. package/src/components/Select/Select.test.tsx +4 -1
  315. package/src/components/Select/Select.tsx +65 -20
  316. package/src/components/Switch/Switch.test.tsx +2 -1
  317. package/src/components/Switch/Switch.tsx +1 -1
  318. package/src/components/Toast/Toast.tsx +1 -1
  319. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  320. package/src/components/UserMenu/UserMenu.tsx +3 -3
  321. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  322. package/src/eslint-rules/pace-core-compliance.js +0 -2
  323. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  324. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  325. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  326. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  327. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  328. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  329. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
  330. package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
  331. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  332. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
  333. package/src/hooks/index.ts +1 -1
  334. package/src/hooks/public/usePublicEvent.ts +10 -10
  335. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  336. package/src/hooks/useAppConfig.ts +24 -5
  337. package/src/hooks/useFileDisplay.ts +298 -36
  338. package/src/hooks/useFileReference.ts +56 -11
  339. package/src/hooks/useFileUrl.ts +1 -1
  340. package/src/hooks/useInactivityTracker.ts +16 -7
  341. package/src/hooks/usePermissionCache.test.ts +85 -8
  342. package/src/hooks/useQueryCache.ts +27 -6
  343. package/src/hooks/useSecureDataAccess.test.ts +87 -42
  344. package/src/hooks/useSecureDataAccess.ts +95 -48
  345. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  346. package/src/providers/services/EventServiceProvider.tsx +37 -17
  347. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  348. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  349. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  350. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  351. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  352. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  353. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  354. package/src/rbac/api.ts +240 -36
  355. package/src/rbac/cache-invalidation.ts +21 -7
  356. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  357. package/src/rbac/components/NavigationGuard.tsx +23 -63
  358. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  359. package/src/rbac/components/NavigationProvider.tsx +13 -11
  360. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  361. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  362. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  363. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  364. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  365. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  366. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  367. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  368. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  369. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  370. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  371. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  372. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  373. package/src/rbac/engine.ts +4 -2
  374. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  375. package/src/rbac/hooks/index.ts +3 -0
  376. package/src/rbac/hooks/useCan.test.ts +101 -53
  377. package/src/rbac/hooks/usePermissions.ts +108 -41
  378. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  379. package/src/rbac/hooks/useRBAC.ts +83 -40
  380. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  381. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  382. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  383. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  384. package/src/rbac/request-deduplication.ts +1 -1
  385. package/src/rbac/secureClient.ts +72 -12
  386. package/src/rbac/security.ts +29 -23
  387. package/src/rbac/types.ts +10 -0
  388. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  389. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  390. package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
  391. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
  392. package/src/rbac/utils/contextValidator.ts +288 -0
  393. package/src/rbac/utils/eventContext.ts +52 -3
  394. package/src/services/AuthService.ts +37 -8
  395. package/src/services/EventService.ts +165 -21
  396. package/src/services/OrganisationService.ts +125 -137
  397. package/src/services/__tests__/EventService.test.ts +26 -21
  398. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  399. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  400. package/src/types/database.generated.ts +166 -201
  401. package/src/types/file-reference.ts +13 -10
  402. package/src/types/supabase.ts +2 -2
  403. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  404. package/src/utils/app/appNameResolver.test.ts +346 -73
  405. package/src/utils/context/superAdminOverride.ts +58 -0
  406. package/src/utils/file-reference/index.ts +65 -37
  407. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  408. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  409. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  410. package/src/utils/google-places/types.ts +1 -1
  411. package/src/utils/request-deduplication.ts +4 -4
  412. package/src/utils/security/secureDataAccess.test.ts +1 -1
  413. package/src/utils/security/secureDataAccess.ts +7 -4
  414. package/src/utils/storage/README.md +1 -1
  415. package/src/utils/storage/helpers.test.ts +1 -1
  416. package/src/utils/storage/helpers.ts +38 -19
  417. package/src/utils/storage/types.ts +15 -8
  418. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  419. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  420. package/src/vite-env.d.ts +2 -2
  421. package/dist/chunk-3GOZZZYH.js.map +0 -1
  422. package/dist/chunk-DDM4CCYT.js.map +0 -1
  423. package/dist/chunk-E7UAOUMY.js +0 -75
  424. package/dist/chunk-E7UAOUMY.js.map +0 -1
  425. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  426. package/dist/chunk-HEHYGYOX.js.map +0 -1
  427. package/dist/chunk-IM4QE42D.js.map +0 -1
  428. package/dist/chunk-MX64ZF6I.js.map +0 -1
  429. package/dist/chunk-SAUPYVLF.js.map +0 -1
  430. package/dist/chunk-THRPYOFK.js.map +0 -1
  431. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  432. package/dist/chunk-VGZZXKBR.js.map +0 -1
  433. package/dist/chunk-YGPFYGA6.js.map +0 -1
  434. package/dist/chunk-YHCN776L.js.map +0 -1
  435. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
  436. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  437. /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
  438. /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,22 +88,25 @@ 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
- // This links the storage path to the record in file_references table
103
+ // This links the storage path to the record in core_file_references table
87
104
  const { data, error } = await this.supabase
88
105
  .rpc('data_file_reference_create', {
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
@@ -119,7 +137,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
119
137
 
120
138
  // Get the created file reference
121
139
  const { data: fileRef, error: fetchError } = await this.supabase
122
- .from('file_references')
140
+ .from('core_file_references')
123
141
  .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
124
142
  .eq('id', data)
125
143
  .single();
@@ -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
151
- .from('file_references')
169
+ let query = this.supabase
170
+ .from('core_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
 
@@ -238,7 +265,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
238
265
  async updateFileReference(id: string, updates: Partial<FileReference>): Promise<FileReference> {
239
266
  try {
240
267
  const { data, error } = await this.supabase
241
- .from('file_references')
268
+ .from('core_file_references')
242
269
  .update(updates)
243
270
  .eq('id', id)
244
271
  .select()
@@ -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', () => {
@@ -411,7 +411,7 @@ export function parseAddressComponents(
411
411
  * Create parsed address from Google Places API place result
412
412
  *
413
413
  * @param place - Place result from Google Places Details API
414
- * @returns Parsed address matching pace_address table structure
414
+ * @returns Parsed address matching core_address table structure
415
415
  */
416
416
  export function createAddressFromPlaceResult(
417
417
  place: {
@@ -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
+
@@ -61,7 +61,7 @@ export interface GooglePlaceDetailsResponse {
61
61
  }
62
62
 
63
63
  /**
64
- * Parsed address matching pace_address table structure
64
+ * Parsed address matching core_address table structure
65
65
  */
66
66
  export interface ParsedAddress {
67
67
  place_id: string;
@@ -53,10 +53,10 @@ export function generateRequestKey(
53
53
  * @example
54
54
  * ```ts
55
55
  * const data = await getOrCreateRequest(
56
- * 'GET:pace_person:{"user_id":"123"}',
56
+ * 'GET:core_person:{"user_id":"123"}',
57
57
  * async () => {
58
58
  * const { data } = await supabase
59
- * .from('pace_person')
59
+ * .from('core_person')
60
60
  * .select('id, first_name')
61
61
  * .eq('user_id', '123')
62
62
  * .single();
@@ -138,12 +138,12 @@ export function getInFlightRequestStats(): {
138
138
  * ```ts
139
139
  * const person = await deduplicatedQuery(
140
140
  * supabase,
141
- * 'pace_person',
141
+ * 'core_person',
142
142
  * { user_id: userId },
143
143
  * 'id, first_name, last_name',
144
144
  * async () => {
145
145
  * const { data } = await supabase
146
- * .from('pace_person')
146
+ * .from('core_person')
147
147
  * .select('id, first_name, last_name')
148
148
  * .eq('user_id', userId)
149
149
  * .single();
@@ -100,7 +100,7 @@ describe('secureDataAccess', () => {
100
100
 
101
101
  describe('ensureOrganisationColumn', () => {
102
102
  it('should return true for tables with organisation_id column', () => {
103
- expect(secureDataAccess.ensureOrganisationColumn('event')).toBe(true);
103
+ expect(secureDataAccess.ensureOrganisationColumn('core_events')).toBe(true);
104
104
  expect(secureDataAccess.ensureOrganisationColumn('organisation_settings')).toBe(true);
105
105
  expect(secureDataAccess.ensureOrganisationColumn('rbac_event_app_roles')).toBe(true);
106
106
  });
@@ -94,15 +94,18 @@ export const createSecureDataAccess = (
94
94
  const ensureOrganisationColumn = (table: string): boolean => {
95
95
  // This is a simplified check - in production you might want to cache this
96
96
  const tablesWithOrganisation = [
97
- 'event', 'organisation_settings',
97
+ 'core_events', 'organisation_settings',
98
98
  'rbac_event_app_roles', 'rbac_organisation_roles',
99
99
  // SECURITY: Phase 2 additions - complete organisation table mapping
100
100
  'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
101
101
  // SECURITY: Emergency additions for Phase 1 fixes
102
- 'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member',
102
+ 'cake_meal', 'cake_mealtype', 'core_person', 'pace_person',
103
+ // NOTE: core_member, medi_profile, core_contact, core_consent, core_identification, core_qualification
104
+ // are now person-scoped (not organisation-scoped) - removed from this list
103
105
  // SECURITY: Phase 3A additions - medical and personal data
104
- 'medi_profile', 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
105
- 'pace_consent', 'pace_contact', 'pace_identification', 'pace_identification_type', 'pace_qualification',
106
+ // NOTE: medi_condition, medi_diet, medi_action_plan, medi_profile_versions are now person-scoped
107
+ // (via medi_profile) - removed from this list
108
+ // core_identification_type remains organisation-scoped (lookup table)
106
109
  'form_responses', 'form_response_values', 'forms',
107
110
  // SECURITY: Phase 3B additions - remaining critical tables
108
111
  'invoice', 'line_item', 'credit_balance', 'payment_method',
@@ -342,7 +342,7 @@ Enable debug logging by checking the browser console for detailed information ab
342
342
  ```typescript
343
343
  // Check file reference status
344
344
  const { data } = await supabase
345
- .from('file_references')
345
+ .from('core_file_references')
346
346
  .select('*')
347
347
  .eq('table_name', 'person')
348
348
  .eq('record_id', recordId)
@@ -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', () => {