@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
@@ -0,0 +1,472 @@
1
+ # Person-Scoped Profiles Migration Guide
2
+
3
+ **Version:** 0.5.190+
4
+ **Migration Date:** 2025-12-05
5
+ **Breaking Changes:** Yes
6
+
7
+ ## Overview
8
+
9
+ Profiles (membership, medical, and contact) have been migrated from **organisation-scoped** to **person-scoped**. Each person now has:
10
+
11
+ - **One membership profile** (`pace_member`) - regardless of organisation
12
+ - **One medical profile** (`medi_profile`) - regardless of organisation
13
+ - **Multiple contact profiles** (`pace_contact`) - person-scoped, multiple per type allowed
14
+
15
+ This change means profiles are no longer tied to specific organisations and are shared across all organisations a person belongs to.
16
+
17
+ ## What Changed
18
+
19
+ ### Tables Modified
20
+
21
+ #### 1. `pace_member`
22
+ - **Removed:** `organisation_id` column
23
+ - **Added:** Unique constraint on `person_id` (one membership per person)
24
+ - **Impact:** A person can now only have ONE membership record, regardless of how many organisations they belong to
25
+
26
+ #### 2. `medi_profile`
27
+ - **Removed:** `organisation_id` column
28
+ - **Impact:** One medical profile per person (via member relationship)
29
+
30
+ #### 3. `pace_contact`
31
+ - **Removed:** `organisation_id` and `member_id` columns
32
+ - **Added:** `person_id` column (NOT NULL, FK to `pace_person`)
33
+ - **Impact:** Contacts are now directly linked to persons, not members
34
+
35
+ #### 4. Related Tables (organisation_id removed)
36
+ - `medi_condition`
37
+ - `medi_diet`
38
+ - `medi_action_plan`
39
+ - `medi_profile_versions`
40
+ - `pace_consent`
41
+ - `pace_identification`
42
+ - `pace_qualification`
43
+
44
+ ### RLS Policy Changes
45
+
46
+ All RLS policies for these tables have been updated to:
47
+ - Remove organisation-based access checks
48
+ - Use person-scoped access control instead
49
+ - Allow access based on person ownership, not organisation membership
50
+
51
+ ## Breaking Changes
52
+
53
+ ### 1. Queries Filtering by `organisation_id`
54
+
55
+ **Before:**
56
+ ```typescript
57
+ // ❌ This will fail - organisation_id column no longer exists
58
+ const { data } = await supabase
59
+ .from('pace_member')
60
+ .select('*')
61
+ .eq('organisation_id', orgId);
62
+ ```
63
+
64
+ **After:**
65
+ ```typescript
66
+ // ✅ Filter by person_id instead
67
+ const { data } = await supabase
68
+ .from('pace_member')
69
+ .select('*')
70
+ .eq('person_id', personId);
71
+ ```
72
+
73
+ ### 2. Joins Using `organisation_id`
74
+
75
+ **Before:**
76
+ ```typescript
77
+ // ❌ This will fail
78
+ const { data } = await supabase
79
+ .from('pace_member')
80
+ .select('*, organisations(*)')
81
+ .eq('organisation_id', orgId);
82
+ ```
83
+
84
+ **After:**
85
+ ```typescript
86
+ // ✅ Join via person → user → organisation memberships
87
+ const { data } = await supabase
88
+ .from('pace_member')
89
+ .select('*, pace_person(*, user_profiles(*))')
90
+ .eq('person_id', personId);
91
+ ```
92
+
93
+ ### 3. Contact Queries Using `member_id`
94
+
95
+ **Before:**
96
+ ```typescript
97
+ // ❌ This will fail - member_id column no longer exists
98
+ const { data } = await supabase
99
+ .from('pace_contact')
100
+ .select('*')
101
+ .eq('member_id', memberId);
102
+ ```
103
+
104
+ **After:**
105
+ ```typescript
106
+ // ✅ Use person_id instead
107
+ const { data } = await supabase
108
+ .from('pace_contact')
109
+ .select('*')
110
+ .eq('person_id', personId);
111
+ ```
112
+
113
+ ### 4. Insert/Update Operations
114
+
115
+ **Before:**
116
+ ```typescript
117
+ // ❌ This will fail - organisation_id required
118
+ await supabase
119
+ .from('pace_member')
120
+ .insert({
121
+ person_id: personId,
122
+ organisation_id: orgId, // ❌ Column doesn't exist
123
+ membership_number: '12345'
124
+ });
125
+ ```
126
+
127
+ **After:**
128
+ ```typescript
129
+ // ✅ No organisation_id needed
130
+ await supabase
131
+ .from('pace_member')
132
+ .insert({
133
+ person_id: personId,
134
+ membership_number: '12345'
135
+ });
136
+ ```
137
+
138
+ ## Impact Assessment Checklist
139
+
140
+ Use this checklist to identify areas in your app that need updates:
141
+
142
+ ### Database Queries
143
+
144
+ - [ ] Search codebase for `pace_member` queries filtering by `organisation_id`
145
+ - [ ] Search codebase for `medi_profile` queries filtering by `organisation_id`
146
+ - [ ] Search codebase for `pace_contact` queries using `member_id`
147
+ - [ ] Search codebase for `pace_consent` queries filtering by `organisation_id`
148
+ - [ ] Search codebase for `pace_identification` queries filtering by `organisation_id`
149
+ - [ ] Search codebase for `pace_qualification` queries filtering by `organisation_id`
150
+ - [ ] Search codebase for `medi_condition`, `medi_diet`, `medi_action_plan` queries filtering by `organisation_id`
151
+
152
+ ### TypeScript Types
153
+
154
+ - [ ] Update TypeScript types (regenerate from database schema)
155
+ - [ ] Remove `organisation_id` from type definitions for profile tables
156
+ - [ ] Update `pace_contact` types to use `person_id` instead of `member_id`
157
+
158
+ ### UI Components
159
+
160
+ - [ ] Review profile display components - remove organisation context assumptions
161
+ - [ ] Update forms that create/edit profiles - remove organisation_id fields
162
+ - [ ] Update contact management - use person_id instead of member_id
163
+ - [ ] Review profile selection/filtering UI - remove organisation filters
164
+
165
+ ### Business Logic
166
+
167
+ - [ ] Review code that assumes multiple memberships per person
168
+ - [ ] Update logic that creates profiles per organisation
169
+ - [ ] Review permission checks that rely on organisation_id for profiles
170
+ - [ ] Update any code that merges/duplicates profiles across organisations
171
+
172
+ ### Data Access Patterns
173
+
174
+ - [ ] Review use of `useSecureDataAccess` hook with profile tables
175
+ - [ ] Update any custom data access utilities
176
+ - [ ] Review RPC functions that query profile tables
177
+
178
+ ## Code Migration Examples
179
+
180
+ ### Example 1: Fetching Member Profile
181
+
182
+ **Before:**
183
+ ```typescript
184
+ async function getMemberProfile(personId: string, orgId: string) {
185
+ const { data } = await supabase
186
+ .from('pace_member')
187
+ .select('*')
188
+ .eq('person_id', personId)
189
+ .eq('organisation_id', orgId) // ❌
190
+ .single();
191
+ return data;
192
+ }
193
+ ```
194
+
195
+ **After:**
196
+ ```typescript
197
+ async function getMemberProfile(personId: string) {
198
+ // ✅ No organisation_id needed - one profile per person
199
+ const { data } = await supabase
200
+ .from('pace_member')
201
+ .select('*')
202
+ .eq('person_id', personId)
203
+ .single();
204
+ return data;
205
+ }
206
+ ```
207
+
208
+ ### Example 2: Fetching Contacts
209
+
210
+ **Before:**
211
+ ```typescript
212
+ async function getContacts(memberId: string) {
213
+ const { data } = await supabase
214
+ .from('pace_contact')
215
+ .select('*')
216
+ .eq('member_id', memberId) // ❌
217
+ .eq('organisation_id', orgId); // ❌
218
+ return data;
219
+ }
220
+ ```
221
+
222
+ **After:**
223
+ ```typescript
224
+ async function getContacts(personId: string) {
225
+ // ✅ Use person_id directly
226
+ const { data } = await supabase
227
+ .from('pace_contact')
228
+ .select('*')
229
+ .eq('person_id', personId);
230
+ return data;
231
+ }
232
+ ```
233
+
234
+ ### Example 3: Creating Medical Profile
235
+
236
+ **Before:**
237
+ ```typescript
238
+ async function createMedicalProfile(memberId: string, orgId: string, data: MedicalData) {
239
+ const { data: profile } = await supabase
240
+ .from('medi_profile')
241
+ .insert({
242
+ member_id: memberId,
243
+ organisation_id: orgId, // ❌
244
+ ...data
245
+ })
246
+ .select()
247
+ .single();
248
+ return profile;
249
+ }
250
+ ```
251
+
252
+ **After:**
253
+ ```typescript
254
+ async function createMedicalProfile(memberId: string, data: MedicalData) {
255
+ // ✅ No organisation_id needed
256
+ const { data: profile } = await supabase
257
+ .from('medi_profile')
258
+ .insert({
259
+ member_id: memberId,
260
+ ...data
261
+ })
262
+ .select()
263
+ .single();
264
+ return profile;
265
+ }
266
+ ```
267
+
268
+ ### Example 4: Getting Person's Member in Organisation Context
269
+
270
+ **Before:**
271
+ ```typescript
272
+ // ❌ This pattern no longer works
273
+ const member = await getMemberForPersonInOrg(personId, orgId);
274
+ ```
275
+
276
+ **After:**
277
+ ```typescript
278
+ // ✅ Get the person's single membership
279
+ const { data: member } = await supabase
280
+ .from('pace_member')
281
+ .select('*')
282
+ .eq('person_id', personId)
283
+ .single();
284
+
285
+ // If you need to check organisation access, check via rbac_organisation_roles
286
+ const { data: orgRoles } = await supabase
287
+ .from('rbac_organisation_roles')
288
+ .select('organisation_id')
289
+ .eq('user_id', userId)
290
+ .eq('organisation_id', orgId)
291
+ .eq('status', 'active');
292
+ ```
293
+
294
+ ## Testing Recommendations
295
+
296
+ ### 1. Unit Tests
297
+
298
+ - [ ] Update test fixtures to remove `organisation_id` from profile records
299
+ - [ ] Update mock data generators
300
+ - [ ] Fix tests that assert organisation_id values
301
+
302
+ ### 2. Integration Tests
303
+
304
+ - [ ] Test profile creation without organisation_id
305
+ - [ ] Test profile queries by person_id
306
+ - [ ] Test contact management with person_id
307
+ - [ ] Verify RLS policies work correctly
308
+
309
+ ### 3. E2E Tests
310
+
311
+ - [ ] Test profile display across different organisations
312
+ - [ ] Verify profiles are shared correctly
313
+ - [ ] Test contact management flows
314
+ - [ ] Verify permission checks still work
315
+
316
+ ### 4. Data Validation
317
+
318
+ - [ ] Verify no duplicate memberships exist per person
319
+ - [ ] Verify medical profiles are correctly linked
320
+ - [ ] Verify contacts are correctly migrated to person_id
321
+
322
+ ## Common Patterns to Update
323
+
324
+ ### Pattern 1: Organisation-Scoped Profile Lists
325
+
326
+ **Before:**
327
+ ```typescript
328
+ // Get all members in an organisation
329
+ const { data } = await supabase
330
+ .from('pace_member')
331
+ .select('*')
332
+ .eq('organisation_id', orgId);
333
+ ```
334
+
335
+ **After:**
336
+ ```typescript
337
+ // Get all members in an organisation via person → user → org roles
338
+ const { data: orgMembers } = await supabase
339
+ .from('rbac_organisation_roles')
340
+ .select(`
341
+ organisation_id,
342
+ user_id,
343
+ pace_person!inner(
344
+ id,
345
+ pace_member(*)
346
+ )
347
+ `)
348
+ .eq('organisation_id', orgId)
349
+ .eq('status', 'active');
350
+ ```
351
+
352
+ ### Pattern 2: Profile Existence Checks
353
+
354
+ **Before:**
355
+ ```typescript
356
+ // Check if member exists in organisation
357
+ const { data } = await supabase
358
+ .from('pace_member')
359
+ .select('id')
360
+ .eq('person_id', personId)
361
+ .eq('organisation_id', orgId)
362
+ .single();
363
+ ```
364
+
365
+ **After:**
366
+ ```typescript
367
+ // Check if person has a membership (regardless of org)
368
+ const { data } = await supabase
369
+ .from('pace_member')
370
+ .select('id')
371
+ .eq('person_id', personId)
372
+ .single();
373
+ ```
374
+
375
+ ### Pattern 3: Profile Creation with Organisation Context
376
+
377
+ **Before:**
378
+ ```typescript
379
+ // Create profile for person in organisation
380
+ await supabase
381
+ .from('pace_member')
382
+ .insert({
383
+ person_id: personId,
384
+ organisation_id: orgId,
385
+ membership_number: generateNumber()
386
+ });
387
+ ```
388
+
389
+ **After:**
390
+ ```typescript
391
+ // Create profile for person (one per person, not per org)
392
+ // Check if already exists first
393
+ const { data: existing } = await supabase
394
+ .from('pace_member')
395
+ .select('id')
396
+ .eq('person_id', personId)
397
+ .single();
398
+
399
+ if (!existing) {
400
+ await supabase
401
+ .from('pace_member')
402
+ .insert({
403
+ person_id: personId,
404
+ membership_number: generateNumber()
405
+ });
406
+ }
407
+ ```
408
+
409
+ ## TypeScript Type Updates
410
+
411
+ After applying migrations, regenerate your database types:
412
+
413
+ ```bash
414
+ npx supabase gen types typescript --project-id <your-project-id> > src/types/database.generated.ts
415
+ ```
416
+
417
+ The updated types will reflect:
418
+ - Removed `organisation_id` from profile tables
419
+ - `pace_contact` using `person_id` instead of `member_id`
420
+ - Updated relationships
421
+
422
+ ## RLS Policy Behavior
423
+
424
+ RLS policies now work as follows:
425
+
426
+ 1. **Person Ownership**: Users can access profiles for persons they own (via `pace_person.user_id`)
427
+ 2. **Contact Permissions**: Users can access contacts where they are the contact person or own the person
428
+ 3. **No Organisation Filtering**: Profiles are accessible regardless of organisation context
429
+ 4. **Super Admin Override**: Super admins can access all profiles
430
+
431
+ ## Migration Support
432
+
433
+ If you encounter issues:
434
+
435
+ 1. **Check Migration Status**: Verify all migrations are applied
436
+ 2. **Verify Types**: Regenerate TypeScript types from current schema
437
+ 3. **Review RLS**: Ensure RLS policies are correctly applied
438
+ 4. **Check Logs**: Review Supabase logs for RLS policy violations
439
+
440
+ ## RPC Functions Updated
441
+
442
+ The following RPC functions have been updated to work with person-scoped profiles:
443
+
444
+ - **`data_pace_linked_profiles_list`** - Now uses `person_id` from `pace_contact` instead of `member_id`
445
+ - **`data_pace_contacts_list`** - Now uses `person_id` from `pace_contact` instead of `member_id`
446
+ - **`data_pace_contact_get`** - Now uses `person_id` from `pace_contact` instead of `member_id`
447
+
448
+ These functions now:
449
+ - Join `pace_contact` via `person_id` instead of `member_id`
450
+ - Get `member_id` from `pace_member` via `person_id` join
451
+ - Get `organisation_id` from user's organisation roles (since profiles are no longer organisation-scoped)
452
+
453
+ If you have custom RPC functions that reference `pace_contact.member_id` or profile table `organisation_id` columns, you'll need to update them similarly.
454
+
455
+ ## Questions?
456
+
457
+ For questions or issues related to this migration:
458
+ - Review the migration files in `supabase/migrations/`
459
+ - Check RLS policy definitions in the migration files
460
+ - Review the pace-core documentation for updated API patterns
461
+ - Check for custom RPC functions that may need updates
462
+
463
+ ## Summary
464
+
465
+ **Key Takeaways:**
466
+ - Profiles are now person-scoped, not organisation-scoped
467
+ - Remove all `organisation_id` filters/queries for profile tables
468
+ - Use `person_id` instead of `member_id` for contacts
469
+ - One membership per person (enforced by unique constraint)
470
+ - Update all TypeScript types after migration
471
+ - Test thoroughly, especially cross-organisation scenarios
472
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.190",
3
+ "version": "0.5.191",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -58,7 +58,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
58
58
  it('should allow anonymous access when event.public_readable = true', async () => {
59
59
  const start = Date.now();
60
60
  const { data, error } = await anonClient
61
- .from('public_recipe_details')
61
+ .from('cake_public_recipe_details')
62
62
  .select('*')
63
63
  .eq('event_id', publicEvent.event_id);
64
64
  const duration = Date.now() - start;
@@ -70,7 +70,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
70
70
 
71
71
  it('should block anonymous access when event.public_readable = false', async () => {
72
72
  const { data, error } = await anonClient
73
- .from('public_recipe_details')
73
+ .from('cake_public_recipe_details')
74
74
  .select('*')
75
75
  .eq('event_id', privateEvent.event_id);
76
76
 
@@ -80,7 +80,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
80
80
 
81
81
  it('should block anonymous access when event.is_visible = false', async () => {
82
82
  const { data, error } = await anonClient
83
- .from('public_recipe_details')
83
+ .from('cake_public_recipe_details')
84
84
  .select('*')
85
85
  .eq('event_id', 'hidden-event-1');
86
86
 
@@ -92,7 +92,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
92
92
  describe('View Column Exposure', () => {
93
93
  it('should expose only expected columns', async () => {
94
94
  const { data, error } = await anonClient
95
- .from('public_recipe_details')
95
+ .from('cake_public_recipe_details')
96
96
  .select('*')
97
97
  .eq('event_id', publicEvent.event_id)
98
98
  .limit(1)
@@ -119,7 +119,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
119
119
  it('should filter by meal type', async () => {
120
120
  const start = Date.now();
121
121
  const { data, error } = await anonClient
122
- .from('public_recipe_details')
122
+ .from('cake_public_recipe_details')
123
123
  .select('*')
124
124
  .eq('event_id', publicEvent.event_id)
125
125
  .eq('mealtype_name', 'Breakfast');
@@ -134,7 +134,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
134
134
  // This test would verify that diet filters work correctly
135
135
  // Adjust based on actual view structure
136
136
  const { data, error } = await anonClient
137
- .from('public_recipe_details')
137
+ .from('cake_public_recipe_details')
138
138
  .select('*')
139
139
  .eq('event_id', publicEvent.event_id)
140
140
  .eq('dish_dietnote', 'Vegetarian');
@@ -148,7 +148,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
148
148
  it('should complete view queries in < 500ms', async () => {
149
149
  const start = Date.now();
150
150
  await anonClient
151
- .from('public_recipe_details')
151
+ .from('cake_public_recipe_details')
152
152
  .select('*')
153
153
  .eq('event_id', publicEvent.event_id)
154
154
  .limit(100);
@@ -160,7 +160,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
160
160
  it('should handle filtered queries efficiently', async () => {
161
161
  const start = Date.now();
162
162
  await anonClient
163
- .from('public_recipe_details')
163
+ .from('cake_public_recipe_details')
164
164
  .select('*')
165
165
  .eq('event_id', publicEvent.event_id)
166
166
  .eq('mealtype_name', 'Breakfast')
@@ -175,7 +175,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
175
175
  describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE_KEY)('Public Recipe View - Authenticated Access', () => {
176
176
  it('should allow authenticated users to view public recipes', async () => {
177
177
  const { data, error } = await authenticatedClient
178
- .from('public_recipe_details')
178
+ .from('cake_public_recipe_details')
179
179
  .select('*')
180
180
  .eq('event_id', publicEvent.event_id);
181
181
 
@@ -188,7 +188,7 @@ describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_PUBLISHABLE
188
188
  // recipes from events in their organisation, even if not public_readable
189
189
  // (This depends on the view definition and RLS policies)
190
190
  const { data, error } = await authenticatedClient
191
- .from('public_recipe_details')
191
+ .from('cake_public_recipe_details')
192
192
  .select('*')
193
193
  .eq('event_id', privateEvent.event_id);
194
194
 
@@ -179,7 +179,7 @@ describe('RLS Policies - Organisations', () => {
179
179
  it('should allow super admin to view all organisations', async () => {
180
180
  const start = Date.now();
181
181
  const { data, error } = await superAdminClient
182
- .from('organisations')
182
+ .from('core_organisations')
183
183
  .select('*')
184
184
  .limit(10);
185
185
  const duration = Date.now() - start;
@@ -192,7 +192,7 @@ describe('RLS Policies - Organisations', () => {
192
192
 
193
193
  it('should allow super admin to update any organisation', async () => {
194
194
  const { data, error } = await superAdminClient
195
- .from('organisations')
195
+ .from('core_organisations')
196
196
  .update({ name: 'Updated Name' })
197
197
  .eq('id', testOrganisation1.id)
198
198
  .select()
@@ -214,7 +214,7 @@ describe('RLS Policies - Organisations', () => {
214
214
  it('should allow org admin to view their organisation', async () => {
215
215
  const start = Date.now();
216
216
  const { data, error } = await orgAdminClient
217
- .from('organisations')
217
+ .from('core_organisations')
218
218
  .select('*')
219
219
  .eq('id', testOrganisation1.id)
220
220
  .single();
@@ -232,7 +232,7 @@ describe('RLS Policies - Organisations', () => {
232
232
 
233
233
  it('should block org admin from viewing other organisations', async () => {
234
234
  const { data, error } = await orgAdminClient
235
- .from('organisations')
235
+ .from('core_organisations')
236
236
  .select('*')
237
237
  .eq('id', testOrganisation2.id)
238
238
  .single();
@@ -246,7 +246,7 @@ describe('RLS Policies - Organisations', () => {
246
246
  it('should allow member to view their organisation', async () => {
247
247
  const start = Date.now();
248
248
  const { data, error } = await regularMemberClient
249
- .from('organisations')
249
+ .from('core_organisations')
250
250
  .select('*')
251
251
  .eq('id', testOrganisation1.id)
252
252
  .single();
@@ -264,7 +264,7 @@ describe('RLS Policies - Organisations', () => {
264
264
 
265
265
  it('should block member from updating organisation', async () => {
266
266
  const { data, error } = await regularMemberClient
267
- .from('organisations')
267
+ .from('core_organisations')
268
268
  .update({ name: 'Unauthorized Update' })
269
269
  .eq('id', testOrganisation1.id);
270
270
 
@@ -288,7 +288,7 @@ describe('RLS Policies - Organisations', () => {
288
288
  describe('Anonymous Access', () => {
289
289
  it('should block anonymous users from viewing organisations', async () => {
290
290
  const { data, error } = await anonClient
291
- .from('organisations')
291
+ .from('core_organisations')
292
292
  .select('*');
293
293
 
294
294
  // Should return empty (RLS blocks anonymous access)
@@ -302,7 +302,7 @@ describe('RLS Policies - Events', () => {
302
302
  it('should allow anonymous access to public events', async () => {
303
303
  const start = Date.now();
304
304
  const { data, error } = await anonClient
305
- .from('event')
305
+ .from('core_events')
306
306
  .select('*')
307
307
  .eq('event_id', testEvent.event_id)
308
308
  .eq('public_readable', true)
@@ -321,7 +321,7 @@ describe('RLS Policies - Events', () => {
321
321
 
322
322
  it('should block anonymous access to non-public events', async () => {
323
323
  const { data, error } = await anonClient
324
- .from('event')
324
+ .from('core_events')
325
325
  .select('*')
326
326
  .eq('event_id', testEvent.event_id)
327
327
  .eq('public_readable', false)
@@ -336,7 +336,7 @@ describe('RLS Policies - Events', () => {
336
336
  it('should allow org member to view events in their organisation', async () => {
337
337
  const start = Date.now();
338
338
  const { data, error } = await regularMemberClient
339
- .from('event')
339
+ .from('core_events')
340
340
  .select('*')
341
341
  .eq('organisation_id', testOrganisation1.id)
342
342
  .limit(10);
@@ -411,7 +411,7 @@ describe('RLS Policies - Performance', () => {
411
411
  it('should complete organisation queries in < 1 second', async () => {
412
412
  const start = Date.now();
413
413
  await superAdminClient
414
- .from('organisations')
414
+ .from('core_organisations')
415
415
  .select('*')
416
416
  .limit(100);
417
417
  const duration = Date.now() - start;
@@ -422,7 +422,7 @@ describe('RLS Policies - Performance', () => {
422
422
  it('should complete event queries in < 1 second', async () => {
423
423
  const start = Date.now();
424
424
  await superAdminClient
425
- .from('event')
425
+ .from('core_events')
426
426
  .select('*')
427
427
  .eq('is_visible', true)
428
428
  .limit(100);
@@ -452,7 +452,7 @@ describe('RLS Policies - Helper Functions', () => {
452
452
 
453
453
  const { data, error } = await superAdminClient
454
454
  .rpc('check_query_performance', {
455
- p_query: 'SELECT * FROM organisations LIMIT 1'
455
+ p_query: 'SELECT * FROM core_organisations LIMIT 1'
456
456
  });
457
457
 
458
458
  // Note: If the RPC function doesn't exist or uses EXPLAIN incorrectly, we'll get an error