@jmruthers/pace-core 0.5.189 → 0.5.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core-usage-manifest.json +0 -4
- package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
- package/dist/{DataTable-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
- package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-C4uxosp6.d.ts} +83 -24
- package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
- package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
- package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
- package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
- package/dist/chunk-4QYC5L4K.js.map +1 -0
- package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
- package/dist/chunk-73HSNNOQ.js.map +1 -0
- package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
- package/dist/chunk-DZWK57KZ.js.map +1 -0
- package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
- package/dist/chunk-HQVPB5MZ.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-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
- package/dist/chunk-J2XXC7R5.js.map +1 -0
- package/dist/{chunk-UCQSRW7Z.js → chunk-NIU6J6OX.js} +425 -378
- package/dist/chunk-NIU6J6OX.js.map +1 -0
- package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
- package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
- package/dist/chunk-RUYZKXOD.js.map +1 -0
- package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
- package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
- package/dist/{chunk-MX64ZF6I.js → chunk-STYK4OH2.js} +11 -11
- package/dist/chunk-STYK4OH2.js.map +1 -0
- package/dist/{chunk-YGPFYGA6.js → chunk-VVBAW5A5.js} +822 -498
- package/dist/chunk-VVBAW5A5.js.map +1 -0
- package/dist/chunk-Y4BUBBHD.js +614 -0
- package/dist/chunk-Y4BUBBHD.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 +3 -4
- package/dist/components.js +19 -19
- package/dist/components.js.map +1 -1
- 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 +10 -5
- package/dist/hooks.js +14 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +13 -11
- package/dist/index.js +79 -69
- 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 +76 -12
- package/dist/rbac/index.js +12 -9
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
- 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 +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +2 -2
- package/docs/api/classes/SecureSupabaseClient.md +21 -16
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
- 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 +151 -92
- 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/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 +9 -9
- package/src/__tests__/rls-policies.test.ts +197 -61
- package/src/components/AddressField/AddressField.test.tsx +42 -0
- package/src/components/AddressField/AddressField.tsx +71 -60
- package/src/components/AddressField/README.md +1 -0
- 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 +8 -8
- 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.tsx +5 -5
- 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.unit.test.ts → usePublicEvent.test.ts} +28 -1
- package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEvent.ts +2 -2
- package/src/hooks/public/usePublicFileDisplay.ts +173 -87
- package/src/hooks/useAppConfig.ts +24 -5
- package/src/hooks/useFileDisplay.ts +297 -34
- 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 +21 -0
- package/src/hooks/useSecureDataAccess.test.ts +80 -35
- package/src/hooks/useSecureDataAccess.ts +80 -37
- 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 +6 -1
- package/src/rbac/utils/contextValidator.ts +288 -0
- package/src/rbac/utils/eventContext.ts +48 -2
- package/src/services/EventService.ts +165 -21
- package/src/services/OrganisationService.ts +37 -2
- package/src/services/__tests__/EventService.test.ts +26 -21
- package/src/types/file-reference.ts +13 -10
- package/src/utils/app/appNameResolver.test.ts +346 -73
- package/src/utils/context/superAdminOverride.ts +58 -0
- package/src/utils/file-reference/index.ts +61 -33
- package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
- 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/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
- /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
- /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.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,13 +88,16 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
74
88
|
// Step 2: Extract file metadata (dimensions, hash, etc.)
|
|
75
89
|
const metadata = await extractFileMetadata(file, {
|
|
76
90
|
appName: 'file-reference',
|
|
77
|
-
orgId: options.organisation_id,
|
|
91
|
+
orgId: options.organisation_id || undefined,
|
|
92
|
+
userId: authenticatedUserId || (isUserScoped ? undefined : options.userId), // Use auth.uid() for user-scoped files
|
|
78
93
|
isPublic: options.is_public || false
|
|
79
94
|
}, 'system');
|
|
80
95
|
|
|
81
96
|
// Step 3: Set organisation context in database session before creating file reference
|
|
82
|
-
//
|
|
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
103
|
// This links the storage path to the record in file_references table
|
|
@@ -89,7 +106,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
89
106
|
p_table_name: options.table_name,
|
|
90
107
|
p_record_id: options.record_id,
|
|
91
108
|
p_file_path: filePath, // Storage path from step 1
|
|
92
|
-
p_organisation_id: options.organisation_id,
|
|
109
|
+
p_organisation_id: options.organisation_id ?? null,
|
|
93
110
|
p_app_id: options.app_id,
|
|
94
111
|
p_page_context: options.pageContext,
|
|
95
112
|
p_event_id: options.event_id || null, // Pass event_id for event-based apps
|
|
@@ -101,7 +118,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
101
118
|
...metadata,
|
|
102
119
|
...options.custom_metadata
|
|
103
120
|
},
|
|
104
|
-
p_is_public: options.is_public || false
|
|
121
|
+
p_is_public: options.is_public || false,
|
|
122
|
+
p_user_id: authenticatedUserId || options.userId || null // Pass authenticated user ID for user-scoped files
|
|
105
123
|
});
|
|
106
124
|
|
|
107
125
|
// Step 5: Rollback - if database insert fails, clean up uploaded file
|
|
@@ -131,10 +149,11 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
// Invalidate cache for this file display entry so newly uploaded files appear immediately
|
|
152
|
+
// For user-scoped files, pass null for organisation_id
|
|
134
153
|
invalidateFileDisplayCache(
|
|
135
154
|
options.table_name,
|
|
136
155
|
options.record_id,
|
|
137
|
-
options.organisation_id,
|
|
156
|
+
options.organisation_id || null,
|
|
138
157
|
options.category
|
|
139
158
|
);
|
|
140
159
|
|
|
@@ -145,15 +164,22 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
145
164
|
}
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
async getFileReference(table_name: string, record_id: string, organisation_id
|
|
167
|
+
async getFileReference(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference | null> {
|
|
149
168
|
try {
|
|
150
|
-
|
|
169
|
+
let query = this.supabase
|
|
151
170
|
.from('file_references')
|
|
152
171
|
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
153
172
|
.eq('table_name', table_name)
|
|
154
|
-
.eq('record_id', record_id)
|
|
155
|
-
|
|
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
|
|
|
@@ -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', () => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Google Maps Script Loader Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/GooglePlaces/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { isGoogleMapsLoaded, loadGoogleMapsScript } from './loadGoogleMapsScript';
|
|
10
|
+
|
|
11
|
+
describe('loadGoogleMapsScript', () => {
|
|
12
|
+
const originalGoogle = (window as any).google;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
(window as any).google = undefined;
|
|
16
|
+
document.head.innerHTML = '';
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
(window as any).google = originalGoogle;
|
|
21
|
+
document.head.innerHTML = '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('detects when Google Maps is loaded', () => {
|
|
25
|
+
expect(isGoogleMapsLoaded()).toBe(false);
|
|
26
|
+
|
|
27
|
+
(window as any).google = { maps: { places: {} } };
|
|
28
|
+
|
|
29
|
+
expect(isGoogleMapsLoaded()).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('resolves immediately when Maps is already available', async () => {
|
|
33
|
+
(window as any).google = { maps: { places: {} } };
|
|
34
|
+
const appendSpy = vi.spyOn(document.head, 'appendChild');
|
|
35
|
+
|
|
36
|
+
await expect(loadGoogleMapsScript('ready-key')).resolves.toBeUndefined();
|
|
37
|
+
|
|
38
|
+
expect(appendSpy).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('loads the script and resolves after the library initializes', async () => {
|
|
42
|
+
const appendSpy = vi.spyOn(document.head, 'appendChild');
|
|
43
|
+
|
|
44
|
+
const loadPromise = loadGoogleMapsScript('api-key');
|
|
45
|
+
|
|
46
|
+
expect(appendSpy).toHaveBeenCalledTimes(1);
|
|
47
|
+
const scriptEl = appendSpy.mock.calls[0][0] as HTMLScriptElement;
|
|
48
|
+
expect(scriptEl.src).toContain('key=api-key');
|
|
49
|
+
|
|
50
|
+
(window as any).google = { maps: { places: {} } };
|
|
51
|
+
scriptEl.onload?.(new Event('load'));
|
|
52
|
+
|
|
53
|
+
await expect(loadPromise).resolves.toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('reuses an existing script element', async () => {
|
|
57
|
+
const existingScript = document.createElement('script');
|
|
58
|
+
existingScript.src = 'https://maps.googleapis.com/maps/api/js?key=existing&libraries=places&loading=async';
|
|
59
|
+
document.head.appendChild(existingScript);
|
|
60
|
+
|
|
61
|
+
const appendSpy = vi.spyOn(document.head, 'appendChild');
|
|
62
|
+
|
|
63
|
+
const loadPromise = loadGoogleMapsScript('new-key');
|
|
64
|
+
|
|
65
|
+
(window as any).google = { maps: { places: {} } };
|
|
66
|
+
existingScript.dispatchEvent(new Event('load'));
|
|
67
|
+
|
|
68
|
+
await expect(loadPromise).resolves.toBeUndefined();
|
|
69
|
+
expect(appendSpy).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('rejects when the script fails to load', async () => {
|
|
73
|
+
const appendSpy = vi.spyOn(document.head, 'appendChild');
|
|
74
|
+
|
|
75
|
+
const loadPromise = loadGoogleMapsScript('bad-key');
|
|
76
|
+
const scriptEl = appendSpy.mock.calls[0][0] as HTMLScriptElement;
|
|
77
|
+
|
|
78
|
+
scriptEl.onerror?.(new Event('error'));
|
|
79
|
+
|
|
80
|
+
await expect(loadPromise).rejects.toThrow('Failed to load Google Maps script');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
@@ -60,7 +60,7 @@ describe('[utility] Storage Helpers', () => {
|
|
|
60
60
|
|
|
61
61
|
it('validates required orgId', () => {
|
|
62
62
|
expect(() => generateFilePath({ orgId: '' } as any, 'test.jpg'))
|
|
63
|
-
.toThrow('orgId is required for file path generation');
|
|
63
|
+
.toThrow('Either orgId or userId is required for file path generation');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('handles special characters in file names', () => {
|
|
@@ -18,33 +18,37 @@ import { createLogger } from '../core/logger';
|
|
|
18
18
|
const log = createLogger('StorageHelpers');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Generate a file path based on organization-first structure
|
|
21
|
+
* Generate a file path based on organization-first or user-first structure
|
|
22
|
+
* - If orgId is provided: {orgId}/{folder}/filename
|
|
23
|
+
* - If userId is provided (and orgId is not): users/{userId}/{folder}/filename
|
|
22
24
|
*/
|
|
23
25
|
export function generateFilePath(options: StorageUploadOptions, fileName: string): string {
|
|
24
|
-
const { orgId, isPublic = false, customPath } = options;
|
|
26
|
+
const { orgId, userId, isPublic = false, customPath } = options;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
throw new Error('orgId is required for file path generation');
|
|
28
|
+
// Validate that either orgId or userId is provided
|
|
29
|
+
if (!orgId && !userId) {
|
|
30
|
+
throw new Error('Either orgId or userId is required for file path generation');
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// Determine base path: organisation-based or user-based
|
|
34
|
+
const basePath = orgId ? orgId : `users/${userId}`;
|
|
35
|
+
|
|
32
36
|
if (isPublic) {
|
|
33
|
-
// Public files go to {
|
|
37
|
+
// Public files go to {basePath}/{folder}/filename
|
|
34
38
|
if (customPath) {
|
|
35
|
-
return `${
|
|
39
|
+
return `${basePath}/${customPath}/${fileName}`;
|
|
36
40
|
}
|
|
37
|
-
return `${
|
|
41
|
+
return `${basePath}/public/${fileName}`;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
// Organization-first structure: {
|
|
44
|
+
// Organization-first or user-first structure: {basePath}/{folder}/filename
|
|
41
45
|
if (customPath) {
|
|
42
|
-
return `${
|
|
46
|
+
return `${basePath}/${customPath}/${fileName}`;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// Use customPath if available, otherwise default to files
|
|
46
50
|
const pathFolder = customPath || 'files';
|
|
47
|
-
return `${
|
|
51
|
+
return `${basePath}/${pathFolder}/${fileName}`;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -86,7 +90,8 @@ export async function extractFileMetadata(
|
|
|
86
90
|
const metadata: StorageFileMetadata = {
|
|
87
91
|
mimeType: file.type,
|
|
88
92
|
size: file.size,
|
|
89
|
-
orgId: options.orgId,
|
|
93
|
+
...(options.orgId && { orgId: options.orgId }),
|
|
94
|
+
...(options.userId && { userId: options.userId }),
|
|
90
95
|
appName: options.appName || 'pace-core',
|
|
91
96
|
uploadedBy,
|
|
92
97
|
uploadedAt: new Date().toISOString(),
|
|
@@ -608,8 +613,14 @@ export async function listFiles(
|
|
|
608
613
|
// Select bucket based on isPublic flag (default to private files bucket)
|
|
609
614
|
const bucketName = getBucketName(options.isPublic || false);
|
|
610
615
|
|
|
611
|
-
//
|
|
612
|
-
|
|
616
|
+
// Validate that either orgId or userId is provided
|
|
617
|
+
if (!options.orgId && !options.userId) {
|
|
618
|
+
throw new Error('Either orgId or userId is required for listing files');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Organization-first or user-first structure
|
|
622
|
+
const basePath = options.orgId ? options.orgId : `users/${options.userId}`;
|
|
623
|
+
const pathPrefix = `${basePath}/`;
|
|
613
624
|
const searchPath = options.pathPrefix ? `${pathPrefix}${options.pathPrefix}` : pathPrefix;
|
|
614
625
|
|
|
615
626
|
const { data, error } = await supabase.storage
|
|
@@ -634,7 +645,8 @@ export async function listFiles(
|
|
|
634
645
|
metadata: {
|
|
635
646
|
mimeType: item.metadata?.mimetype || 'application/octet-stream',
|
|
636
647
|
size: item.metadata?.size || 0,
|
|
637
|
-
orgId: options.orgId,
|
|
648
|
+
...(options.orgId && { orgId: options.orgId }),
|
|
649
|
+
...(options.userId && { userId: options.userId }),
|
|
638
650
|
appName: options.appName,
|
|
639
651
|
uploadedBy: 'unknown',
|
|
640
652
|
uploadedAt: item.created_at || new Date().toISOString(),
|
|
@@ -716,13 +728,20 @@ export async function downloadFile(
|
|
|
716
728
|
export async function archiveFile(
|
|
717
729
|
supabase: SupabaseClient,
|
|
718
730
|
path: string,
|
|
719
|
-
options: { appName: string; orgId
|
|
731
|
+
options: { appName: string; orgId?: string; userId?: string; isPublic?: boolean }
|
|
720
732
|
): Promise<{ success: boolean; error?: string }> {
|
|
721
733
|
try {
|
|
722
734
|
const bucketName = getBucketName(options.isPublic || false);
|
|
723
735
|
|
|
724
|
-
// Generate archived path for organization-first structure
|
|
725
|
-
|
|
736
|
+
// Generate archived path for organization-first or user-first structure
|
|
737
|
+
let archivedPath: string;
|
|
738
|
+
if (options.orgId) {
|
|
739
|
+
archivedPath = path.replace(`${options.orgId}/`, `archived/${options.orgId}/`);
|
|
740
|
+
} else if (options.userId) {
|
|
741
|
+
archivedPath = path.replace(`users/${options.userId}/`, `archived/users/${options.userId}/`);
|
|
742
|
+
} else {
|
|
743
|
+
throw new Error('Either orgId or userId is required for archiving files');
|
|
744
|
+
}
|
|
726
745
|
|
|
727
746
|
// Copy file to archived location
|
|
728
747
|
const { error: copyError } = await supabase.storage
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
export interface StorageUploadOptions {
|
|
6
6
|
/** The app name from rbac_apps */
|
|
7
7
|
appName: string;
|
|
8
|
-
/** Organisation ID for scoping */
|
|
9
|
-
orgId
|
|
8
|
+
/** Organisation ID for scoping (required if userId not provided) */
|
|
9
|
+
orgId?: string;
|
|
10
|
+
/** User ID for user-scoped files (required if orgId not provided) */
|
|
11
|
+
userId?: string;
|
|
10
12
|
/** Whether the file should be publicly accessible */
|
|
11
13
|
isPublic?: boolean;
|
|
12
14
|
/** Optional tags for categorisation */
|
|
13
15
|
tags?: string[];
|
|
14
|
-
/** Optional custom path within the app/org structure */
|
|
16
|
+
/** Optional custom path within the app/org/user structure */
|
|
15
17
|
customPath?: string;
|
|
16
18
|
/** Optional metadata to store with the file */
|
|
17
19
|
metadata?: Record<string, any>;
|
|
@@ -23,7 +25,8 @@ export interface StorageFileMetadata {
|
|
|
23
25
|
width?: number;
|
|
24
26
|
height?: number;
|
|
25
27
|
hash?: string;
|
|
26
|
-
orgId
|
|
28
|
+
orgId?: string;
|
|
29
|
+
userId?: string;
|
|
27
30
|
appName: string;
|
|
28
31
|
uploadedBy: string;
|
|
29
32
|
uploadedAt: string;
|
|
@@ -43,8 +46,10 @@ export interface StorageUploadResult {
|
|
|
43
46
|
export interface StorageUrlOptions {
|
|
44
47
|
/** The app name from rbac_apps */
|
|
45
48
|
appName: string;
|
|
46
|
-
/** Organisation ID for scoping */
|
|
47
|
-
orgId
|
|
49
|
+
/** Organisation ID for scoping (required if userId not provided) */
|
|
50
|
+
orgId?: string;
|
|
51
|
+
/** User ID for user-scoped files (required if orgId not provided) */
|
|
52
|
+
userId?: string;
|
|
48
53
|
/** Expiry time in seconds for signed URLs (default: 3600) */
|
|
49
54
|
expiresIn?: number;
|
|
50
55
|
}
|
|
@@ -52,8 +57,10 @@ export interface StorageUrlOptions {
|
|
|
52
57
|
export interface StorageListOptions {
|
|
53
58
|
/** The app name from rbac_apps */
|
|
54
59
|
appName: string;
|
|
55
|
-
/** Organisation ID for scoping */
|
|
56
|
-
orgId
|
|
60
|
+
/** Organisation ID for scoping (required if userId not provided) */
|
|
61
|
+
orgId?: string;
|
|
62
|
+
/** User ID for user-scoped files (required if orgId not provided) */
|
|
63
|
+
userId?: string;
|
|
57
64
|
/** Optional path prefix to filter by */
|
|
58
65
|
pathPrefix?: string;
|
|
59
66
|
/** Optional tags to filter by */
|