@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,87 @@
1
+ /**
2
+ * Request Deduplication for RBAC Permission Checks
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/RequestDeduplication
5
+ * @since 2.0.0
6
+ *
7
+ * This module provides request deduplication to prevent multiple identical
8
+ * permission checks from being made simultaneously. When multiple components
9
+ * request the same permission at the same time, they share the same promise.
10
+ */
11
+
12
+ import { PermissionCheck } from './types';
13
+ import { RBACCache } from './cache';
14
+
15
+ /**
16
+ * Map of in-flight permission check requests
17
+ * Key: cache key string, Value: Promise<boolean>
18
+ */
19
+ const inFlightRequests = new Map<string, Promise<boolean>>();
20
+
21
+ /**
22
+ * Generate a deduplication key from permission check input
23
+ *
24
+ * @param input - Permission check input
25
+ * @returns Deduplication key string
26
+ */
27
+ function generateDeduplicationKey(input: PermissionCheck): string {
28
+ return RBACCache.generatePermissionKey({
29
+ userId: input.userId,
30
+ organisationId: input.scope.organisationId!,
31
+ eventId: input.scope.eventId,
32
+ appId: input.scope.appId,
33
+ permission: input.permission,
34
+ pageId: input.pageId,
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Get or create a deduplicated permission check request
40
+ *
41
+ * If a request for the same permission is already in-flight, returns the existing promise.
42
+ * Otherwise, creates a new request and tracks it.
43
+ *
44
+ * @param input - Permission check input
45
+ * @param checkFn - Function to perform the actual permission check
46
+ * @returns Promise resolving to permission result
47
+ */
48
+ export async function getOrCreateRequest(
49
+ input: PermissionCheck,
50
+ checkFn: (input: PermissionCheck) => Promise<boolean>
51
+ ): Promise<boolean> {
52
+ const key = generateDeduplicationKey(input);
53
+
54
+ // Check if request is already in-flight
55
+ const existingRequest = inFlightRequests.get(key);
56
+ if (existingRequest) {
57
+ return existingRequest;
58
+ }
59
+
60
+ // Create new request
61
+ const requestPromise = checkFn(input).finally(() => {
62
+ // Clean up when request completes (success or failure)
63
+ inFlightRequests.delete(key);
64
+ });
65
+
66
+ // Track the request
67
+ inFlightRequests.set(key, requestPromise);
68
+
69
+ return requestPromise;
70
+ }
71
+
72
+ /**
73
+ * Clear all in-flight requests (useful for testing or cleanup)
74
+ */
75
+ export function clearInFlightRequests(): void {
76
+ inFlightRequests.clear();
77
+ }
78
+
79
+ /**
80
+ * Get count of in-flight requests (useful for monitoring)
81
+ *
82
+ * @returns Number of in-flight requests
83
+ */
84
+ export function getInFlightRequestCount(): number {
85
+ return inFlightRequests.size;
86
+ }
87
+
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Deep equality check utility for RBAC
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Utils/DeepEqual
5
+ * @since 2.0.0
6
+ *
7
+ * Provides deep equality checking for scope objects and other RBAC data structures.
8
+ */
9
+
10
+ import { Scope } from '../types';
11
+
12
+ /**
13
+ * Deep equality check for two values
14
+ *
15
+ * @param a - First value
16
+ * @param b - Second value
17
+ * @returns True if values are deeply equal
18
+ */
19
+ export function deepEqual(a: unknown, b: unknown): boolean {
20
+ if (a === b) {
21
+ return true;
22
+ }
23
+
24
+ if (a == null || b == null) {
25
+ return a === b;
26
+ }
27
+
28
+ if (typeof a !== typeof b) {
29
+ return false;
30
+ }
31
+
32
+ if (typeof a !== 'object') {
33
+ return false;
34
+ }
35
+
36
+ if (Array.isArray(a) !== Array.isArray(b)) {
37
+ return false;
38
+ }
39
+
40
+ if (Array.isArray(a) && Array.isArray(b)) {
41
+ if (a.length !== b.length) {
42
+ return false;
43
+ }
44
+ for (let i = 0; i < a.length; i++) {
45
+ if (!deepEqual(a[i], b[i])) {
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+
52
+ const keysA = Object.keys(a as Record<string, unknown>);
53
+ const keysB = Object.keys(b as Record<string, unknown>);
54
+
55
+ if (keysA.length !== keysB.length) {
56
+ return false;
57
+ }
58
+
59
+ for (const key of keysA) {
60
+ if (!keysB.includes(key)) {
61
+ return false;
62
+ }
63
+ if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ return true;
69
+ }
70
+
71
+ /**
72
+ * Deep equality check for Scope objects
73
+ *
74
+ * @param a - First scope
75
+ * @param b - Second scope
76
+ * @returns True if scopes are deeply equal
77
+ */
78
+ export function scopeEqual(a: Scope | null | undefined, b: Scope | null | undefined): boolean {
79
+ if (a === b) {
80
+ return true;
81
+ }
82
+
83
+ if (a == null || b == null) {
84
+ return a === b;
85
+ }
86
+
87
+ return (
88
+ a.organisationId === b.organisationId &&
89
+ a.eventId === b.eventId &&
90
+ a.appId === b.appId
91
+ );
92
+ }
93
+
@@ -240,14 +240,14 @@
240
240
  /* Custom utility styles go here */
241
241
 
242
242
 
243
- /* Hide spinner arrows on number inputs in DataTable */
244
- .datatable-number-no-spinners::-webkit-inner-spin-button,
245
- .datatable-number-no-spinners::-webkit-outer-spin-button {
243
+ /* Hide spinner arrows on all number inputs (modern UX convention) */
244
+ input[type="number"]::-webkit-inner-spin-button,
245
+ input[type="number"]::-webkit-outer-spin-button {
246
246
  -webkit-appearance: none;
247
247
  margin: 0;
248
248
  }
249
-
250
- .datatable-number-no-spinners {
249
+
250
+ input[type="number"] {
251
251
  -moz-appearance: textfield;
252
252
  }
253
253
  }
@@ -3320,13 +3320,13 @@ export type Database = {
3320
3320
  },
3321
3321
  ]
3322
3322
  }
3323
- pace_id_documents: {
3323
+ pace_identification: {
3324
3324
  Row: {
3325
3325
  created_at: string | null
3326
3326
  document_number: string | null
3327
- document_type: string
3328
3327
  expiry_date: string | null
3329
3328
  id: string
3329
+ identification_type_id: number | null
3330
3330
  issue_city: string | null
3331
3331
  issue_country: string | null
3332
3332
  issue_date: string | null
@@ -3339,9 +3339,9 @@ export type Database = {
3339
3339
  Insert: {
3340
3340
  created_at?: string | null
3341
3341
  document_number?: string | null
3342
- document_type: string
3343
3342
  expiry_date?: string | null
3344
3343
  id?: string
3344
+ identification_type_id?: number | null
3345
3345
  issue_city?: string | null
3346
3346
  issue_country?: string | null
3347
3347
  issue_date?: string | null
@@ -3354,9 +3354,9 @@ export type Database = {
3354
3354
  Update: {
3355
3355
  created_at?: string | null
3356
3356
  document_number?: string | null
3357
- document_type?: string
3358
3357
  expiry_date?: string | null
3359
3358
  id?: string
3359
+ identification_type_id?: number | null
3360
3360
  issue_city?: string | null
3361
3361
  issue_country?: string | null
3362
3362
  issue_date?: string | null
@@ -3368,14 +3368,21 @@ export type Database = {
3368
3368
  }
3369
3369
  Relationships: [
3370
3370
  {
3371
- foreignKeyName: "fk_pace_id_documents_organisation_id"
3371
+ foreignKeyName: "fk_pace_identification_organisation_id"
3372
3372
  columns: ["organisation_id"]
3373
3373
  isOneToOne: false
3374
3374
  referencedRelation: "organisations"
3375
3375
  referencedColumns: ["id"]
3376
3376
  },
3377
3377
  {
3378
- foreignKeyName: "identification_documents_member_id_fkey"
3378
+ foreignKeyName: "fk_pace_identification_type_id"
3379
+ columns: ["identification_type_id"]
3380
+ isOneToOne: false
3381
+ referencedRelation: "pace_identification_type"
3382
+ referencedColumns: ["id"]
3383
+ },
3384
+ {
3385
+ foreignKeyName: "pace_identification_member_id_fkey"
3379
3386
  columns: ["member_id"]
3380
3387
  isOneToOne: false
3381
3388
  referencedRelation: "pace_member"
@@ -3383,6 +3390,53 @@ export type Database = {
3383
3390
  },
3384
3391
  ]
3385
3392
  }
3393
+ pace_identification_type: {
3394
+ Row: {
3395
+ created_at: string | null
3396
+ created_by: string | null
3397
+ description: string | null
3398
+ id: number
3399
+ is_active: boolean | null
3400
+ name: string
3401
+ organisation_id: string
3402
+ sort_order: number | null
3403
+ updated_at: string | null
3404
+ updated_by: string | null
3405
+ }
3406
+ Insert: {
3407
+ created_at?: string | null
3408
+ created_by?: string | null
3409
+ description?: string | null
3410
+ id?: never
3411
+ is_active?: boolean | null
3412
+ name: string
3413
+ organisation_id: string
3414
+ sort_order?: number | null
3415
+ updated_at?: string | null
3416
+ updated_by?: string | null
3417
+ }
3418
+ Update: {
3419
+ created_at?: string | null
3420
+ created_by?: string | null
3421
+ description?: string | null
3422
+ id?: never
3423
+ is_active?: boolean | null
3424
+ name?: string
3425
+ organisation_id?: string
3426
+ sort_order?: number | null
3427
+ updated_at?: string | null
3428
+ updated_by?: string | null
3429
+ }
3430
+ Relationships: [
3431
+ {
3432
+ foreignKeyName: "pace_identification_type_organisation_id_fkey"
3433
+ columns: ["organisation_id"]
3434
+ isOneToOne: false
3435
+ referencedRelation: "organisations"
3436
+ referencedColumns: ["id"]
3437
+ },
3438
+ ]
3439
+ }
3386
3440
  pace_member: {
3387
3441
  Row: {
3388
3442
  address_id: string | null
@@ -3858,7 +3912,7 @@ export type Database = {
3858
3912
  },
3859
3913
  ]
3860
3914
  }
3861
- pace_qualifications: {
3915
+ pace_qualification: {
3862
3916
  Row: {
3863
3917
  created_at: string | null
3864
3918
  credential_id: string | null
@@ -3900,14 +3954,14 @@ export type Database = {
3900
3954
  }
3901
3955
  Relationships: [
3902
3956
  {
3903
- foreignKeyName: "fk_pace_qualifications_organisation_id"
3957
+ foreignKeyName: "fk_pace_qualification_organisation_id"
3904
3958
  columns: ["organisation_id"]
3905
3959
  isOneToOne: false
3906
3960
  referencedRelation: "organisations"
3907
3961
  referencedColumns: ["id"]
3908
3962
  },
3909
3963
  {
3910
- foreignKeyName: "qualifications_member_id_fkey"
3964
+ foreignKeyName: "pace_qualification_member_id_fkey"
3911
3965
  columns: ["member_id"]
3912
3966
  isOneToOne: false
3913
3967
  referencedRelation: "pace_member"
@@ -55,6 +55,7 @@ export enum FileCategory {
55
55
  * Options for uploading a file with a file reference
56
56
  * @property pageContext - The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
57
57
  * Used for context-aware permission checks. Required to check appropriate page-level permissions.
58
+ * @property event_id - Optional event ID for event-scoped permission checks. Required for event-based apps.
58
59
  */
59
60
  export interface FileUploadOptions {
60
61
  table_name: string;
@@ -62,7 +63,9 @@ export interface FileUploadOptions {
62
63
  organisation_id: string;
63
64
  app_id: AppId;
64
65
  category: FileCategory;
66
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
65
67
  pageContext: string;
68
+ event_id?: string;
66
69
  is_public?: boolean;
67
70
  custom_metadata?: Record<string, unknown>;
68
71
  }
@@ -84,7 +87,6 @@ export interface FileReferenceService {
84
87
 
85
88
  export interface StorageUploadOptions {
86
89
  orgId: string;
87
- category: FileCategory;
88
90
  isPublic?: boolean;
89
91
  customPath?: string;
90
92
  }
@@ -50,6 +50,7 @@ const mockFileUploadOptions = {
50
50
  organisation_id: 'test-org-123',
51
51
  app_id: 'test-app-123',
52
52
  category: FileCategory.GENERAL_DOCUMENTS,
53
+ folder: 'documents',
53
54
  pageContext: 'configuration',
54
55
  is_public: false
55
56
  };
@@ -105,7 +106,7 @@ describe('[service] FileReferenceServiceImpl', () => {
105
106
  testFile,
106
107
  expect.objectContaining({
107
108
  orgId: mockFileUploadOptions.organisation_id,
108
- customPath: mockFileUploadOptions.category,
109
+ customPath: mockFileUploadOptions.folder,
109
110
  isPublic: mockFileUploadOptions.is_public
110
111
  })
111
112
  );
@@ -448,9 +449,22 @@ describe('[service] FileReferenceServiceImpl', () => {
448
449
  });
449
450
 
450
451
  it('lists all file references for record', async () => {
451
- const mockFiles = [mockFileReference];
452
- mockSupabase.rpc.mockResolvedValue({ data: [{ id: 'file-ref-123' }], error: null });
453
- (mockSupabase.from() as any).in.mockResolvedValue({ data: mockFiles, error: null });
452
+ // Mock RPC to return full data structure (as per new implementation)
453
+ // RPC returns: id, file_path, file_metadata, is_public, created_at
454
+ // The code constructs FileReference objects from this RPC response
455
+ mockSupabase.rpc.mockResolvedValue({
456
+ data: [{
457
+ id: 'file-ref-123',
458
+ file_path: mockFileReference.file_path,
459
+ file_metadata: {
460
+ ...mockFileReference.file_metadata,
461
+ app_id: mockFileReference.app_id // Include app_id in metadata for proper construction
462
+ },
463
+ is_public: mockFileReference.is_public,
464
+ created_at: mockFileReference.created_at
465
+ }],
466
+ error: null
467
+ });
454
468
 
455
469
  const result = await service.listFileReferences(
456
470
  'test_table',
@@ -458,7 +472,21 @@ describe('[service] FileReferenceServiceImpl', () => {
458
472
  'test-org-123'
459
473
  );
460
474
 
461
- expect(result).toEqual(mockFiles);
475
+ // Verify result has correct structure (constructed from RPC response)
476
+ expect(result).toHaveLength(1);
477
+ expect(result[0].id).toBe('file-ref-123');
478
+ expect(result[0].table_name).toBe('test_table');
479
+ expect(result[0].record_id).toBe('test-record-123');
480
+ expect(result[0].organisation_id).toBe('test-org-123');
481
+ expect(result[0].file_path).toBe(mockFileReference.file_path);
482
+ // file_metadata: code extracts fileName and fileType from file_path, then spreads item.file_metadata
483
+ // Since item.file_metadata has fileName: 'test-document.pdf' and fileType: 'application/pdf',
484
+ // the spread overwrites the extracted values
485
+ // So we expect the metadata's values, not the extracted ones
486
+ expect(result[0].file_metadata.fileName).toBe('test-document.pdf');
487
+ expect(result[0].file_metadata.fileType).toBe('application/pdf');
488
+ expect(result[0].is_public).toBe(mockFileReference.is_public);
489
+ expect(result[0].app_id).toBe(mockFileReference.app_id);
462
490
  });
463
491
  });
464
492
 
@@ -757,22 +785,75 @@ describe('[utility] createFileReferenceService', () => {
757
785
  describe('[utility] uploadFileWithReference', () => {
758
786
  it('uploads file and creates reference successfully', async () => {
759
787
  const testFile = createTestFile();
788
+
789
+ // Mock successful RPC call that returns file reference ID
790
+ mockSupabase.rpc.mockResolvedValue({
791
+ data: 'file-ref-123',
792
+ error: null
793
+ });
794
+
795
+ // Mock successful file reference fetch - using the same pattern as other tests
796
+ (mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
797
+ data: mockFileReference,
798
+ error: null
799
+ });
800
+
801
+ // Mock storage signed URL generation
802
+ mockSupabase.storage = {
803
+ from: vi.fn().mockReturnValue({
804
+ createSignedUrl: vi.fn().mockResolvedValue({
805
+ data: { signedUrl: 'https://example.com/signed-url' },
806
+ error: null
807
+ })
808
+ })
809
+ } as any;
810
+
760
811
  const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
761
812
  expect(result).toHaveProperty('file_reference');
762
813
  expect('file_url' in result).toBe(true);
763
814
  });
764
815
 
765
816
  it('handles upload failures gracefully', async () => {
766
- // This path is covered by service tests; here just ensure function returns structured result on success path
817
+ // Mock upload failure
818
+ mockUploadFile.mockResolvedValue({
819
+ success: false,
820
+ error: 'Upload failed'
821
+ });
822
+
767
823
  const testFile = createTestFile();
768
- const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
769
- expect(result).toHaveProperty('file_reference');
824
+ await expect(uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile))
825
+ .rejects.toThrow('Upload failed');
770
826
  });
771
827
 
772
828
  it('handles URL generation failures gracefully', async () => {
773
829
  const testFile = createTestFile();
830
+
831
+ // Mock successful RPC call
832
+ mockSupabase.rpc.mockResolvedValue({
833
+ data: 'file-ref-123',
834
+ error: null
835
+ });
836
+
837
+ // Mock successful file reference fetch
838
+ (mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
839
+ data: mockFileReference,
840
+ error: null
841
+ });
842
+
843
+ // Mock storage to fail on signed URL generation
844
+ mockSupabase.storage = {
845
+ from: vi.fn().mockReturnValue({
846
+ createSignedUrl: vi.fn().mockResolvedValue({
847
+ data: null,
848
+ error: { message: 'URL generation failed' }
849
+ })
850
+ })
851
+ } as any;
852
+
774
853
  const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
775
854
  expect(result).toHaveProperty('file_reference');
855
+ // URL should be empty string when generation fails
856
+ expect(result.file_url).toBe('');
776
857
  });
777
858
 
778
859
  it('validates required parameters', async () => {
@@ -26,7 +26,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
26
26
  *
27
27
  * Storage Flow:
28
28
  * 1. Upload file to storage bucket first (files or public-files based on is_public flag)
29
- * - Path format: {orgId}/{category}/{timestamp-uuid-filename}
29
+ * - Path format: {orgId}/{folder}/{timestamp-uuid-filename}
30
30
  * - Bucket selection: 'files' (private) or 'public-files' (public)
31
31
  * 2. Extract file metadata (dimensions, hash, etc.)
32
32
  * 3. Set organisation context for RLS policies
@@ -48,15 +48,18 @@ export class FileReferenceServiceImpl implements FileReferenceService {
48
48
  if (!options.record_id) {
49
49
  throw new Error('record_id is required for file upload');
50
50
  }
51
+ if (!options.folder) {
52
+ throw new Error('folder is required for file upload. The folder prop determines the storage path.');
53
+ }
51
54
 
52
55
  // Step 1: Upload file to storage bucket first
53
- // This generates a unique path: {orgId}/{category}/{timestamp-uuid-filename}
56
+ // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename}
54
57
  // Bucket is automatically selected based on is_public flag
55
58
  const uploadResult = await uploadFile(this.supabase, file, {
56
59
  appName: 'file-reference',
57
60
  orgId: options.organisation_id,
58
61
  isPublic: options.is_public || false,
59
- customPath: options.category // Use category as the custom path segment
62
+ customPath: options.folder // Use folder prop as the custom path segment
60
63
  });
61
64
  if (!uploadResult.success) {
62
65
  throw new Error(`Failed to upload file: ${uploadResult.error}`);
@@ -89,6 +92,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
89
92
  p_organisation_id: options.organisation_id,
90
93
  p_app_id: options.app_id,
91
94
  p_page_context: options.pageContext,
95
+ p_event_id: options.event_id || null, // Pass event_id for event-based apps
92
96
  p_file_metadata: {
93
97
  fileName: file.name,
94
98
  fileType: file.type,
@@ -106,14 +110,23 @@ export class FileReferenceServiceImpl implements FileReferenceService {
106
110
  throw new Error(`Failed to create file reference: ${error.message}`);
107
111
  }
108
112
 
113
+ // Check if RPC returned null (permission denied or other failure)
114
+ if (!data || data === null) {
115
+ // Clean up the uploaded file since DB insert failed
116
+ await deleteFile(this.supabase, filePath, options.is_public || false);
117
+ throw new Error(`File upload denied: insufficient permissions. You need 'create:page.${options.pageContext}' or 'update:page.${options.pageContext}' permission for the '${options.pageContext}' page. Make sure the page exists in rbac_app_pages table.`);
118
+ }
119
+
109
120
  // Get the created file reference
110
121
  const { data: fileRef, error: fetchError } = await this.supabase
111
122
  .from('file_references')
112
- .select('*')
123
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
113
124
  .eq('id', data)
114
125
  .single();
115
126
 
116
127
  if (fetchError || !fileRef) {
128
+ // Clean up uploaded file if we can't fetch the reference
129
+ await deleteFile(this.supabase, filePath, options.is_public || false);
117
130
  throw new Error(`Failed to fetch created file reference: ${fetchError?.message}`);
118
131
  }
119
132
 
@@ -136,7 +149,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
136
149
  try {
137
150
  const { data, error } = await this.supabase
138
151
  .from('file_references')
139
- .select('*')
152
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
140
153
  .eq('table_name', table_name)
141
154
  .eq('record_id', record_id)
142
155
  .eq('organisation_id', organisation_id)
@@ -284,24 +297,50 @@ export class FileReferenceServiceImpl implements FileReferenceService {
284
297
  throw new Error(`Failed to list file references: ${error.message}`);
285
298
  }
286
299
 
287
- // RPC returns partial data, need to fetch full file references
300
+ // RPC returns: id, file_path, file_metadata, is_public, created_at
301
+ // We can construct FileReference objects directly from RPC response + function parameters
302
+ // This avoids a second query and reduces network requests
288
303
  if (!data || data.length === 0) {
289
304
  return [];
290
305
  }
291
306
 
292
- // Fetch full file reference data for each ID
293
- // RPC returns array of objects with at least an 'id' property
294
- const ids = data.map((item: { id: string }) => item.id);
295
- const { data: fullData, error: fetchError } = await this.supabase
296
- .from('file_references')
297
- .select('*')
298
- .in('id', ids);
299
-
300
- if (fetchError) {
301
- throw new Error(`Failed to fetch file references: ${fetchError.message}`);
307
+ // Construct FileReference objects from RPC response
308
+ // This avoids RLS issues with direct queries - the RPC already validated permissions
309
+ interface RpcFileItem {
310
+ id: string;
311
+ file_path: string;
312
+ file_metadata: { app_id?: string; [key: string]: unknown };
313
+ is_public?: boolean;
314
+ created_at?: string;
302
315
  }
316
+ const fileReferences: FileReference[] = data
317
+ .filter((item: RpcFileItem) => item.id && item.file_path && item.file_metadata)
318
+ .map((item: RpcFileItem) => {
319
+ // Extract file name and type from file_path
320
+ const fileName = item.file_path.split('/').pop() || 'unknown';
321
+ const fileType = fileName.split('.').pop() || 'unknown';
322
+
323
+ // Construct complete FileReference from RPC response + function parameters
324
+ const fileRef: FileReference = {
325
+ id: item.id,
326
+ table_name: table_name,
327
+ record_id: record_id,
328
+ file_path: item.file_path,
329
+ file_metadata: {
330
+ fileName,
331
+ fileType,
332
+ ...(item.file_metadata || {}),
333
+ } as FileMetadata,
334
+ organisation_id: organisation_id,
335
+ app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
336
+ is_public: item.is_public ?? false,
337
+ created_at: item.created_at || new Date().toISOString(),
338
+ updated_at: item.created_at || new Date().toISOString() // RPC doesn't return updated_at, use created_at
339
+ };
340
+ return fileRef;
341
+ });
303
342
 
304
- return (fullData || []) as FileReference[];
343
+ return fileReferences;
305
344
  } catch (error) {
306
345
  log.error('Error listing file references:', error);
307
346
  throw error;