@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.
- package/core-usage-manifest.json +0 -4
- package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
- package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
- package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
- package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
- package/dist/chunk-G37KK66H.js.map +1 -0
- package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
- package/dist/chunk-HW3OVDUF.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
- package/dist/chunk-I7PSE6JW.js.map +1 -0
- package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
- package/dist/chunk-LOMZXPSN.js.map +1 -0
- package/dist/chunk-OETXORNB.js +614 -0
- package/dist/chunk-OETXORNB.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
- package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
- package/dist/chunk-VRGWKHDB.js.map +1 -0
- package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
- package/dist/chunk-XYXSXPUK.js.map +1 -0
- package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
- package/dist/chunk-ZSAAAMVR.js.map +1 -0
- package/dist/components.d.ts +5 -6
- package/dist/components.js +19 -19
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
- package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
- package/dist/hooks.d.ts +20 -15
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.js +86 -81
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +77 -13
- package/dist/rbac/index.js +12 -9
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
- package/dist/utils.d.ts +8 -8
- package/dist/utils.js +16 -16
- package/docs/README.md +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +2 -2
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +2 -2
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +25 -20
- package/docs/api/classes/StorageUtils.md +7 -4
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +20 -6
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +9 -9
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +62 -16
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +2 -2
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +26 -12
- package/docs/api/interfaces/FileUploadProps.md +30 -19
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
- package/docs/api/interfaces/NavigationContextType.md +9 -9
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +7 -7
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +8 -8
- package/docs/api/interfaces/PagePermissionContextType.md +8 -8
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +2 -2
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +3 -11
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +10 -10
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +9 -9
- package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +4 -4
- package/docs/api/interfaces/StorageFileInfo.md +7 -7
- package/docs/api/interfaces/StorageFileMetadata.md +25 -14
- package/docs/api/interfaces/StorageListOptions.md +22 -9
- package/docs/api/interfaces/StorageListResult.md +4 -4
- package/docs/api/interfaces/StorageUploadOptions.md +21 -8
- package/docs/api/interfaces/StorageUploadResult.md +6 -6
- package/docs/api/interfaces/StorageUrlOptions.md +19 -6
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
- package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +165 -106
- package/docs/api-reference/components.md +15 -7
- package/docs/api-reference/providers.md +2 -2
- package/docs/api-reference/rpc-functions.md +1 -0
- package/docs/best-practices/README.md +1 -1
- package/docs/best-practices/deployment.md +8 -8
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/getting-started/installation-guide.md +4 -4
- package/docs/getting-started/quick-start.md +3 -3
- package/docs/migration/MIGRATION_GUIDE.md +3 -3
- package/docs/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- package/docs/rbac/compliance/compliance-guide.md +2 -2
- package/docs/rbac/event-based-apps.md +2 -2
- package/docs/rbac/getting-started.md +2 -2
- package/docs/rbac/quick-start.md +2 -2
- package/docs/security/README.md +4 -4
- package/docs/standards/07-rbac-and-rls-standard.md +430 -7
- package/docs/troubleshooting/README.md +2 -2
- package/docs/troubleshooting/migration.md +3 -3
- package/package.json +1 -3
- package/scripts/check-pace-core-compliance.cjs +1 -1
- package/scripts/check-pace-core-compliance.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +301 -0
- package/src/__tests__/public-recipe-view.test.ts +19 -19
- package/src/__tests__/rls-policies.test.ts +210 -74
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +7 -6
- package/src/components/Alert/Alert.test.tsx +50 -10
- package/src/components/Alert/Alert.tsx +5 -3
- package/src/components/Avatar/Avatar.test.tsx +95 -43
- package/src/components/Avatar/Avatar.tsx +16 -16
- package/src/components/Button/Button.test.tsx +2 -1
- package/src/components/Button/Button.tsx +3 -3
- package/src/components/Calendar/Calendar.test.tsx +53 -37
- package/src/components/Calendar/Calendar.tsx +409 -82
- package/src/components/Card/Card.test.tsx +7 -4
- package/src/components/Card/Card.tsx +3 -6
- package/src/components/Checkbox/Checkbox.tsx +2 -2
- package/src/components/DataTable/components/ActionButtons.tsx +5 -5
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
- package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
- package/src/components/DataTable/components/DataTableBody.tsx +12 -12
- package/src/components/DataTable/components/DataTableCore.tsx +3 -3
- package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
- package/src/components/DataTable/components/EditableRow.tsx +2 -2
- package/src/components/DataTable/components/EmptyState.tsx +3 -3
- package/src/components/DataTable/components/GroupHeader.tsx +2 -2
- package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
- package/src/components/DataTable/components/ImportModal.tsx +4 -4
- package/src/components/DataTable/components/LoadingState.tsx +1 -1
- package/src/components/DataTable/components/PaginationControls.tsx +11 -11
- package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
- package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
- package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
- package/src/components/Dialog/Dialog.tsx +2 -2
- package/src/components/EventSelector/EventSelector.tsx +7 -7
- package/src/components/FileDisplay/FileDisplay.tsx +291 -179
- package/src/components/FileUpload/FileUpload.tsx +7 -4
- package/src/components/Header/Header.test.tsx +28 -0
- package/src/components/Header/Header.tsx +22 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
- package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
- package/src/components/PaceAppLayout/test-setup.tsx +1 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
- package/src/components/Progress/Progress.test.tsx +18 -19
- package/src/components/Progress/Progress.tsx +31 -32
- package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
- package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
- package/src/components/Select/Select.test.tsx +4 -1
- package/src/components/Select/Select.tsx +65 -20
- package/src/components/Switch/Switch.test.tsx +2 -1
- package/src/components/Switch/Switch.tsx +1 -1
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/components/Tooltip/Tooltip.test.tsx +8 -2
- package/src/components/UserMenu/UserMenu.tsx +3 -3
- package/src/eslint-rules/pace-core-compliance.cjs +0 -2
- package/src/eslint-rules/pace-core-compliance.js +0 -2
- package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
- package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
- package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
- package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
- package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +10 -10
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +298 -36
- package/src/hooks/useFileReference.ts +56 -11
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useInactivityTracker.ts +16 -7
- package/src/hooks/usePermissionCache.test.ts +85 -8
- package/src/hooks/useQueryCache.ts +27 -6
- package/src/hooks/useSecureDataAccess.test.ts +87 -42
- package/src/hooks/useSecureDataAccess.ts +95 -48
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- package/src/providers/services/EventServiceProvider.tsx +37 -17
- package/src/providers/services/InactivityServiceProvider.tsx +4 -4
- package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
- package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
- package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
- package/src/rbac/api.ts +240 -36
- package/src/rbac/cache-invalidation.ts +21 -7
- package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
- package/src/rbac/components/NavigationGuard.tsx +23 -63
- package/src/rbac/components/NavigationProvider.test.tsx +52 -23
- package/src/rbac/components/NavigationProvider.tsx +13 -11
- package/src/rbac/components/PagePermissionGuard.tsx +77 -203
- package/src/rbac/components/PagePermissionProvider.tsx +13 -11
- package/src/rbac/components/PermissionEnforcer.tsx +24 -62
- package/src/rbac/components/RoleBasedRouter.tsx +14 -12
- package/src/rbac/components/SecureDataProvider.tsx +13 -11
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
- package/src/rbac/engine.ts +4 -2
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
- package/src/rbac/hooks/index.ts +3 -0
- package/src/rbac/hooks/useCan.test.ts +101 -53
- package/src/rbac/hooks/usePermissions.ts +108 -41
- package/src/rbac/hooks/useRBAC.test.ts +11 -3
- package/src/rbac/hooks/useRBAC.ts +83 -40
- package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
- package/src/rbac/hooks/useResolvedScope.ts +128 -70
- package/src/rbac/hooks/useSecureSupabase.ts +36 -19
- package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
- package/src/rbac/request-deduplication.ts +1 -1
- package/src/rbac/secureClient.ts +72 -12
- package/src/rbac/security.ts +29 -23
- package/src/rbac/types.ts +10 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
- package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +52 -3
- package/src/services/AuthService.ts +37 -8
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +125 -137
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/file-reference.ts +13 -10
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +65 -37
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- package/src/utils/storage/helpers.test.ts +1 -1
- package/src/utils/storage/helpers.ts +38 -19
- package/src/utils/storage/types.ts +15 -8
- package/src/utils/validation/__tests__/csrf.test.ts +105 -0
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
- package/src/vite-env.d.ts +2 -2
- package/dist/chunk-3GOZZZYH.js.map +0 -1
- package/dist/chunk-DDM4CCYT.js.map +0 -1
- package/dist/chunk-E7UAOUMY.js +0 -75
- package/dist/chunk-E7UAOUMY.js.map +0 -1
- package/dist/chunk-F2IMUDXZ.js.map +0 -1
- package/dist/chunk-HEHYGYOX.js.map +0 -1
- package/dist/chunk-IM4QE42D.js.map +0 -1
- package/dist/chunk-MX64ZF6I.js.map +0 -1
- package/dist/chunk-SAUPYVLF.js.map +0 -1
- package/dist/chunk-THRPYOFK.js.map +0 -1
- package/dist/chunk-UCQSRW7Z.js.map +0 -1
- package/dist/chunk-VGZZXKBR.js.map +0 -1
- package/dist/chunk-YGPFYGA6.js.map +0 -1
- package/dist/chunk-YHCN776L.js.map +0 -1
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
- /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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
83
|
-
|
|
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
|
|
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('
|
|
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
|
|
167
|
+
async getFileReference(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference | null> {
|
|
149
168
|
try {
|
|
150
|
-
|
|
151
|
-
.from('
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
|
|
@@ -53,10 +53,10 @@ export function generateRequestKey(
|
|
|
53
53
|
* @example
|
|
54
54
|
* ```ts
|
|
55
55
|
* const data = await getOrCreateRequest(
|
|
56
|
-
* 'GET:
|
|
56
|
+
* 'GET:core_person:{"user_id":"123"}',
|
|
57
57
|
* async () => {
|
|
58
58
|
* const { data } = await supabase
|
|
59
|
-
* .from('
|
|
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
|
-
* '
|
|
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('
|
|
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('
|
|
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
|
-
'
|
|
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', '
|
|
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
|
-
|
|
105
|
-
|
|
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('
|
|
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', () => {
|