@jmruthers/pace-core 0.5.185 → 0.5.187

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 (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @file Google Places API Types
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces
5
+ * @since 0.1.0
6
+ */
7
+
8
+ /**
9
+ * Google Places Autocomplete API response prediction
10
+ */
11
+ export interface GooglePlaceAutocompletePrediction {
12
+ description: string;
13
+ place_id: string;
14
+ structured_formatting?: {
15
+ main_text: string;
16
+ secondary_text: string;
17
+ };
18
+ types?: string[];
19
+ }
20
+
21
+ /**
22
+ * Google Places Autocomplete API response
23
+ */
24
+ export interface GooglePlaceAutocompleteResponse {
25
+ predictions: GooglePlaceAutocompletePrediction[];
26
+ status: string;
27
+ error_message?: string;
28
+ }
29
+
30
+ /**
31
+ * Google Places Details API address component
32
+ */
33
+ export interface GoogleAddressComponent {
34
+ long_name: string;
35
+ short_name: string;
36
+ types: string[];
37
+ }
38
+
39
+ /**
40
+ * Google Places Details API geometry
41
+ */
42
+ export interface GoogleGeometry {
43
+ location: {
44
+ lat: number;
45
+ lng: number;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Google Places Details API response
51
+ */
52
+ export interface GooglePlaceDetailsResponse {
53
+ result: {
54
+ place_id: string;
55
+ formatted_address: string;
56
+ address_components: GoogleAddressComponent[];
57
+ geometry: GoogleGeometry;
58
+ };
59
+ status: string;
60
+ error_message?: string;
61
+ }
62
+
63
+ /**
64
+ * Parsed address matching pace_address table structure
65
+ */
66
+ export interface ParsedAddress {
67
+ place_id: string;
68
+ full_address: string | null;
69
+ street_number: string | null;
70
+ route: string | null;
71
+ suburb: string | null;
72
+ state: string | null;
73
+ postcode: string | null;
74
+ country: string | null;
75
+ lat: number | null;
76
+ lng: number | null;
77
+ }
78
+
79
+ /**
80
+ * Options for Google Places Autocomplete API
81
+ */
82
+ export interface AutocompleteOptions {
83
+ /** Restrict results to specific countries (ISO 3166-1 Alpha-2 country codes) */
84
+ components?: string;
85
+ /** Location bias (lat,lng) */
86
+ location?: string;
87
+ /** Radius in meters for location bias */
88
+ radius?: number;
89
+ /** Restrict results to specific place types */
90
+ types?: string;
91
+ /** Language code for results */
92
+ language?: string;
93
+ }
94
+
@@ -156,3 +156,26 @@ export {
156
156
  getGoogleMapsUrl
157
157
  } from './location';
158
158
  export type { Coordinates } from './location';
159
+
160
+ // Google Places utilities
161
+ export {
162
+ fetchPlaceAutocomplete,
163
+ fetchPlaceDetails,
164
+ parseAddressComponents,
165
+ createAddressFromPlaceResult,
166
+ getAddressByPlaceId,
167
+ } from './google-places';
168
+ export type {
169
+ GooglePlaceAutocompletePrediction,
170
+ ParsedAddress,
171
+ AutocompleteOptions,
172
+ } from './google-places';
173
+
174
+ // Request deduplication utilities
175
+ export {
176
+ generateRequestKey,
177
+ getOrCreateRequest,
178
+ clearInFlightRequests,
179
+ getInFlightRequestStats,
180
+ deduplicatedQuery
181
+ } from './request-deduplication';
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Request Deduplication Utility
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/RequestDeduplication
5
+ * @since 2.0.0
6
+ *
7
+ * Provides request deduplication to prevent duplicate in-flight requests.
8
+ * When multiple components request the same data simultaneously, only one
9
+ * request is made and all callers share the same promise.
10
+ */
11
+
12
+ import { createLogger } from './core/logger';
13
+
14
+ const log = createLogger('request-deduplication');
15
+
16
+ /**
17
+ * In-flight request cache
18
+ * Key: request identifier (e.g., "GET:table:filter:value")
19
+ * Value: Promise that resolves to the request result
20
+ */
21
+ const inFlightRequests = new Map<string, Promise<any>>();
22
+
23
+ /**
24
+ * Generate a request key from request parameters
25
+ *
26
+ * @param method - HTTP method (GET, POST, etc.)
27
+ * @param table - Table name
28
+ * @param filters - Filter object
29
+ * @param select - Select columns
30
+ * @returns Request key string
31
+ */
32
+ export function generateRequestKey(
33
+ method: string,
34
+ table: string,
35
+ filters?: Record<string, any>,
36
+ select?: string
37
+ ): string {
38
+ const filterStr = filters ? JSON.stringify(filters) : '';
39
+ const selectStr = select || '*';
40
+ return `${method}:${table}:${filterStr}:${selectStr}`;
41
+ }
42
+
43
+ /**
44
+ * Get or create a request
45
+ *
46
+ * If a request with the same key is already in-flight, returns the existing promise.
47
+ * Otherwise, creates a new request and stores it for deduplication.
48
+ *
49
+ * @param key - Request key
50
+ * @param requestFn - Function that performs the actual request
51
+ * @returns Promise that resolves to the request result
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const data = await getOrCreateRequest(
56
+ * 'GET:pace_person:{"user_id":"123"}',
57
+ * async () => {
58
+ * const { data } = await supabase
59
+ * .from('pace_person')
60
+ * .select('id, first_name')
61
+ * .eq('user_id', '123')
62
+ * .single();
63
+ * return data;
64
+ * }
65
+ * );
66
+ * ```
67
+ */
68
+ export async function getOrCreateRequest<T>(
69
+ key: string,
70
+ requestFn: () => Promise<T>
71
+ ): Promise<T> {
72
+ // Check if request is already in-flight
73
+ const existingRequest = inFlightRequests.get(key);
74
+ if (existingRequest) {
75
+ log.debug(`Request deduplication: reusing in-flight request for ${key}`);
76
+ return existingRequest as Promise<T>;
77
+ }
78
+
79
+ // Create new request
80
+ log.debug(`Creating new request for ${key}`);
81
+ const requestPromise = requestFn()
82
+ .then((result) => {
83
+ // Remove from in-flight cache after completion
84
+ inFlightRequests.delete(key);
85
+ return result;
86
+ })
87
+ .catch((error) => {
88
+ // Remove from in-flight cache on error
89
+ inFlightRequests.delete(key);
90
+ throw error;
91
+ });
92
+
93
+ // Store in-flight request
94
+ inFlightRequests.set(key, requestPromise);
95
+
96
+ return requestPromise;
97
+ }
98
+
99
+ /**
100
+ * Clear all in-flight requests
101
+ *
102
+ * Useful for cleanup or testing.
103
+ */
104
+ export function clearInFlightRequests(): void {
105
+ const count = inFlightRequests.size;
106
+ inFlightRequests.clear();
107
+ log.debug(`Cleared ${count} in-flight requests`);
108
+ }
109
+
110
+ /**
111
+ * Get statistics about in-flight requests
112
+ *
113
+ * @returns Statistics object
114
+ */
115
+ export function getInFlightRequestStats(): {
116
+ count: number;
117
+ keys: string[];
118
+ } {
119
+ return {
120
+ count: inFlightRequests.size,
121
+ keys: Array.from(inFlightRequests.keys()),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Supabase query wrapper with automatic deduplication
127
+ *
128
+ * Wraps a Supabase query to automatically deduplicate identical requests.
129
+ *
130
+ * @param supabase - Supabase client
131
+ * @param table - Table name
132
+ * @param filters - Filter object (e.g., { user_id: '123' })
133
+ * @param select - Select columns (default: '*')
134
+ * @param requestFn - Function that performs the query
135
+ * @returns Promise that resolves to query result
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const person = await deduplicatedQuery(
140
+ * supabase,
141
+ * 'pace_person',
142
+ * { user_id: userId },
143
+ * 'id, first_name, last_name',
144
+ * async () => {
145
+ * const { data } = await supabase
146
+ * .from('pace_person')
147
+ * .select('id, first_name, last_name')
148
+ * .eq('user_id', userId)
149
+ * .single();
150
+ * return data;
151
+ * }
152
+ * );
153
+ * ```
154
+ */
155
+ export async function deduplicatedQuery<T>(
156
+ supabase: any,
157
+ table: string,
158
+ filters: Record<string, any>,
159
+ select: string,
160
+ requestFn: () => Promise<T>
161
+ ): Promise<T> {
162
+ const key = generateRequestKey('GET', table, filters, select);
163
+ return getOrCreateRequest(key, requestFn);
164
+ }
165
+
@@ -102,7 +102,7 @@ export const createSecureDataAccess = (
102
102
  'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member',
103
103
  // SECURITY: Phase 3A additions - medical and personal data
104
104
  'medi_profile', 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
105
- 'pace_consent', 'pace_contact', 'pace_id_documents', 'pace_qualifications',
105
+ 'pace_consent', 'pace_contact', 'pace_identification', 'pace_identification_type', 'pace_qualification',
106
106
  'form_responses', 'form_response_values', 'forms',
107
107
  // SECURITY: Phase 3B additions - remaining critical tables
108
108
  'invoice', 'line_item', 'credit_balance', 'payment_method',
@@ -30,21 +30,21 @@ export function generateFilePath(options: StorageUploadOptions, fileName: string
30
30
  }
31
31
 
32
32
  if (isPublic) {
33
- // Public files go to {orgId}/{category}/filename
33
+ // Public files go to {orgId}/{folder}/filename
34
34
  if (customPath) {
35
35
  return `${orgId}/${customPath}/${fileName}`;
36
36
  }
37
37
  return `${orgId}/public/${fileName}`;
38
38
  }
39
39
 
40
- // Organization-first structure: {orgId}/{category}/filename
40
+ // Organization-first structure: {orgId}/{folder}/filename
41
41
  if (customPath) {
42
42
  return `${orgId}/${customPath}/${fileName}`;
43
43
  }
44
44
 
45
45
  // Use customPath if available, otherwise default to files
46
- const pathCategory = customPath || 'files';
47
- return `${orgId}/${pathCategory}/${fileName}`;
46
+ const pathFolder = customPath || 'files';
47
+ return `${orgId}/${pathFolder}/${fileName}`;
48
48
  }
49
49
 
50
50
  /**
@@ -153,6 +153,66 @@ async function generateFileHash(file: File): Promise<string> {
153
153
  return `sha256:${hashHex}`;
154
154
  }
155
155
 
156
+ /**
157
+ * Ensure a folder exists in the storage bucket
158
+ * In Supabase storage, folders are created automatically when files are uploaded,
159
+ * but this function explicitly creates the folder structure by uploading a placeholder
160
+ * file if the folder doesn't exist
161
+ * @param supabase - Supabase client instance
162
+ * @param folderPath - Folder path to ensure exists (e.g., 'orgId/folder')
163
+ * @param bucketName - Bucket name
164
+ * @returns True if folder exists or was created, false on error
165
+ */
166
+ async function ensureFolderExists(
167
+ supabase: SupabaseClient,
168
+ folderPath: string,
169
+ bucketName: string
170
+ ): Promise<boolean> {
171
+ try {
172
+ // Check if folder exists by trying to list it
173
+ const { data, error } = await supabase.storage
174
+ .from(bucketName)
175
+ .list(folderPath, {
176
+ limit: 1
177
+ });
178
+
179
+ // If listing succeeds (even with empty data), the folder exists
180
+ if (!error) {
181
+ return true;
182
+ }
183
+
184
+ // If we get a "not found" error, the folder doesn't exist yet
185
+ // Create it by uploading a placeholder file
186
+ // Supabase storage doesn't support empty folders, so we create a .keep file
187
+ const placeholderPath = `${folderPath}/.keep`;
188
+ const placeholderBlob = new Blob([''], { type: 'text/plain' });
189
+ const placeholderFile = new File([placeholderBlob], '.keep', { type: 'text/plain' });
190
+
191
+ const { error: uploadError } = await supabase.storage
192
+ .from(bucketName)
193
+ .upload(placeholderPath, placeholderFile, {
194
+ cacheControl: '3600',
195
+ upsert: true, // Use upsert to avoid errors if file already exists
196
+ contentType: 'text/plain'
197
+ });
198
+
199
+ if (uploadError) {
200
+ // If we can't create the placeholder, log it but don't fail
201
+ // The folder will be created automatically when we upload the actual file
202
+ log.debug(`Could not create folder placeholder (will be created on upload): ${uploadError.message}`);
203
+ return true; // Still return true - folder will be created on actual file upload
204
+ }
205
+
206
+ // Folder structure now exists
207
+ return true;
208
+ } catch (error) {
209
+ // If there's an exception, log it but proceed anyway
210
+ // The folder structure will be created when we upload the actual file
211
+ log.debug(`Folder creation exception (will be created on upload): ${error instanceof Error ? error.message : 'Unknown error'}`);
212
+ return true; // Return true to proceed - folder will be created on actual file upload
213
+ }
214
+ }
215
+
156
216
  /**
157
217
  * Upload a file to Supabase storage with app segregation
158
218
  */
@@ -175,13 +235,21 @@ export async function uploadFile(
175
235
  const uniqueFileName = generateUniqueFileName(file.name);
176
236
  const filePath = generateFilePath(options, uniqueFileName);
177
237
 
238
+ // Extract folder path from file path (everything except the filename)
239
+ const folderPath = filePath.substring(0, filePath.lastIndexOf('/'));
240
+
178
241
  // Extract metadata
179
242
  const metadata = await extractFileMetadata(file, options, 'current-user'); // TODO: Get actual user ID
180
243
 
181
244
  // Select bucket based on isPublic flag
182
245
  const bucketName = getBucketName(options.isPublic || false);
183
246
 
247
+ // Ensure folder exists (Supabase creates folders automatically on upload,
248
+ // but we verify the path is accessible)
249
+ await ensureFolderExists(supabase, folderPath, bucketName);
250
+
184
251
  // Upload file to Supabase
252
+ // Note: Supabase will automatically create the folder structure if it doesn't exist
185
253
  const { data, error } = await supabase.storage
186
254
  .from(bucketName)
187
255
  .upload(filePath, file, {
@@ -352,6 +420,145 @@ export async function getSignedUrl(
352
420
  }
353
421
  }
354
422
 
423
+ // Global URL cache for batch operations (shared with useFileUrlCache)
424
+ const globalUrlCache = new Map<string, { url: string; expiresAt: number }>();
425
+ const MAX_CACHE_SIZE = 500;
426
+ const DEFAULT_TTL_MS = 3600 * 1000;
427
+
428
+ function getCacheKey(fileId: string, filePath: string, isPublic: boolean): string {
429
+ return `file-url:${fileId}:${isPublic ? 'public' : 'private'}`;
430
+ }
431
+
432
+ function cleanupUrlCache(): void {
433
+ const now = Date.now();
434
+
435
+ // Remove expired entries
436
+ for (const [key, value] of globalUrlCache.entries()) {
437
+ if (value.expiresAt < now) {
438
+ globalUrlCache.delete(key);
439
+ }
440
+ }
441
+
442
+ // Enforce size limit by removing oldest entries
443
+ if (globalUrlCache.size > MAX_CACHE_SIZE) {
444
+ const entries = Array.from(globalUrlCache.entries());
445
+ entries.sort((a, b) => a[1].expiresAt - b[1].expiresAt);
446
+
447
+ const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
448
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
449
+ globalUrlCache.delete(entries[i][0]);
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Generate URLs for multiple file references in parallel
456
+ * This batches URL generation to reduce sequential requests and uses caching
457
+ * @param supabase - Supabase client instance
458
+ * @param fileReferences - Array of file references to generate URLs for
459
+ * @param options - URL options including expiry time and organisation ID
460
+ * @returns Map of file ID to URL string (only includes successful URL generations)
461
+ */
462
+ export async function generateFileUrlsBatch(
463
+ supabase: SupabaseClient,
464
+ fileReferences: Array<{ id: string; file_path: string; is_public: boolean }>,
465
+ options: StorageUrlOptions & { orgId?: string }
466
+ ): Promise<Map<string, string>> {
467
+ const urlMap = new Map<string, string>();
468
+
469
+ if (fileReferences.length === 0) {
470
+ return urlMap;
471
+ }
472
+
473
+ const now = Date.now();
474
+ const ttl = (options.expiresIn || 3600) * 1000; // Convert seconds to milliseconds
475
+
476
+ // Separate files into cached, public, and private
477
+ const publicFiles: Array<{ id: string; file_path: string }> = [];
478
+ const privateFiles: Array<{ id: string; file_path: string }> = [];
479
+ const uncachedFiles: Array<{ id: string; file_path: string; is_public: boolean }> = [];
480
+
481
+ for (const fileRef of fileReferences) {
482
+ const cacheKey = getCacheKey(fileRef.id, fileRef.file_path, fileRef.is_public);
483
+ const cached = globalUrlCache.get(cacheKey);
484
+
485
+ // Use cached URL if still valid
486
+ if (cached && cached.expiresAt > now) {
487
+ urlMap.set(fileRef.id, cached.url);
488
+ continue;
489
+ }
490
+
491
+ // Add to processing queue
492
+ if (fileRef.is_public) {
493
+ publicFiles.push({ id: fileRef.id, file_path: fileRef.file_path });
494
+ } else {
495
+ privateFiles.push({ id: fileRef.id, file_path: fileRef.file_path });
496
+ }
497
+ uncachedFiles.push(fileRef);
498
+ }
499
+
500
+ // Generate public URLs synchronously (they're just string concatenation)
501
+ for (const file of publicFiles) {
502
+ try {
503
+ const url = getPublicUrl(supabase, file.file_path, true);
504
+ if (url) {
505
+ urlMap.set(file.id, url);
506
+ // Cache the URL
507
+ const cacheKey = getCacheKey(file.id, file.file_path, true);
508
+ globalUrlCache.set(cacheKey, {
509
+ url,
510
+ expiresAt: now + ttl
511
+ });
512
+ }
513
+ } catch (err) {
514
+ log.error(`Failed to generate public URL for file ${file.id}:`, err);
515
+ }
516
+ }
517
+
518
+ // Generate signed URLs in parallel using Promise.all
519
+ if (privateFiles.length > 0) {
520
+ const signedUrlPromises = privateFiles.map(async (file) => {
521
+ try {
522
+ const signedUrlResult = await getSignedUrl(supabase, file.file_path, {
523
+ appName: options.appName || 'pace-core',
524
+ orgId: options.orgId,
525
+ expiresIn: options.expiresIn || 3600
526
+ });
527
+ const url = signedUrlResult?.url || null;
528
+
529
+ // Cache the URL if generated successfully
530
+ if (url) {
531
+ const cacheKey = getCacheKey(file.id, file.file_path, false);
532
+ globalUrlCache.set(cacheKey, {
533
+ url,
534
+ expiresAt: now + ttl
535
+ });
536
+ }
537
+
538
+ return { id: file.id, url };
539
+ } catch (err) {
540
+ log.error(`Failed to generate signed URL for file ${file.id}:`, err);
541
+ return { id: file.id, url: null };
542
+ }
543
+ });
544
+
545
+ const signedUrlResults = await Promise.all(signedUrlPromises);
546
+
547
+ for (const result of signedUrlResults) {
548
+ if (result.url) {
549
+ urlMap.set(result.id, result.url);
550
+ }
551
+ }
552
+ }
553
+
554
+ // Clean up cache after adding new entries
555
+ if (uncachedFiles.length > 0) {
556
+ cleanupUrlCache();
557
+ }
558
+
559
+ return urlMap;
560
+ }
561
+
355
562
  /**
356
563
  * Delete a file from storage
357
564
  * @param supabase - Supabase client instance