@jmruthers/pace-core 0.5.190 → 0.5.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  2. package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
  3. package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
  4. package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
  5. package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
  6. package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
  7. package/dist/chunk-6LTQQAT6.js.map +1 -0
  8. package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
  9. package/dist/chunk-6TQDD426.js.map +1 -0
  10. package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
  11. package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
  12. package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
  13. package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
  14. package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
  15. package/dist/chunk-ROXMHMY2.js.map +1 -0
  16. package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
  17. package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
  18. package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
  19. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  20. package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
  21. package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
  22. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  23. package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
  24. package/dist/chunk-XYXSXPUK.js.map +1 -0
  25. package/dist/components.d.ts +3 -3
  26. package/dist/components.js +8 -8
  27. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  28. package/dist/hooks.d.ts +12 -12
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +7 -7
  31. package/dist/index.js +18 -23
  32. package/dist/index.js.map +1 -1
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +1 -1
  35. package/dist/rbac/index.js +6 -6
  36. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
  39. package/dist/utils.d.ts +8 -8
  40. package/dist/utils.js +2 -2
  41. package/docs/api/classes/ColumnFactory.md +1 -1
  42. package/docs/api/classes/ErrorBoundary.md +1 -1
  43. package/docs/api/classes/InvalidScopeError.md +1 -1
  44. package/docs/api/classes/Logger.md +1 -1
  45. package/docs/api/classes/MissingUserContextError.md +1 -1
  46. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  47. package/docs/api/classes/PermissionDeniedError.md +1 -1
  48. package/docs/api/classes/RBACAuditManager.md +2 -2
  49. package/docs/api/classes/RBACCache.md +1 -1
  50. package/docs/api/classes/RBACEngine.md +2 -2
  51. package/docs/api/classes/RBACError.md +1 -1
  52. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  53. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  54. package/docs/api/classes/StorageUtils.md +1 -1
  55. package/docs/api/enums/FileCategory.md +1 -1
  56. package/docs/api/enums/LogLevel.md +1 -1
  57. package/docs/api/enums/RBACErrorCode.md +1 -1
  58. package/docs/api/enums/RPCFunction.md +1 -1
  59. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  60. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  63. package/docs/api/interfaces/AvatarProps.md +1 -1
  64. package/docs/api/interfaces/BadgeProps.md +1 -1
  65. package/docs/api/interfaces/ButtonProps.md +1 -1
  66. package/docs/api/interfaces/CalendarProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/ComplianceResult.md +1 -1
  71. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  72. package/docs/api/interfaces/DataRecord.md +1 -1
  73. package/docs/api/interfaces/DataTableAction.md +1 -1
  74. package/docs/api/interfaces/DataTableColumn.md +1 -1
  75. package/docs/api/interfaces/DataTableProps.md +1 -1
  76. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  77. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  78. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  79. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  80. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  82. package/docs/api/interfaces/ExportColumn.md +1 -1
  83. package/docs/api/interfaces/ExportOptions.md +1 -1
  84. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  85. package/docs/api/interfaces/FileMetadata.md +1 -1
  86. package/docs/api/interfaces/FileReference.md +1 -1
  87. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  88. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  89. package/docs/api/interfaces/FileUploadProps.md +1 -1
  90. package/docs/api/interfaces/FooterProps.md +1 -1
  91. package/docs/api/interfaces/FormFieldProps.md +1 -1
  92. package/docs/api/interfaces/FormProps.md +1 -1
  93. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  94. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  95. package/docs/api/interfaces/InputProps.md +1 -1
  96. package/docs/api/interfaces/LabelProps.md +1 -1
  97. package/docs/api/interfaces/LoggerConfig.md +1 -1
  98. package/docs/api/interfaces/LoginFormProps.md +1 -1
  99. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  100. package/docs/api/interfaces/NavigationContextType.md +1 -1
  101. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  102. package/docs/api/interfaces/NavigationItem.md +1 -1
  103. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  104. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  105. package/docs/api/interfaces/Organisation.md +1 -1
  106. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  107. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  108. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  109. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  110. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  111. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  112. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  113. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  114. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  115. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  116. package/docs/api/interfaces/PaletteData.md +1 -1
  117. package/docs/api/interfaces/ParsedAddress.md +2 -2
  118. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  119. package/docs/api/interfaces/ProgressProps.md +1 -1
  120. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  121. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  122. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  123. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  124. package/docs/api/interfaces/QuickFix.md +1 -1
  125. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  126. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  127. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  128. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  129. package/docs/api/interfaces/RBACConfig.md +2 -2
  130. package/docs/api/interfaces/RBACContext.md +1 -1
  131. package/docs/api/interfaces/RBACLogger.md +1 -1
  132. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  133. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  134. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  135. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  136. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  137. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  138. package/docs/api/interfaces/RBACResult.md +1 -1
  139. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  140. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  141. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  142. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  143. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  144. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  145. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  146. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  147. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  148. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  149. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  150. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  151. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  152. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  153. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  154. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  155. package/docs/api/interfaces/RouteConfig.md +1 -1
  156. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  157. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  158. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  159. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  160. package/docs/api/interfaces/SetupIssue.md +1 -1
  161. package/docs/api/interfaces/StorageConfig.md +1 -1
  162. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  163. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  164. package/docs/api/interfaces/StorageListOptions.md +1 -1
  165. package/docs/api/interfaces/StorageListResult.md +1 -1
  166. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  167. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  168. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  169. package/docs/api/interfaces/StyleImport.md +1 -1
  170. package/docs/api/interfaces/SwitchProps.md +1 -1
  171. package/docs/api/interfaces/TabsContentProps.md +1 -1
  172. package/docs/api/interfaces/TabsListProps.md +1 -1
  173. package/docs/api/interfaces/TabsProps.md +1 -1
  174. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  175. package/docs/api/interfaces/TextareaProps.md +1 -1
  176. package/docs/api/interfaces/ToastActionElement.md +1 -1
  177. package/docs/api/interfaces/ToastProps.md +1 -1
  178. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  179. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  180. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  181. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  182. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  183. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  184. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  185. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  186. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  187. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  188. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  189. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  190. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  191. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  192. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  193. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  194. package/docs/api/interfaces/UserEventAccess.md +1 -1
  195. package/docs/api/interfaces/UserMenuProps.md +1 -1
  196. package/docs/api/interfaces/UserProfile.md +1 -1
  197. package/docs/api/modules.md +16 -16
  198. package/docs/migration/README.md +18 -0
  199. package/docs/migration/database-changes-december-2025.md +767 -0
  200. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  201. package/package.json +1 -1
  202. package/src/__tests__/public-recipe-view.test.ts +10 -10
  203. package/src/__tests__/rls-policies.test.ts +13 -13
  204. package/src/components/AddressField/README.md +6 -6
  205. package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
  206. package/src/components/Select/Select.test.tsx +4 -1
  207. package/src/components/Select/Select.tsx +60 -15
  208. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
  209. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
  210. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
  211. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
  212. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
  213. package/src/hooks/public/usePublicEvent.ts +8 -8
  214. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  215. package/src/hooks/useFileDisplay.ts +8 -9
  216. package/src/hooks/useQueryCache.ts +6 -6
  217. package/src/hooks/useSecureDataAccess.test.ts +8 -8
  218. package/src/hooks/useSecureDataAccess.ts +15 -11
  219. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  220. package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
  221. package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
  222. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
  223. package/src/rbac/utils/eventContext.ts +5 -2
  224. package/src/services/AuthService.ts +37 -8
  225. package/src/services/OrganisationService.ts +92 -139
  226. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  227. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  228. package/src/types/database.generated.ts +166 -201
  229. package/src/types/supabase.ts +2 -2
  230. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  231. package/src/utils/file-reference/index.ts +4 -4
  232. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  233. package/src/utils/google-places/types.ts +1 -1
  234. package/src/utils/request-deduplication.ts +4 -4
  235. package/src/utils/security/secureDataAccess.test.ts +1 -1
  236. package/src/utils/security/secureDataAccess.ts +7 -4
  237. package/src/utils/storage/README.md +1 -1
  238. package/dist/chunk-4QYC5L4K.js.map +0 -1
  239. package/dist/chunk-J2XXC7R5.js.map +0 -1
  240. package/dist/chunk-NIU6J6OX.js.map +0 -1
  241. package/dist/chunk-RUYZKXOD.js.map +0 -1
  242. package/dist/chunk-STYK4OH2.js.map +0 -1
  243. package/dist/chunk-VVBAW5A5.js.map +0 -1
  244. /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
  245. /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  246. /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
  247. /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
  248. /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
  249. /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
@@ -27,6 +27,15 @@ interface OrganisationRoleRpcResponse {
27
27
  organisation_id: string;
28
28
  role: 'org_admin' | 'leader' | 'member' | 'supporter';
29
29
  status: 'active' | 'inactive' | 'suspended';
30
+ // Organisation fields from RPC
31
+ name?: string;
32
+ display_name?: string;
33
+ subscription_tier?: string;
34
+ settings?: unknown;
35
+ is_active?: boolean;
36
+ parent_id?: string;
37
+ organisation_created_at?: string;
38
+ organisation_updated_at?: string;
30
39
  [key: string]: unknown;
31
40
  }
32
41
 
@@ -358,159 +367,106 @@ export class OrganisationService extends BaseService implements IOrganisationSer
358
367
  this.notify();
359
368
 
360
369
  try {
361
- // Get user's organisation memberships using secure RPC function
362
- // Include all roles (org_admin, leader, member, supporter) - supporters can access PORTAL
363
- let memberships, membershipError;
370
+ // Get user's organisation roles directly from rbac_organisation_roles table
371
+ // This queries the source table directly instead of using the RPC which filters to match core_organisation_memberships view
372
+ // We still filter to active, non-revoked roles for the org selector
373
+ // The join includes organisation data, so we don't need a separate query that might be filtered by RLS
374
+ let memberships, membershipError, organisations: Organisation[] = [];
364
375
  try {
365
- // Add timeout and abort signal to prevent hanging RPC calls
366
- const timeoutPromise = new Promise((_, reject) => {
367
- const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
368
- abortSignal.addEventListener('abort', () => {
369
- clearTimeout(timeoutId);
370
- reject(new Error('Request aborted'));
371
- });
372
- });
373
-
374
- const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
375
- p_user_id: this.user.id,
376
- p_organisation_id: null
377
- });
378
-
379
- // Check if request was aborted before making the call
376
+ // Check if request was aborted before making query
380
377
  if (abortSignal.aborted) {
381
378
  throw new Error('Request aborted');
382
379
  }
380
+
381
+ const { data: rolesData, error: rolesError } = await this.supabaseClient
382
+ .from('rbac_organisation_roles')
383
+ .select(`
384
+ id,
385
+ user_id,
386
+ organisation_id,
387
+ role,
388
+ status,
389
+ granted_at,
390
+ granted_by,
391
+ revoked_at,
392
+ revoked_by,
393
+ notes,
394
+ created_at,
395
+ updated_at,
396
+ core_organisations!inner(
397
+ id,
398
+ name,
399
+ display_name,
400
+ subscription_tier,
401
+ settings,
402
+ is_active,
403
+ parent_id,
404
+ created_at,
405
+ updated_at
406
+ )
407
+ `)
408
+ .eq('user_id', this.user.id)
409
+ .eq('status', 'active')
410
+ .is('revoked_at', null);
383
411
 
384
- const result = await Promise.race([rpcPromise, timeoutPromise]) as { data: OrganisationRoleRpcResponse[] | null; error: Error | null };
412
+ if (rolesError) {
413
+ logger.error("OrganisationService", "Error loading organisation roles:", rolesError);
414
+ throw rolesError;
415
+ }
385
416
 
386
- // Filter to organisation members (org_admin, leader, member, supporter)
387
- // Supporters are included to allow PORTAL access
388
- // Map to branded types when filtering
389
- memberships = result.data?.filter((role) =>
390
- ['org_admin', 'leader', 'member', 'supporter'].includes(role.role)
391
- ).map((m) => ({
417
+ // Map to branded types and extract organisation data from the join
418
+ // The join already includes organisation data, so we don't need a separate query
419
+ memberships = rolesData?.map((m) => ({
392
420
  ...m,
393
421
  user_id: assertUserId(m.user_id),
394
422
  organisation_id: assertOrganisationId(m.organisation_id),
395
423
  })) || [];
396
- membershipError = result.error;
397
- } catch (queryError) {
398
- membershipError = queryError instanceof Error ? queryError : new Error(String(queryError));
399
- }
400
-
401
- if (membershipError) {
402
- logger.error("OrganisationService", "Error loading memberships:", membershipError);
403
424
 
404
- // If RPC fails with timeout, try direct database query as fallback
405
- if (membershipError.message?.includes('timeout')) {
406
- try {
407
- // Check if request was aborted before making fallback query
408
- if (abortSignal.aborted) {
409
- throw new Error('Request aborted');
410
- }
411
-
412
- const { data: fallbackData, error: fallbackError } = await this.supabaseClient
413
- .from('rbac_organisation_roles')
414
- .select(`
415
- id,
416
- user_id,
417
- organisation_id,
418
- role,
419
- status,
420
- granted_at,
421
- granted_by,
422
- revoked_at,
423
- revoked_by,
424
- notes,
425
- created_at,
426
- updated_at,
427
- organisations!inner(
428
- id,
429
- name,
430
- display_name,
431
- subscription_tier,
432
- settings,
433
- is_active,
434
- parent_id,
435
- created_at,
436
- updated_at
437
- )
438
- `)
439
- .eq('user_id', this.user.id)
440
- .eq('status', 'active')
441
- .is('revoked_at', null)
442
- .in('role', ['org_admin', 'leader', 'member', 'supporter']);
443
-
444
- if (fallbackError) {
445
- logger.error("OrganisationService", "Fallback query also failed:", fallbackError);
446
- throw membershipError; // Throw original error
447
- }
448
-
449
- // Map to branded types
450
- memberships = fallbackData?.map((m) => ({
451
- ...m,
452
- user_id: assertUserId(m.user_id),
453
- organisation_id: assertOrganisationId(m.organisation_id),
454
- })) || [];
455
- membershipError = null;
456
- } catch (fallbackErr) {
457
- logger.error("OrganisationService", "Fallback query failed:", fallbackErr);
458
- throw membershipError; // Throw original error
425
+ // Extract unique organisations from the join results
426
+ // Use a Map to deduplicate by organisation ID
427
+ // Supabase returns joined data nested under the relation name
428
+ const organisationsMap = new Map<string, Organisation>();
429
+ rolesData?.forEach((role: any) => {
430
+ // The join returns organisation data nested under 'core_organisations' key
431
+ const orgData = role.core_organisations;
432
+ if (orgData && role.organisation_id && !organisationsMap.has(role.organisation_id)) {
433
+ organisationsMap.set(role.organisation_id, {
434
+ id: orgData.id,
435
+ name: orgData.name,
436
+ display_name: orgData.display_name,
437
+ subscription_tier: orgData.subscription_tier,
438
+ settings: orgData.settings,
439
+ is_active: orgData.is_active,
440
+ parent_id: orgData.parent_id,
441
+ created_at: orgData.created_at,
442
+ updated_at: orgData.updated_at,
443
+ } as Organisation);
459
444
  }
445
+ });
446
+
447
+ organisations = Array.from(organisationsMap.values());
448
+
449
+ // Extract organisations from join results
450
+ } catch (queryError) {
451
+ // Extract error message properly from Supabase error objects
452
+ if (queryError instanceof Error) {
453
+ membershipError = queryError;
454
+ } else if (queryError && typeof queryError === 'object' && 'message' in queryError) {
455
+ membershipError = new Error(String((queryError as any).message));
460
456
  } else {
461
- throw membershipError;
457
+ membershipError = new Error(String(queryError));
462
458
  }
459
+ logger.error("OrganisationService", "Error loading organisation roles:", membershipError);
460
+ throw membershipError;
463
461
  }
464
462
 
465
463
  if (!memberships || memberships.length === 0) {
466
464
  throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
467
465
  }
468
466
 
469
- // Get organisation details for the memberships
470
- const organisationIds = memberships
471
- .map((m) => m.organisation_id)
472
- .filter((id: string) => {
473
- // Better validation to prevent empty string UUID errors
474
- if (!id || typeof id !== 'string') {
475
- logger.warn("OrganisationService", "Invalid organisation ID (not string):", id);
476
- return false;
477
- }
478
- const trimmedId = id.trim();
479
- if (trimmedId === '') {
480
- logger.warn("OrganisationService", "Empty organisation ID found");
481
- return false;
482
- }
483
- // Validate UUID format
484
- const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
485
- if (!isValidUuid) {
486
- logger.warn("OrganisationService", "Invalid UUID format:", trimmedId);
487
- }
488
- return isValidUuid;
489
- });
490
-
491
- if (organisationIds.length === 0) {
492
- logger.warn("OrganisationService", "No valid organisation IDs found in memberships:", memberships);
493
- throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
494
- }
495
-
496
- // Check if request was aborted before making organisations query
497
- if (abortSignal.aborted) {
498
- throw new Error('Request aborted');
499
- }
500
-
501
- const { data: allOrganisations, error: orgError } = await this.supabaseClient
502
- .from('organisations')
503
- .select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
504
-
505
- if (orgError) {
506
- logger.error("OrganisationService", "Error loading organisations:", orgError);
507
- throw orgError;
467
+ if (!organisations || organisations.length === 0) {
468
+ throw new Error('No organisations found in role data') as OrganisationSecurityError;
508
469
  }
509
-
510
- // Filter manually on the client side
511
- const organisations = allOrganisations?.filter(org =>
512
- organisationIds.includes(org.id)
513
- ) || [];
514
470
 
515
471
  // Create a map of organisation_id to role from the memberships data
516
472
  const roleMap = new Map<string, string>();
@@ -522,6 +478,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
522
478
  const orgs = organisations as Organisation[];
523
479
  const activeOrgs = orgs.filter(org => org.is_active);
524
480
 
481
+ // Filter to active organisations only
482
+
525
483
  if (activeOrgs.length === 0) {
526
484
  throw new Error('User has no access to active organisations') as OrganisationSecurityError;
527
485
  }
@@ -549,16 +507,13 @@ export class OrganisationService extends BaseService implements IOrganisationSer
549
507
  initialOrg = validPersistedOrg;
550
508
  selectionMethod = 'persisted';
551
509
  } else {
552
- logger.warn("OrganisationService", "Persisted organisation not found in active orgs, clearing cache");
553
510
  localStorage.removeItem('pace-core-selected-organisation');
554
511
  }
555
512
  } else {
556
- logger.warn("OrganisationService", "Invalid persisted organisation ID, clearing cache");
557
513
  localStorage.removeItem('pace-core-selected-organisation');
558
514
  }
559
515
  }
560
516
  } catch (storageError) {
561
- logger.warn("OrganisationService", "Failed to restore persisted organisation:", storageError);
562
517
  // Clear potentially corrupted cache
563
518
  localStorage.removeItem('pace-core-selected-organisation');
564
519
  }
@@ -610,10 +565,8 @@ export class OrganisationService extends BaseService implements IOrganisationSer
610
565
  } catch (err) {
611
566
  const error = err as Error;
612
567
  // "User has no access to active organisations" is a valid state for users without orgs (e.g., profile pages)
613
- // Log it as a warning instead of an error to reduce noise
614
- if (error.message === 'User has no access to active organisations') {
615
- logger.warn("OrganisationService", "User has no active organisations (this is expected for users without organisation access):", error);
616
- } else {
568
+ // Only log actual errors, not expected states
569
+ if (error.message !== 'User has no access to active organisations') {
617
570
  logger.error("OrganisationService", "Failed to load organisations:", err);
618
571
  }
619
572
  this._error = error;
@@ -88,16 +88,29 @@ describe('OrganisationService Pagination & Validation', () => {
88
88
  });
89
89
 
90
90
  mockSupabase.from.mockImplementation((table: string) => {
91
- if (table === 'organisations') {
91
+ if (table === 'rbac_organisation_roles') {
92
92
  return {
93
93
  select: vi.fn().mockResolvedValue({
94
- data: [mockOrganisation, mockOrganisation2],
94
+ data: [
95
+ {
96
+ ...mockMembership,
97
+ core_organisations: mockOrganisation
98
+ },
99
+ {
100
+ ...mockMembership2,
101
+ core_organisations: mockOrganisation2
102
+ }
103
+ ],
95
104
  error: null
96
- })
105
+ }),
106
+ eq: vi.fn().mockReturnThis(),
107
+ is: vi.fn().mockReturnThis()
97
108
  };
98
109
  }
99
110
  return {
100
- select: vi.fn().mockResolvedValue({ data: [], error: null })
111
+ select: vi.fn().mockResolvedValue({ data: [], error: null }),
112
+ eq: vi.fn().mockReturnThis(),
113
+ is: vi.fn().mockReturnThis()
101
114
  };
102
115
  });
103
116
 
@@ -175,16 +188,29 @@ describe('OrganisationService Pagination & Validation', () => {
175
188
  });
176
189
 
177
190
  mockSupabase.from.mockImplementation((table: string) => {
178
- if (table === 'organisations') {
191
+ if (table === 'rbac_organisation_roles') {
179
192
  return {
180
193
  select: vi.fn().mockResolvedValue({
181
- data: [mockOrganisation, inactiveOrganisation],
194
+ data: [
195
+ {
196
+ ...mockMembership,
197
+ core_organisations: mockOrganisation
198
+ },
199
+ {
200
+ ...inactiveMembership,
201
+ core_organisations: inactiveOrganisation
202
+ }
203
+ ],
182
204
  error: null
183
- })
205
+ }),
206
+ eq: vi.fn().mockReturnThis(),
207
+ is: vi.fn().mockReturnThis()
184
208
  };
185
209
  }
186
210
  return {
187
- select: vi.fn().mockResolvedValue({ data: [], error: null })
211
+ select: vi.fn().mockResolvedValue({ data: [], error: null }),
212
+ eq: vi.fn().mockReturnThis(),
213
+ is: vi.fn().mockReturnThis()
188
214
  };
189
215
  });
190
216