@jmruthers/pace-core 0.5.190 → 0.5.193
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/dist/{AuthService-CbP_utw2.d.ts → AuthService-DjnJHDtC.d.ts} +1 -0
- package/dist/{DataTable-ON3IXISJ.js → DataTable-5FU7IESH.js} +7 -6
- package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-C0Sm_e5k.d.ts} +4 -2
- package/dist/{UnifiedAuthProvider-BYA9qB-o.d.ts → UnifiedAuthProvider-185Ih4dj.d.ts} +2 -0
- package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-RGJTDE2C.js} +3 -3
- package/dist/{api-I6UCQ5S6.js → api-N774RPUA.js} +2 -2
- package/dist/chunk-6C4YBBJM 5.js +628 -0
- package/dist/chunk-7D4SUZUM.js 2.map +1 -0
- package/dist/{chunk-73HSNNOQ.js → chunk-7EQTDTTJ.js} +47 -74
- package/dist/chunk-7EQTDTTJ.js 2.map +1 -0
- package/dist/chunk-7EQTDTTJ.js.map +1 -0
- package/dist/{chunk-J2XXC7R5.js → chunk-7FLMSG37.js} +409 -244
- package/dist/chunk-7FLMSG37.js 2.map +1 -0
- package/dist/chunk-7FLMSG37.js.map +1 -0
- package/dist/{chunk-NIU6J6OX.js → chunk-BC4IJKSL.js} +23 -32
- package/dist/chunk-BC4IJKSL.js.map +1 -0
- package/dist/{chunk-SDMHPX3X.js → chunk-E3SPN4VZ 5.js } +198 -53
- package/dist/chunk-E3SPN4VZ.js +12917 -0
- package/dist/{chunk-SDMHPX3X.js.map → chunk-E3SPN4VZ.js.map} +1 -1
- package/dist/chunk-E66EQZE6 5.js +37 -0
- package/dist/chunk-E66EQZE6.js 2.map +1 -0
- package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
- package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
- package/dist/{chunk-STYK4OH2.js → chunk-HWIIPPNI.js} +44 -225
- package/dist/chunk-HWIIPPNI.js.map +1 -0
- package/dist/chunk-I7PSE6JW 5.js +191 -0
- package/dist/chunk-I7PSE6JW.js 2.map +1 -0
- package/dist/{chunk-Y4BUBBHD.js → chunk-IIELH4DL.js} +211 -136
- package/dist/chunk-IIELH4DL.js.map +1 -0
- package/dist/{chunk-RUYZKXOD.js → chunk-KNC55RTG.js} +17 -5
- package/dist/chunk-KNC55RTG.js 5.map +1 -0
- package/dist/chunk-KNC55RTG.js.map +1 -0
- package/dist/chunk-KQCRWDSA.js 5.map +1 -0
- package/dist/{chunk-4QYC5L4K.js → chunk-LFNCN2SP.js} +26 -30
- package/dist/chunk-LFNCN2SP.js 2.map +1 -0
- package/dist/chunk-LFNCN2SP.js.map +1 -0
- package/dist/chunk-LMC26NLJ 2.js +84 -0
- package/dist/{chunk-VVBAW5A5.js → chunk-NOAYCWCX 5.js } +118 -110
- package/dist/chunk-NOAYCWCX.js +4993 -0
- package/dist/chunk-NOAYCWCX.js.map +1 -0
- package/dist/chunk-QWWZ5CAQ.js 3.map +1 -0
- package/dist/chunk-QXHPKYJV 3.js +113 -0
- package/dist/chunk-R77UEZ4E 3.js +68 -0
- package/dist/chunk-VBXEHIUJ.js 6.map +1 -0
- package/dist/{chunk-HQVPB5MZ.js → chunk-XNXXZ43G.js} +77 -33
- package/dist/chunk-XNXXZ43G.js.map +1 -0
- package/dist/chunk-ZSAAAMVR 6.js +25 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +8 -8
- package/dist/components.js 5.map +1 -0
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- package/dist/hooks.d.ts +12 -12
- package/dist/hooks.js +9 -9
- package/dist/index.d.ts +11 -11
- package/dist/index.js +20 -27
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +2 -20
- package/dist/rbac/index.js +7 -9
- package/dist/styles/index 2.js +12 -0
- package/dist/styles/index.js 5.map +1 -0
- package/dist/theming/runtime 5.js +19 -0
- package/dist/theming/runtime.js 5.map +1 -0
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
- package/dist/utils.d.ts +8 -8
- package/dist/utils.js +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +2 -2
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +10 -10
- package/docs/api/classes/StorageUtils.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +24 -11
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- 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 +2 -2
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +2 -2
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +2 -2
- package/docs/api/interfaces/PermissionEnforcerProps.md +4 -4
- package/docs/api/interfaces/ProgressProps.md +1 -1
- 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 +2 -2
- 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 +2 -2
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +2 -2
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +2 -2
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +2 -2
- 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 +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +2 -2
- package/docs/api/interfaces/RouteConfig.md +2 -2
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- 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 +60 -38
- 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 +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +202 -217
- package/docs/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +768 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- package/docs/rbac/event-based-apps.md +124 -6
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.cjs +292 -57
- package/src/__tests__/public-recipe-view.test.ts +10 -10
- package/src/__tests__/rls-policies.test.ts +16 -14
- package/src/components/AddressField/README.md +6 -6
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +172 -45
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +121 -28
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +9 -8
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +20 -52
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +170 -34
- package/src/components/DataTable/__tests__/keyboard.test.tsx +75 -12
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +75 -11
- package/src/components/DataTable/components/UnifiedTableBody.tsx +85 -14
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +75 -10
- package/src/components/FileDisplay/FileDisplay.test.tsx +2 -1
- package/src/components/FileDisplay/FileDisplay.tsx +16 -4
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +6 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +1 -10
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -16
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +25 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +97 -68
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +0 -7
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +5 -9
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +0 -1
- package/src/components/PublicLayout/PublicPageProvider.tsx +0 -1
- package/src/components/Select/Select.test.tsx +4 -1
- package/src/components/Select/Select.tsx +60 -15
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +23 -15
- package/src/hooks/public/usePublicEvent.ts +8 -8
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/services/useAuthService.ts +21 -3
- package/src/hooks/services/useEventService.ts +21 -3
- package/src/hooks/services/useInactivityService.ts +21 -3
- package/src/hooks/services/useOrganisationService.ts +21 -3
- package/src/hooks/useFileDisplay.ts +18 -26
- package/src/hooks/useQueryCache.ts +6 -6
- package/src/hooks/useSecureDataAccess.test.ts +24 -17
- package/src/hooks/useSecureDataAccess.ts +18 -13
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- package/src/providers/services/EventServiceProvider.tsx +0 -8
- package/src/providers/services/UnifiedAuthProvider.tsx +174 -24
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +10 -16
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +82 -0
- package/src/rbac/adapters.tsx +3 -22
- package/src/rbac/api.test.ts +2 -2
- package/src/rbac/api.ts +7 -1
- package/src/rbac/components/EnhancedNavigationMenu.tsx +2 -15
- package/src/rbac/components/NavigationGuard.tsx +1 -10
- package/src/rbac/components/NavigationProvider.tsx +0 -1
- package/src/rbac/components/PermissionEnforcer.tsx +45 -12
- package/src/rbac/components/SecureDataProvider.tsx +0 -1
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +7 -43
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +4 -11
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +3 -3
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +1 -1
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +1 -1
- package/src/rbac/engine.ts +14 -2
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/usePermissions.ts +51 -11
- package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
- package/src/rbac/hooks/useRBAC.ts +3 -13
- package/src/rbac/hooks/useResolvedScope.test.ts +75 -54
- package/src/rbac/hooks/useResolvedScope.ts +58 -33
- package/src/rbac/hooks/useSecureSupabase.ts +4 -9
- package/src/rbac/secureClient.ts +31 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
- package/src/rbac/utils/eventContext.ts +5 -2
- package/src/services/AuthService.ts +37 -8
- package/src/services/EventService.ts +4 -57
- package/src/services/InactivityService.ts +127 -34
- package/src/services/OrganisationService.ts +160 -149
- 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/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/file-reference/index.ts +4 -4
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- 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/dist/chunk-4QYC5L4K.js.map +0 -1
- package/dist/chunk-73HSNNOQ.js.map +0 -1
- package/dist/chunk-HQVPB5MZ.js.map +0 -1
- package/dist/chunk-J2XXC7R5.js.map +0 -1
- package/dist/chunk-NIU6J6OX.js.map +0 -1
- package/dist/chunk-RUYZKXOD.js.map +0 -1
- package/dist/chunk-STYK4OH2.js.map +0 -1
- package/dist/chunk-VVBAW5A5.js.map +0 -1
- package/dist/chunk-Y4BUBBHD.js.map +0 -1
- package/scripts/check-pace-core-compliance.js +0 -512
- package/src/rbac/hooks/useSuperAdminBypass.ts +0 -126
- package/src/utils/context/superAdminOverride.ts +0 -58
- /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-5FU7IESH.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-RGJTDE2C.js.map} +0 -0
- /package/dist/{api-I6UCQ5S6.js.map → api-N774RPUA.js.map} +0 -0
|
@@ -144,49 +144,74 @@ export function useResolvedScope({
|
|
|
144
144
|
let appConfig: AppConfig | null = null;
|
|
145
145
|
|
|
146
146
|
// Try to resolve app config from database (with caching)
|
|
147
|
+
// Only query if user is authenticated (RLS policies require authentication)
|
|
147
148
|
if (supabase && appName) {
|
|
148
149
|
try {
|
|
149
|
-
// Check
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// Check if user is authenticated before querying (RLS requires auth)
|
|
151
|
+
// HTTP 406 errors are expected when not authenticated, so we skip the query
|
|
152
|
+
const { data: session } = await supabase.auth.getSession();
|
|
153
|
+
if (!session?.session) {
|
|
154
|
+
// User not authenticated - skip app resolution, will retry after login
|
|
155
|
+
// This is expected on login pages, so don't log as error
|
|
156
|
+
log.debug(`Skipping app resolution for "${appName}" - user not authenticated`);
|
|
155
157
|
} else {
|
|
156
|
-
//
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Check if app exists but is inactive
|
|
166
|
-
const { data: inactiveApp } = await supabase
|
|
158
|
+
// Check cache first
|
|
159
|
+
const cached = appConfigCache.get(appName);
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
if (cached && (now - cached.timestamp) < CACHE_TTL) {
|
|
162
|
+
appId = cached.appId;
|
|
163
|
+
appConfig = cached.appConfig;
|
|
164
|
+
} else {
|
|
165
|
+
// Cache miss or expired - fetch from database
|
|
166
|
+
const { data: app, error } = await supabase
|
|
167
167
|
.from('rbac_apps')
|
|
168
|
-
.select('id, name, is_active')
|
|
168
|
+
.select('id, name, requires_event, is_active')
|
|
169
169
|
.eq('name', appName)
|
|
170
|
-
.
|
|
170
|
+
.eq('is_active', true)
|
|
171
|
+
.single() as { data: { id: string; name: string; requires_event: boolean; is_active: boolean } | null; error: any };
|
|
171
172
|
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
// Don't
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
if (error) {
|
|
174
|
+
// HTTP 406 is expected when not authenticated (RLS blocks query)
|
|
175
|
+
// Don't log as error if it's a 406 - this is expected behavior
|
|
176
|
+
if (error.code === '406' || error.code === 'PGRST116' || error.message?.includes('406')) {
|
|
177
|
+
log.debug(`App resolution blocked by RLS for "${appName}" - user may not be authenticated`);
|
|
178
|
+
// Don't cache - will retry after authentication
|
|
179
|
+
appId = undefined;
|
|
180
|
+
} else {
|
|
181
|
+
// Check if app exists but is inactive
|
|
182
|
+
const { data: inactiveApp } = await supabase
|
|
183
|
+
.from('rbac_apps')
|
|
184
|
+
.select('id, name, is_active')
|
|
185
|
+
.eq('name', appName)
|
|
186
|
+
.single() as { data: { id: string; name: string; is_active: boolean } | null };
|
|
187
|
+
|
|
188
|
+
if (inactiveApp) {
|
|
189
|
+
log.error(`App "${appName}" exists but is inactive (is_active: ${inactiveApp.is_active})`);
|
|
190
|
+
// Don't cache inactive apps - set appId to undefined
|
|
191
|
+
appId = undefined;
|
|
192
|
+
} else {
|
|
193
|
+
log.error(`App "${appName}" not found in rbac_apps table`, { error });
|
|
194
|
+
// Don't cache missing apps - set appId to undefined
|
|
195
|
+
appId = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (app) {
|
|
199
|
+
appId = app.id;
|
|
200
|
+
appConfig = { requires_event: app.requires_event ?? false };
|
|
201
|
+
// Only cache successful lookups of active apps
|
|
202
|
+
appConfigCache.set(appName, { appId, appConfig, timestamp: now });
|
|
180
203
|
}
|
|
181
|
-
} else if (app) {
|
|
182
|
-
appId = app.id;
|
|
183
|
-
appConfig = { requires_event: app.requires_event ?? false };
|
|
184
|
-
// Only cache successful lookups of active apps
|
|
185
|
-
appConfigCache.set(appName, { appId, appConfig, timestamp: now });
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
} catch (error) {
|
|
189
|
-
|
|
207
|
+
// Handle network errors or other unexpected errors gracefully
|
|
208
|
+
// Don't log 406 errors as they're expected when not authenticated
|
|
209
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
210
|
+
if (!errorMessage.includes('406') && !errorMessage.includes('PGRST116')) {
|
|
211
|
+
log.error('Unexpected error resolving app config:', error);
|
|
212
|
+
} else {
|
|
213
|
+
log.debug('App resolution skipped - authentication required');
|
|
214
|
+
}
|
|
190
215
|
}
|
|
191
216
|
}
|
|
192
217
|
|
|
@@ -120,7 +120,7 @@ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
|
120
120
|
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
121
121
|
import { useEvents } from '../../hooks/useEvents';
|
|
122
122
|
import { useResolvedScope } from './useResolvedScope';
|
|
123
|
-
import {
|
|
123
|
+
import { useOrganisationSecurity } from '../../hooks/useOrganisationSecurity';
|
|
124
124
|
import { createSecureClient, SecureSupabaseClient } from '../secureClient';
|
|
125
125
|
import type { Database } from '../../types/database';
|
|
126
126
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
@@ -215,14 +215,9 @@ export function useSecureSupabase(
|
|
|
215
215
|
const eventLoading = 'eventLoading' in eventsContext ? eventsContext.eventLoading : false;
|
|
216
216
|
|
|
217
217
|
// Check super admin status for conditional filtering
|
|
218
|
-
// Use
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const { isSuperAdmin: verifiedIsSuperAdmin, isLoading: isVerifyingSuperAdmin } = useSuperAdminBypass();
|
|
222
|
-
const metadataHint = Boolean(user?.app_metadata?.is_super_admin) || Boolean(user?.user_metadata?.is_super_admin);
|
|
223
|
-
// If verified as super admin, use that. If verification in progress, use metadata hint optimistically.
|
|
224
|
-
// Once verification completes and user is not super admin, verifiedIsSuperAdmin will be false.
|
|
225
|
-
const isSuperAdmin = verifiedIsSuperAdmin || (isVerifyingSuperAdmin && metadataHint);
|
|
218
|
+
// Use verified status from useOrganisationSecurity which checks the database
|
|
219
|
+
const { superAdminContext } = useOrganisationSecurity();
|
|
220
|
+
const isSuperAdmin = superAdminContext.isSuperAdmin;
|
|
226
221
|
|
|
227
222
|
// Resolve scope to get appId
|
|
228
223
|
const { resolvedScope } = useResolvedScope({
|
package/src/rbac/secureClient.ts
CHANGED
|
@@ -152,6 +152,19 @@ export class SecureSupabaseClient {
|
|
|
152
152
|
|
|
153
153
|
// Override insert to add organisation context
|
|
154
154
|
query.insert = (values: any) => {
|
|
155
|
+
// Tables that don't have organisation_id column
|
|
156
|
+
const tablesWithoutOrganisationId = [
|
|
157
|
+
'core_organisations', // Organisation table itself - uses 'id' as primary key
|
|
158
|
+
'rbac_apps', // App configuration table - no organisation scope
|
|
159
|
+
'rbac_app_pages', // Page configuration table - scoped by app_id, not organisation_id
|
|
160
|
+
'rbac_global_roles', // Global roles - no organisation scope
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
// Skip adding organisation_id for tables that don't have it
|
|
164
|
+
if (tablesWithoutOrganisationId.includes(tableName)) {
|
|
165
|
+
return originalInsert(values);
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
// For rbac_user_profiles, only add organisation_id if not super admin
|
|
156
169
|
// Super admins can create users in any org, non-super-admins are restricted
|
|
157
170
|
if (tableName === 'rbac_user_profiles') {
|
|
@@ -204,6 +217,24 @@ export class SecureSupabaseClient {
|
|
|
204
217
|
* - Always apply org filter unless super admin bypasses it
|
|
205
218
|
*/
|
|
206
219
|
private addOrganisationFilter(query: any, tableName: string) {
|
|
220
|
+
// Tables that don't have organisation_id column - RLS policies handle access control
|
|
221
|
+
const tablesWithoutOrganisationId = [
|
|
222
|
+
'core_organisations', // Organisation table itself - uses 'id' as primary key
|
|
223
|
+
'rbac_apps', // App configuration table - no organisation scope
|
|
224
|
+
'rbac_app_pages', // Page configuration table - scoped by app_id, not organisation_id
|
|
225
|
+
'rbac_global_roles', // Global roles - no organisation scope
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// Skip organisation filter for tables that don't have organisation_id column
|
|
229
|
+
if (tablesWithoutOrganisationId.includes(tableName)) {
|
|
230
|
+
return query; // RLS policies handle access control for these tables
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If organisation context is not set, don't add a filter (e.g., super admin without selected org)
|
|
234
|
+
if (!this.organisationId) {
|
|
235
|
+
return query;
|
|
236
|
+
}
|
|
237
|
+
|
|
207
238
|
// For rbac_user_profiles, use conditional filtering based on super admin status
|
|
208
239
|
if (tableName === 'rbac_user_profiles') {
|
|
209
240
|
// Super admins: No org filter (see all users via RLS)
|
|
@@ -41,7 +41,7 @@ describe('Event Context Utilities', () => {
|
|
|
41
41
|
// Clear cache before each test to prevent test interference
|
|
42
42
|
clearAllOrgDerivationCache();
|
|
43
43
|
mockSupabase = createMockSupabaseClient();
|
|
44
|
-
mockQuery = mockSupabase.from('
|
|
44
|
+
mockQuery = mockSupabase.from('core_events');
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
afterEach(() => {
|
|
@@ -63,7 +63,7 @@ describe('Event Context Utilities', () => {
|
|
|
63
63
|
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
64
64
|
|
|
65
65
|
expect(result).toBe(organisationId);
|
|
66
|
-
expect(mockSupabase.from).toHaveBeenCalledWith('
|
|
66
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
|
|
67
67
|
expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
|
|
68
68
|
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
|
|
69
69
|
expect(mockQuery.single).toHaveBeenCalled();
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Event Context Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/EventContext/Tests
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for event context utilities in the RBAC system.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { Database } from '../../../types/database';
|
|
13
|
+
import { UUID, Scope } from '../../types';
|
|
14
|
+
import {
|
|
15
|
+
getOrganisationFromEvent,
|
|
16
|
+
createScopeFromEvent,
|
|
17
|
+
isEventBasedScope,
|
|
18
|
+
isValidEventBasedScope,
|
|
19
|
+
clearAllOrgDerivationCache,
|
|
20
|
+
clearOrgDerivationCache
|
|
21
|
+
} from '../eventContext';
|
|
22
|
+
|
|
23
|
+
// Mock Supabase client
|
|
24
|
+
const createMockSupabaseClient = () => {
|
|
25
|
+
const mockQuery = {
|
|
26
|
+
select: vi.fn().mockReturnThis(),
|
|
27
|
+
eq: vi.fn().mockReturnThis(),
|
|
28
|
+
single: vi.fn()
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fromMock = vi.fn().mockReturnValue(mockQuery);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
from: fromMock,
|
|
35
|
+
query: mockQuery
|
|
36
|
+
} as unknown as SupabaseClient<Database>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe('Event Context Utilities', () => {
|
|
40
|
+
let mockSupabase: SupabaseClient<Database>;
|
|
41
|
+
let mockQuery: any;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Clear cache before each test
|
|
45
|
+
clearAllOrgDerivationCache();
|
|
46
|
+
|
|
47
|
+
mockSupabase = createMockSupabaseClient();
|
|
48
|
+
// Reset mockQuery to get a fresh query builder for each test
|
|
49
|
+
mockQuery = {
|
|
50
|
+
select: vi.fn().mockReturnThis(),
|
|
51
|
+
eq: vi.fn().mockReturnThis(),
|
|
52
|
+
single: vi.fn()
|
|
53
|
+
};
|
|
54
|
+
(mockSupabase.from as any).mockReturnValue(mockQuery);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
// Clear cache after each test
|
|
60
|
+
clearAllOrgDerivationCache();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getOrganisationFromEvent', () => {
|
|
64
|
+
it('should return organisation ID when event exists', async () => {
|
|
65
|
+
const eventId = 'event-123';
|
|
66
|
+
const organisationId = 'org-456';
|
|
67
|
+
|
|
68
|
+
mockQuery.single.mockResolvedValue({
|
|
69
|
+
data: { organisation_id: organisationId },
|
|
70
|
+
error: null
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
74
|
+
|
|
75
|
+
expect(result).toBe(organisationId);
|
|
76
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
|
|
77
|
+
expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
|
|
78
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
|
|
79
|
+
expect(mockQuery.single).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return null when event does not exist', async () => {
|
|
83
|
+
const eventId = 'nonexistent-event';
|
|
84
|
+
|
|
85
|
+
mockQuery.single.mockResolvedValue({
|
|
86
|
+
data: null,
|
|
87
|
+
error: { message: 'Event not found' }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
91
|
+
|
|
92
|
+
expect(result).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return null when data is null', async () => {
|
|
96
|
+
const eventId = 'event-123';
|
|
97
|
+
|
|
98
|
+
// Clear cache for this specific test
|
|
99
|
+
clearOrgDerivationCache(eventId);
|
|
100
|
+
|
|
101
|
+
mockQuery.single.mockResolvedValue({
|
|
102
|
+
data: null,
|
|
103
|
+
error: null
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
107
|
+
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle database errors gracefully', async () => {
|
|
112
|
+
const eventId = 'event-123';
|
|
113
|
+
const dbError = new Error('Database connection failed');
|
|
114
|
+
|
|
115
|
+
// Clear cache for this specific test
|
|
116
|
+
clearOrgDerivationCache(eventId);
|
|
117
|
+
|
|
118
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
119
|
+
|
|
120
|
+
await expect(getOrganisationFromEvent(mockSupabase, eventId))
|
|
121
|
+
.rejects.toThrow('Database connection failed');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle empty organisation_id', async () => {
|
|
125
|
+
const eventId = 'event-123';
|
|
126
|
+
|
|
127
|
+
// Clear cache for this specific test
|
|
128
|
+
clearOrgDerivationCache(eventId);
|
|
129
|
+
|
|
130
|
+
mockQuery.single.mockResolvedValue({
|
|
131
|
+
data: { organisation_id: null },
|
|
132
|
+
error: null
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
136
|
+
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('createScopeFromEvent', () => {
|
|
142
|
+
it('should create complete scope when event exists', async () => {
|
|
143
|
+
const eventId = 'event-123';
|
|
144
|
+
const organisationId = 'org-456';
|
|
145
|
+
const appId = 'app-789';
|
|
146
|
+
|
|
147
|
+
mockQuery.single.mockResolvedValue({
|
|
148
|
+
data: { organisation_id: organisationId },
|
|
149
|
+
error: null
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await createScopeFromEvent(mockSupabase, eventId, appId);
|
|
153
|
+
|
|
154
|
+
expect(result).toEqual({
|
|
155
|
+
organisationId,
|
|
156
|
+
eventId,
|
|
157
|
+
appId
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should create scope without appId when not provided', async () => {
|
|
162
|
+
const eventId = 'event-123';
|
|
163
|
+
const organisationId = 'org-456';
|
|
164
|
+
|
|
165
|
+
mockQuery.single.mockResolvedValue({
|
|
166
|
+
data: { organisation_id: organisationId },
|
|
167
|
+
error: null
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual({
|
|
173
|
+
organisationId,
|
|
174
|
+
eventId,
|
|
175
|
+
appId: undefined
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return null when event does not exist', async () => {
|
|
180
|
+
const eventId = 'nonexistent-event';
|
|
181
|
+
|
|
182
|
+
mockQuery.single.mockResolvedValue({
|
|
183
|
+
data: null,
|
|
184
|
+
error: { message: 'Event not found' }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
188
|
+
|
|
189
|
+
expect(result).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return null when organisation lookup fails', async () => {
|
|
193
|
+
const eventId = 'event-123';
|
|
194
|
+
|
|
195
|
+
// Clear cache for this specific test
|
|
196
|
+
clearOrgDerivationCache(eventId);
|
|
197
|
+
|
|
198
|
+
mockQuery.single.mockResolvedValue({
|
|
199
|
+
data: { organisation_id: null },
|
|
200
|
+
error: null
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
204
|
+
|
|
205
|
+
expect(result).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle database errors gracefully', async () => {
|
|
209
|
+
const eventId = 'event-123';
|
|
210
|
+
const dbError = new Error('Database connection failed');
|
|
211
|
+
|
|
212
|
+
// Clear cache for this specific test
|
|
213
|
+
clearOrgDerivationCache(eventId);
|
|
214
|
+
|
|
215
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
216
|
+
|
|
217
|
+
await expect(createScopeFromEvent(mockSupabase, eventId))
|
|
218
|
+
.rejects.toThrow('Database connection failed');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('isEventBasedScope', () => {
|
|
223
|
+
it('should return true for event-based scope (no organisationId, has eventId)', () => {
|
|
224
|
+
const scope: Scope = {
|
|
225
|
+
eventId: 'event-123',
|
|
226
|
+
appId: 'app-456'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
expect(isEventBasedScope(scope)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return false when organisationId is present', () => {
|
|
233
|
+
const scope: Scope = {
|
|
234
|
+
organisationId: 'org-123',
|
|
235
|
+
eventId: 'event-123',
|
|
236
|
+
appId: 'app-456'
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return false when eventId is missing', () => {
|
|
243
|
+
const scope: Scope = {
|
|
244
|
+
appId: 'app-456'
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return false when both organisationId and eventId are missing', () => {
|
|
251
|
+
const scope: Scope = {
|
|
252
|
+
appId: 'app-456'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return false when eventId is null', () => {
|
|
259
|
+
const scope: Scope = {
|
|
260
|
+
eventId: null,
|
|
261
|
+
appId: 'app-456'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return false when eventId is undefined', () => {
|
|
268
|
+
const scope: Scope = {
|
|
269
|
+
eventId: undefined,
|
|
270
|
+
appId: 'app-456'
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('isValidEventBasedScope', () => {
|
|
278
|
+
it('should return true for valid event-based scope', () => {
|
|
279
|
+
const scope: Scope = {
|
|
280
|
+
eventId: 'event-123',
|
|
281
|
+
appId: 'app-456'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return false when eventId is missing', () => {
|
|
288
|
+
const scope: Scope = {
|
|
289
|
+
appId: 'app-456'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should return false when eventId is null', () => {
|
|
296
|
+
const scope: Scope = {
|
|
297
|
+
eventId: null,
|
|
298
|
+
appId: 'app-456'
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should return false when eventId is undefined', () => {
|
|
305
|
+
const scope: Scope = {
|
|
306
|
+
eventId: undefined,
|
|
307
|
+
appId: 'app-456'
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return false when organisationId is present (not event-based)', () => {
|
|
314
|
+
const scope: Scope = {
|
|
315
|
+
organisationId: 'org-123',
|
|
316
|
+
eventId: 'event-123',
|
|
317
|
+
appId: 'app-456'
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should return true for event-based scope without appId', () => {
|
|
324
|
+
const scope: Scope = {
|
|
325
|
+
eventId: 'event-123'
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('Edge Cases and Error Handling', () => {
|
|
333
|
+
it('should handle malformed event IDs', async () => {
|
|
334
|
+
const malformedEventId = '';
|
|
335
|
+
|
|
336
|
+
mockQuery.single.mockResolvedValue({
|
|
337
|
+
data: null,
|
|
338
|
+
error: { message: 'Invalid event ID' }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const result = await getOrganisationFromEvent(mockSupabase, malformedEventId);
|
|
342
|
+
|
|
343
|
+
expect(result).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should handle very long event IDs', async () => {
|
|
347
|
+
const longEventId = 'a'.repeat(1000);
|
|
348
|
+
const organisationId = 'org-456';
|
|
349
|
+
|
|
350
|
+
mockQuery.single.mockResolvedValue({
|
|
351
|
+
data: { organisation_id: organisationId },
|
|
352
|
+
error: null
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = await getOrganisationFromEvent(mockSupabase, longEventId);
|
|
356
|
+
|
|
357
|
+
expect(result).toBe(organisationId);
|
|
358
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', longEventId);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should handle special characters in event IDs', async () => {
|
|
362
|
+
const specialEventId = 'event-123!@#$%^&*()';
|
|
363
|
+
const organisationId = 'org-456';
|
|
364
|
+
|
|
365
|
+
mockQuery.single.mockResolvedValue({
|
|
366
|
+
data: { organisation_id: organisationId },
|
|
367
|
+
error: null
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = await getOrganisationFromEvent(mockSupabase, specialEventId);
|
|
371
|
+
|
|
372
|
+
expect(result).toBe(organisationId);
|
|
373
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', specialEventId);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle concurrent calls to getOrganisationFromEvent', async () => {
|
|
377
|
+
const eventId1 = 'event-123';
|
|
378
|
+
const eventId2 = 'event-456';
|
|
379
|
+
const organisationId1 = 'org-123';
|
|
380
|
+
const organisationId2 = 'org-456';
|
|
381
|
+
|
|
382
|
+
// Clear cache for these specific events
|
|
383
|
+
clearOrgDerivationCache(eventId1);
|
|
384
|
+
clearOrgDerivationCache(eventId2);
|
|
385
|
+
|
|
386
|
+
// Create separate query builders for concurrent calls
|
|
387
|
+
const mockQuery1 = {
|
|
388
|
+
select: vi.fn().mockReturnThis(),
|
|
389
|
+
eq: vi.fn().mockReturnThis(),
|
|
390
|
+
single: vi.fn().mockResolvedValue({
|
|
391
|
+
data: { organisation_id: organisationId1 },
|
|
392
|
+
error: null
|
|
393
|
+
})
|
|
394
|
+
};
|
|
395
|
+
const mockQuery2 = {
|
|
396
|
+
select: vi.fn().mockReturnThis(),
|
|
397
|
+
eq: vi.fn().mockReturnThis(),
|
|
398
|
+
single: vi.fn().mockResolvedValue({
|
|
399
|
+
data: { organisation_id: organisationId2 },
|
|
400
|
+
error: null
|
|
401
|
+
})
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
(mockSupabase.from as any)
|
|
405
|
+
.mockReturnValueOnce(mockQuery1)
|
|
406
|
+
.mockReturnValueOnce(mockQuery2);
|
|
407
|
+
|
|
408
|
+
const [result1, result2] = await Promise.all([
|
|
409
|
+
getOrganisationFromEvent(mockSupabase, eventId1),
|
|
410
|
+
getOrganisationFromEvent(mockSupabase, eventId2)
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
expect(result1).toBe(organisationId1);
|
|
414
|
+
expect(result2).toBe(organisationId2);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should handle concurrent calls to createScopeFromEvent', async () => {
|
|
418
|
+
const eventId1 = 'event-123';
|
|
419
|
+
const eventId2 = 'event-456';
|
|
420
|
+
const organisationId1 = 'org-123';
|
|
421
|
+
const organisationId2 = 'org-456';
|
|
422
|
+
const appId1 = 'app-123';
|
|
423
|
+
const appId2 = 'app-456';
|
|
424
|
+
|
|
425
|
+
// Clear cache for these specific events
|
|
426
|
+
clearOrgDerivationCache(eventId1);
|
|
427
|
+
clearOrgDerivationCache(eventId2);
|
|
428
|
+
|
|
429
|
+
// Create separate query builders for concurrent calls
|
|
430
|
+
const mockQuery1 = {
|
|
431
|
+
select: vi.fn().mockReturnThis(),
|
|
432
|
+
eq: vi.fn().mockReturnThis(),
|
|
433
|
+
single: vi.fn().mockResolvedValue({
|
|
434
|
+
data: { organisation_id: organisationId1 },
|
|
435
|
+
error: null
|
|
436
|
+
})
|
|
437
|
+
};
|
|
438
|
+
const mockQuery2 = {
|
|
439
|
+
select: vi.fn().mockReturnThis(),
|
|
440
|
+
eq: vi.fn().mockReturnThis(),
|
|
441
|
+
single: vi.fn().mockResolvedValue({
|
|
442
|
+
data: { organisation_id: organisationId2 },
|
|
443
|
+
error: null
|
|
444
|
+
})
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
(mockSupabase.from as any)
|
|
448
|
+
.mockReturnValueOnce(mockQuery1)
|
|
449
|
+
.mockReturnValueOnce(mockQuery2);
|
|
450
|
+
|
|
451
|
+
const [result1, result2] = await Promise.all([
|
|
452
|
+
createScopeFromEvent(mockSupabase, eventId1, appId1),
|
|
453
|
+
createScopeFromEvent(mockSupabase, eventId2, appId2)
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
expect(result1).toEqual({
|
|
457
|
+
organisationId: organisationId1,
|
|
458
|
+
eventId: eventId1,
|
|
459
|
+
appId: appId1
|
|
460
|
+
});
|
|
461
|
+
expect(result2).toEqual({
|
|
462
|
+
organisationId: organisationId2,
|
|
463
|
+
eventId: eventId2,
|
|
464
|
+
appId: appId2
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('Type Safety', () => {
|
|
470
|
+
it('should handle UUID types correctly', () => {
|
|
471
|
+
const validUUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
472
|
+
const scope: Scope = {
|
|
473
|
+
eventId: validUUID,
|
|
474
|
+
appId: validUUID
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should handle string types correctly', () => {
|
|
481
|
+
const stringId = 'event-123';
|
|
482
|
+
const scope: Scope = {
|
|
483
|
+
eventId: stringId,
|
|
484
|
+
appId: 'app-456'
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|