@jmruthers/pace-core 0.5.186 → 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 (284) 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-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  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-OALXJH4Y.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-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
  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-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  23. package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
  26. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  27. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/implementation-guides/file-upload-storage.md +29 -0
  208. package/docs/rbac/README.md +2 -1
  209. package/docs/rbac/api-reference.md +11 -0
  210. package/docs/rbac/performance.md +320 -0
  211. package/docs/standards/01-architecture-standard.md +5 -0
  212. package/docs/standards/05-security-standard.md +12 -0
  213. package/package.json +1 -1
  214. package/src/components/AddressField/AddressField.test.tsx +411 -0
  215. package/src/components/AddressField/AddressField.tsx +323 -0
  216. package/src/components/AddressField/README.md +336 -0
  217. package/src/components/AddressField/index.ts +10 -0
  218. package/src/components/AddressField/types.ts +65 -0
  219. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  220. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  221. package/src/components/index.ts +2 -0
  222. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  223. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  224. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  227. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  228. package/src/hooks/useAddressAutocomplete.ts +268 -0
  229. package/src/hooks/useFileDisplay.ts +3 -15
  230. package/src/hooks/useFileReference.test.ts +20 -3
  231. package/src/hooks/useFileReference.ts +3 -24
  232. package/src/hooks/useFileUrlCache.ts +246 -0
  233. package/src/hooks/useInactivityTracker.ts +31 -20
  234. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  235. package/src/hooks/useOrganisationSecurity.ts +3 -3
  236. package/src/hooks/useQueryCache.ts +315 -0
  237. package/src/index.ts +2 -0
  238. package/src/providers/services/EventServiceProvider.tsx +4 -1
  239. package/src/rbac/api.test.ts +21 -6
  240. package/src/rbac/api.ts +32 -11
  241. package/src/rbac/audit-batched.ts +223 -0
  242. package/src/rbac/audit-enhanced.ts +2 -2
  243. package/src/rbac/audit.test.ts +6 -5
  244. package/src/rbac/audit.ts +34 -6
  245. package/src/rbac/cache-invalidation.ts +63 -12
  246. package/src/rbac/cache.test.ts +2 -2
  247. package/src/rbac/cache.ts +61 -14
  248. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  249. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  250. package/src/rbac/config.ts +9 -0
  251. package/src/rbac/engine.ts +2 -21
  252. package/src/rbac/hooks/usePermissions.ts +21 -5
  253. package/src/rbac/index.ts +19 -0
  254. package/src/rbac/performance.ts +210 -0
  255. package/src/rbac/request-deduplication.ts +87 -0
  256. package/src/rbac/utils/deep-equal.ts +93 -0
  257. package/src/types/file-reference.ts +0 -1
  258. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  259. package/src/utils/file-reference/index.ts +44 -15
  260. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  261. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  262. package/src/utils/google-places/index.ts +26 -0
  263. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  264. package/src/utils/google-places/types.ts +94 -0
  265. package/src/utils/index.ts +23 -0
  266. package/src/utils/request-deduplication.ts +165 -0
  267. package/src/utils/storage/helpers.ts +143 -4
  268. package/dist/chunk-445GEP27.js.map +0 -1
  269. package/dist/chunk-FMUCXFII.js +0 -76
  270. package/dist/chunk-FMUCXFII.js.map +0 -1
  271. package/dist/chunk-FSFQFJCU.js.map +0 -1
  272. package/dist/chunk-FXFJRTKI.js.map +0 -1
  273. package/dist/chunk-GRIQLQ52.js.map +0 -1
  274. package/dist/chunk-HDCUMOOI.js.map +0 -1
  275. package/dist/chunk-OALXJH4Y.js.map +0 -1
  276. package/dist/chunk-TC7D3CR3.js.map +0 -1
  277. package/dist/chunk-U6WNSFX5.js.map +0 -1
  278. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  279. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  280. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  281. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  282. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  283. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
  284. /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * @file Google Places API Utilities
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces
5
+ * @since 0.1.0
6
+ *
7
+ * Utility functions for interacting with Google Places API.
8
+ * Uses the Google Maps JavaScript API to avoid CORS issues.
9
+ *
10
+ * Features:
11
+ * - Places Autocomplete Service integration
12
+ * - Places Service integration
13
+ * - Address component parsing
14
+ * - Error handling
15
+ * - Request deduplication
16
+ */
17
+
18
+ import { getOrCreateRequest } from '../request-deduplication';
19
+ import { createLogger } from '../core/logger';
20
+ import { loadGoogleMapsScript, isGoogleMapsLoaded } from './loadGoogleMapsScript';
21
+ import type {
22
+ GooglePlaceAutocompletePrediction,
23
+ ParsedAddress,
24
+ AutocompleteOptions,
25
+ } from './types';
26
+
27
+ const log = createLogger('google-places');
28
+
29
+ // Google Maps types are defined in loadGoogleMapsScript.ts
30
+
31
+ /**
32
+ * Fetch place autocomplete predictions using Google Maps JavaScript API
33
+ *
34
+ * @param query - Search query string
35
+ * @param apiKey - Google Places API key
36
+ * @param options - Optional autocomplete options
37
+ * @returns Promise resolving to array of predictions
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const predictions = await fetchPlaceAutocomplete(
42
+ * '123 Main St',
43
+ * 'your-api-key',
44
+ * { components: 'country:au' }
45
+ * );
46
+ * ```
47
+ */
48
+ export async function fetchPlaceAutocomplete(
49
+ query: string,
50
+ apiKey: string,
51
+ options?: AutocompleteOptions
52
+ ): Promise<GooglePlaceAutocompletePrediction[]> {
53
+ if (!query.trim()) {
54
+ return [];
55
+ }
56
+
57
+ if (!apiKey) {
58
+ throw new Error('Google Places API key is required');
59
+ }
60
+
61
+ // Ensure Google Maps script is loaded
62
+ if (!isGoogleMapsLoaded()) {
63
+ await loadGoogleMapsScript(apiKey, 'places');
64
+ }
65
+
66
+ const requestKey = `google-places-autocomplete:${query}:${JSON.stringify(options || {})}`;
67
+
68
+ return getOrCreateRequest(requestKey, async () => {
69
+ try {
70
+ log.debug(`Fetching autocomplete for query: ${query}`);
71
+
72
+ if (!window.google?.maps?.places) {
73
+ throw new Error('Google Maps Places library not available');
74
+ }
75
+
76
+ // Try to use the new AutocompleteSuggestion API first, fallback to legacy AutocompleteService
77
+ const useNewAPI = window.google.maps.places.AutocompleteSuggestion !== undefined;
78
+
79
+ if (useNewAPI) {
80
+ // Use new AutocompleteSuggestion API
81
+ const request: {
82
+ input: string;
83
+ includedRegionCodes?: string[];
84
+ locationBias?: {
85
+ circle?: {
86
+ center: { latitude: number; longitude: number };
87
+ radius: number;
88
+ };
89
+ };
90
+ includedPrimaryTypes?: string[];
91
+ languageCode?: string;
92
+ } = {
93
+ input: query.trim(),
94
+ };
95
+
96
+ // Parse components option (e.g., "country:au" -> ["au"])
97
+ if (options?.components) {
98
+ const componentParts = options.components.split('|');
99
+ const countries: string[] = [];
100
+
101
+ componentParts.forEach((part) => {
102
+ const [key, value] = part.split(':');
103
+ if (key === 'country') {
104
+ countries.push(...value.split(',').map(c => c.toUpperCase()));
105
+ }
106
+ });
107
+
108
+ if (countries.length > 0) {
109
+ request.includedRegionCodes = countries;
110
+ }
111
+ }
112
+
113
+ if (options?.location && options?.radius) {
114
+ const [lat, lng] = options.location.split(',').map(Number);
115
+ request.locationBias = {
116
+ circle: {
117
+ center: { latitude: lat, longitude: lng },
118
+ radius: options.radius,
119
+ },
120
+ };
121
+ }
122
+
123
+ if (options?.language) {
124
+ request.languageCode = options.language;
125
+ }
126
+
127
+ // Call new API
128
+ return new Promise<GooglePlaceAutocompletePrediction[]>((resolve, reject) => {
129
+ window.google!.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(request)
130
+ .then((response) => {
131
+ if (response.suggestions && response.suggestions.length > 0) {
132
+ const result: GooglePlaceAutocompletePrediction[] = response.suggestions
133
+ .filter(s => s.placePrediction)
134
+ .map((s) => ({
135
+ description: s.placePrediction!.text.text,
136
+ place_id: s.placePrediction!.placeId,
137
+ structured_formatting: {
138
+ main_text: s.placePrediction!.structuredFormat?.mainText?.text || s.placePrediction!.text.text,
139
+ secondary_text: s.placePrediction!.structuredFormat?.secondaryText?.text || '',
140
+ },
141
+ }));
142
+ log.debug(`Received ${result.length} predictions (new API)`);
143
+ resolve(result);
144
+ } else {
145
+ log.debug('No results found (new API)');
146
+ resolve([]);
147
+ }
148
+ })
149
+ .catch((error) => {
150
+ log.error('Autocomplete fetch failed (new API):', error);
151
+ reject(new Error(`Failed to fetch autocomplete predictions: ${error.message || 'Unknown error'}`));
152
+ });
153
+ });
154
+ }
155
+
156
+ // Fallback to legacy AutocompleteService
157
+ const autocompleteService = new window.google.maps.places.AutocompleteService();
158
+
159
+ // Build request
160
+ const request: {
161
+ input: string;
162
+ componentRestrictions?: { country: string | string[] };
163
+ location?: { lat: () => number; lng: () => number };
164
+ radius?: number;
165
+ types?: string[];
166
+ language?: string;
167
+ } = {
168
+ input: query.trim(),
169
+ };
170
+
171
+ // Parse components option (e.g., "country:au" -> { country: "AU" })
172
+ if (options?.components) {
173
+ const componentParts = options.components.split('|');
174
+ const restrictions: { country?: string[] } = {};
175
+ const countries: string[] = [];
176
+
177
+ componentParts.forEach((part) => {
178
+ const [key, value] = part.split(':');
179
+ if (key === 'country') {
180
+ // Convert to uppercase for consistency with Google Maps API
181
+ countries.push(...value.split(',').map(c => c.toUpperCase()));
182
+ }
183
+ });
184
+
185
+ if (countries.length > 0) {
186
+ request.componentRestrictions = { country: countries.length === 1 ? countries[0] : countries };
187
+ }
188
+ }
189
+
190
+ if (options?.location) {
191
+ const [lat, lng] = options.location.split(',').map(Number);
192
+ if (window.google?.maps?.LatLng) {
193
+ request.location = new window.google.maps.LatLng(lat, lng);
194
+ }
195
+ }
196
+
197
+ if (options?.radius) {
198
+ request.radius = options.radius;
199
+ }
200
+
201
+ if (options?.types) {
202
+ request.types = [options.types];
203
+ }
204
+
205
+ if (options?.language) {
206
+ request.language = options.language;
207
+ }
208
+
209
+ // Call AutocompleteService
210
+ return new Promise<GooglePlaceAutocompletePrediction[]>((resolve, reject) => {
211
+ autocompleteService.getPlacePredictions(request, (predictions, status) => {
212
+ if (status === 'OK' && predictions) {
213
+ log.debug(`Received ${predictions.length} predictions`);
214
+ // Convert to our format
215
+ const result: GooglePlaceAutocompletePrediction[] = predictions.map((pred) => ({
216
+ description: pred.description,
217
+ place_id: pred.place_id,
218
+ structured_formatting: {
219
+ main_text: pred.structured_formatting.main_text,
220
+ secondary_text: pred.structured_formatting.secondary_text,
221
+ },
222
+ }));
223
+ resolve(result);
224
+ } else if (status === 'ZERO_RESULTS') {
225
+ log.debug('No results found');
226
+ resolve([]);
227
+ } else {
228
+ const errorMsg = `Google Places API error: ${status}`;
229
+ log.error('Autocomplete fetch failed:', errorMsg);
230
+ reject(new Error(errorMsg));
231
+ }
232
+ });
233
+ });
234
+ } catch (error) {
235
+ if (error instanceof Error) {
236
+ log.error('Autocomplete fetch failed:', error.message);
237
+ throw error;
238
+ }
239
+ log.error('Autocomplete fetch failed: Unknown error');
240
+ throw new Error('Failed to fetch autocomplete predictions');
241
+ }
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Fetch place details from Google Places API
247
+ *
248
+ * @param placeId - Google Place ID
249
+ * @param apiKey - Google Places API key
250
+ * @returns Promise resolving to place details
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * const place = await fetchPlaceDetails('ChIJ...', 'your-api-key');
255
+ * ```
256
+ */
257
+ export async function fetchPlaceDetails(
258
+ placeId: string,
259
+ apiKey: string
260
+ ): Promise<{
261
+ place_id: string;
262
+ formatted_address?: string;
263
+ address_components?: Array<{
264
+ long_name: string;
265
+ short_name: string;
266
+ types: string[];
267
+ }>;
268
+ geometry?: {
269
+ location?: {
270
+ lat: () => number;
271
+ lng: () => number;
272
+ };
273
+ };
274
+ }> {
275
+ if (!placeId) {
276
+ throw new Error('Place ID is required');
277
+ }
278
+
279
+ if (!apiKey) {
280
+ throw new Error('Google Places API key is required');
281
+ }
282
+
283
+ // Ensure Google Maps script is loaded
284
+ if (!isGoogleMapsLoaded()) {
285
+ await loadGoogleMapsScript(apiKey, 'places');
286
+ }
287
+
288
+ const requestKey = `google-places-details:${placeId}`;
289
+
290
+ return getOrCreateRequest(requestKey, async () => {
291
+ try {
292
+ log.debug(`Fetching place details for place_id: ${placeId}`);
293
+
294
+ if (!window.google?.maps?.places) {
295
+ throw new Error('Google Maps Places library not available');
296
+ }
297
+
298
+ // Create a dummy element for PlacesService (it needs an element but we don't use it)
299
+ const dummyElement = document.createElement('div');
300
+ const placesService = new window.google.maps.places.PlacesService(dummyElement);
301
+
302
+ // Call getDetails
303
+ return new Promise<{
304
+ place_id: string;
305
+ formatted_address?: string;
306
+ address_components?: Array<{
307
+ long_name: string;
308
+ short_name: string;
309
+ types: string[];
310
+ }>;
311
+ geometry?: {
312
+ location?: {
313
+ lat: () => number;
314
+ lng: () => number;
315
+ };
316
+ };
317
+ }>((resolve, reject) => {
318
+ placesService.getDetails(
319
+ {
320
+ placeId: placeId,
321
+ fields: ['place_id', 'formatted_address', 'address_components', 'geometry'],
322
+ },
323
+ (place, status) => {
324
+ if (status === 'OK' && place) {
325
+ log.debug('Place details fetched successfully');
326
+ resolve(place as any);
327
+ } else if (status === 'NOT_FOUND') {
328
+ log.error('Place not found:', placeId);
329
+ reject(new Error('Place not found'));
330
+ } else {
331
+ const errorMsg = `Failed to fetch place details: ${status}`;
332
+ log.error('Place details fetch failed:', errorMsg);
333
+ reject(new Error(errorMsg));
334
+ }
335
+ }
336
+ );
337
+ });
338
+ } catch (error) {
339
+ if (error instanceof Error) {
340
+ log.error('Place details fetch failed:', error.message);
341
+ throw error;
342
+ }
343
+ log.error('Place details fetch failed: Unknown error');
344
+ throw new Error('Failed to fetch place details');
345
+ }
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Parse address components from Google Places API response
351
+ *
352
+ * @param components - Array of address components from Google API
353
+ * @returns Parsed address components
354
+ */
355
+ export function parseAddressComponents(
356
+ components: Array<{
357
+ long_name: string;
358
+ short_name: string;
359
+ types: string[];
360
+ }> | undefined
361
+ ): {
362
+ street_number: string | null;
363
+ route: string | null;
364
+ suburb: string | null;
365
+ state: string | null;
366
+ postcode: string | null;
367
+ country: string | null;
368
+ } {
369
+ if (!components || components.length === 0) {
370
+ return {
371
+ street_number: null,
372
+ route: null,
373
+ suburb: null,
374
+ state: null,
375
+ postcode: null,
376
+ country: null,
377
+ };
378
+ }
379
+
380
+ const result = {
381
+ street_number: null as string | null,
382
+ route: null as string | null,
383
+ suburb: null as string | null,
384
+ state: null as string | null,
385
+ postcode: null as string | null,
386
+ country: null as string | null,
387
+ };
388
+
389
+ for (const component of components) {
390
+ const types = component.types || [];
391
+
392
+ if (types.includes('street_number')) {
393
+ result.street_number = component.long_name;
394
+ } else if (types.includes('route')) {
395
+ result.route = component.long_name;
396
+ } else if (types.includes('locality') || types.includes('sublocality')) {
397
+ result.suburb = component.long_name;
398
+ } else if (types.includes('administrative_area_level_1')) {
399
+ result.state = component.short_name;
400
+ } else if (types.includes('postal_code')) {
401
+ result.postcode = component.long_name;
402
+ } else if (types.includes('country')) {
403
+ result.country = component.short_name;
404
+ }
405
+ }
406
+
407
+ return result;
408
+ }
409
+
410
+ /**
411
+ * Create parsed address from Google Places API place result
412
+ *
413
+ * @param place - Place result from Google Places Details API
414
+ * @returns Parsed address matching pace_address table structure
415
+ */
416
+ export function createAddressFromPlaceResult(
417
+ place: {
418
+ place_id: string;
419
+ formatted_address?: string;
420
+ address_components?: Array<{
421
+ long_name: string;
422
+ short_name: string;
423
+ types: string[];
424
+ }>;
425
+ geometry?: {
426
+ location?: {
427
+ lat: () => number;
428
+ lng: () => number;
429
+ };
430
+ };
431
+ }
432
+ ): ParsedAddress {
433
+ const components = parseAddressComponents(place.address_components || []);
434
+
435
+ return {
436
+ place_id: place.place_id,
437
+ full_address: place.formatted_address || null,
438
+ street_number: components.street_number,
439
+ route: components.route,
440
+ suburb: components.suburb,
441
+ state: components.state,
442
+ postcode: components.postcode,
443
+ country: components.country,
444
+ lat: place.geometry?.location?.lat() ?? null,
445
+ lng: place.geometry?.location?.lng() ?? null,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Get full address details by place_id
451
+ * Useful for retrieving address from stored place_id without autocomplete search
452
+ *
453
+ * @param placeId - Google Place ID
454
+ * @param apiKey - Google Places API key
455
+ * @returns Promise resolving to parsed address
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * const address = await getAddressByPlaceId('ChIJ...', 'your-api-key');
460
+ * // Returns: { place_id: 'ChIJ...', full_address: '...', ... }
461
+ * ```
462
+ */
463
+ export async function getAddressByPlaceId(
464
+ placeId: string,
465
+ apiKey: string
466
+ ): Promise<ParsedAddress | null> {
467
+ try {
468
+ const place = await fetchPlaceDetails(placeId, apiKey);
469
+ return createAddressFromPlaceResult(place);
470
+ } catch (error) {
471
+ log.error('Failed to get address by place_id:', error);
472
+ return null;
473
+ }
474
+ }
475
+
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @file Google Places API Utilities Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export {
9
+ fetchPlaceAutocomplete,
10
+ fetchPlaceDetails,
11
+ parseAddressComponents,
12
+ createAddressFromPlaceResult,
13
+ getAddressByPlaceId,
14
+ } from './googlePlacesUtils';
15
+
16
+ export {
17
+ loadGoogleMapsScript,
18
+ isGoogleMapsLoaded,
19
+ } from './loadGoogleMapsScript';
20
+
21
+ export type {
22
+ GooglePlaceAutocompletePrediction,
23
+ ParsedAddress,
24
+ AutocompleteOptions,
25
+ } from './types';
26
+
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @file Google Maps Script Loader
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces
5
+ * @since 0.1.0
6
+ *
7
+ * Utility to dynamically load the Google Maps JavaScript API.
8
+ * This is required because the REST API doesn't support CORS from browsers.
9
+ */
10
+
11
+ // Type definitions for Google Maps (minimal, just what we need)
12
+ declare global {
13
+ interface Window {
14
+ google?: {
15
+ maps: {
16
+ places: {
17
+ // New AutocompleteSuggestion API (recommended)
18
+ AutocompleteSuggestion: {
19
+ fetchAutocompleteSuggestions: (
20
+ request: {
21
+ input: string;
22
+ includedRegionCodes?: string[];
23
+ locationBias?: {
24
+ circle?: {
25
+ center: { latitude: number; longitude: number };
26
+ radius: number;
27
+ };
28
+ };
29
+ includedPrimaryTypes?: string[];
30
+ languageCode?: string;
31
+ }
32
+ ) => Promise<{
33
+ suggestions: Array<{
34
+ placePrediction?: {
35
+ placeId: string;
36
+ text: {
37
+ text: string;
38
+ matches: Array<{
39
+ endOffset: number;
40
+ startOffset: number;
41
+ }>;
42
+ };
43
+ structuredFormat?: {
44
+ mainText: { text: string };
45
+ secondaryText: { text: string };
46
+ };
47
+ };
48
+ }>;
49
+ }>;
50
+ };
51
+ // Legacy AutocompleteService (deprecated but still works)
52
+ AutocompleteService: new () => {
53
+ getPlacePredictions: (
54
+ request: {
55
+ input: string;
56
+ componentRestrictions?: { country: string | string[] };
57
+ location?: { lat: () => number; lng: () => number };
58
+ radius?: number;
59
+ types?: string[];
60
+ language?: string;
61
+ },
62
+ callback: (
63
+ predictions: Array<{
64
+ description: string;
65
+ place_id: string;
66
+ structured_formatting: {
67
+ main_text: string;
68
+ secondary_text: string;
69
+ };
70
+ }> | null,
71
+ status: string
72
+ ) => void
73
+ ) => void;
74
+ };
75
+ PlacesService: new (element: HTMLElement) => {
76
+ getDetails: (
77
+ request: { placeId: string; fields?: string[] },
78
+ callback: (
79
+ place: {
80
+ place_id: string;
81
+ formatted_address?: string;
82
+ address_components?: Array<{
83
+ long_name: string;
84
+ short_name: string;
85
+ types: string[];
86
+ }>;
87
+ geometry?: {
88
+ location?: {
89
+ lat: () => number;
90
+ lng: () => number;
91
+ };
92
+ };
93
+ } | null,
94
+ status: string
95
+ ) => void
96
+ ) => void;
97
+ };
98
+ PlacesServiceStatus: {
99
+ OK: string;
100
+ ZERO_RESULTS: string;
101
+ NOT_FOUND: string;
102
+ REQUEST_DENIED: string;
103
+ INVALID_REQUEST: string;
104
+ OVER_QUERY_LIMIT: string;
105
+ };
106
+ };
107
+ LatLng: new (lat: number, lng: number) => { lat: () => number; lng: () => number };
108
+ };
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Load Google Maps JavaScript API script
115
+ * @param apiKey - Google Places API key
116
+ * @param libraries - Comma-separated list of libraries to load (default: 'places')
117
+ * @returns Promise that resolves when the script is loaded
118
+ */
119
+ export function loadGoogleMapsScript(
120
+ apiKey: string,
121
+ libraries: string = 'places'
122
+ ): Promise<void> {
123
+ return new Promise((resolve, reject) => {
124
+ // Check if script is already loaded
125
+ if (window.google && window.google.maps && window.google.maps.places) {
126
+ resolve();
127
+ return;
128
+ }
129
+
130
+ // Check if script is already being loaded
131
+ const existingScript = document.querySelector(
132
+ `script[src*="maps.googleapis.com/maps/api/js"]`
133
+ );
134
+ if (existingScript) {
135
+ // Wait for existing script to load
136
+ existingScript.addEventListener('load', () => {
137
+ // Wait for the library to initialize with multiple retries
138
+ let attempts = 0;
139
+ const maxAttempts = 20; // 2 seconds total
140
+
141
+ const checkPlaces = () => {
142
+ if (window.google?.maps?.places) {
143
+ resolve();
144
+ } else if (attempts < maxAttempts) {
145
+ attempts++;
146
+ setTimeout(checkPlaces, 100);
147
+ } else {
148
+ reject(new Error('Google Maps script loaded but Places library not available. Make sure the Places API is enabled in your Google Cloud Console.'));
149
+ }
150
+ };
151
+
152
+ checkPlaces();
153
+ });
154
+ existingScript.addEventListener('error', () => {
155
+ reject(new Error('Failed to load Google Maps script'));
156
+ });
157
+ return;
158
+ }
159
+
160
+ // Create and load script
161
+ const script = document.createElement('script');
162
+ script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=${libraries}&loading=async`;
163
+ script.async = true;
164
+ script.defer = true;
165
+
166
+ script.onload = () => {
167
+ // Wait for the library to initialize with multiple retries
168
+ let attempts = 0;
169
+ const maxAttempts = 20; // 2 seconds total (20 * 100ms)
170
+
171
+ const checkPlaces = () => {
172
+ if (window.google?.maps?.places) {
173
+ resolve();
174
+ } else if (attempts < maxAttempts) {
175
+ attempts++;
176
+ setTimeout(checkPlaces, 100);
177
+ } else {
178
+ // Check if google.maps exists but places doesn't
179
+ if (window.google?.maps && !window.google.maps.places) {
180
+ reject(new Error('Google Maps loaded but Places library not available. Make sure the Places API is enabled in your Google Cloud Console and the "places" library is included in the script URL.'));
181
+ } else if (!window.google) {
182
+ reject(new Error('Google Maps script loaded but google object not available. Check your API key and network connection.'));
183
+ } else {
184
+ reject(new Error('Google Maps script loaded but Places library not available after multiple attempts. Make sure the Places API is enabled in your Google Cloud Console.'));
185
+ }
186
+ }
187
+ };
188
+
189
+ // Start checking immediately, then retry if needed
190
+ checkPlaces();
191
+ };
192
+
193
+ script.onerror = () => {
194
+ reject(new Error('Failed to load Google Maps script'));
195
+ };
196
+
197
+ document.head.appendChild(script);
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Check if Google Maps is already loaded
203
+ */
204
+ export function isGoogleMapsLoaded(): boolean {
205
+ return !!(window.google && window.google.maps && window.google.maps.places);
206
+ }
207
+