@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,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
+
@@ -87,7 +87,6 @@ export interface FileReferenceService {
87
87
 
88
88
  export interface StorageUploadOptions {
89
89
  orgId: string;
90
- category: FileCategory;
91
90
  isPublic?: boolean;
92
91
  customPath?: string;
93
92
  }
@@ -449,9 +449,22 @@ describe('[service] FileReferenceServiceImpl', () => {
449
449
  });
450
450
 
451
451
  it('lists all file references for record', async () => {
452
- const mockFiles = [mockFileReference];
453
- mockSupabase.rpc.mockResolvedValue({ data: [{ id: 'file-ref-123' }], error: null });
454
- (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
+ });
455
468
 
456
469
  const result = await service.listFileReferences(
457
470
  'test_table',
@@ -459,7 +472,21 @@ describe('[service] FileReferenceServiceImpl', () => {
459
472
  'test-org-123'
460
473
  );
461
474
 
462
- 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);
463
490
  });
464
491
  });
465
492
 
@@ -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,6 +48,9 @@ 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
56
  // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename}
@@ -117,7 +120,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
117
120
  // Get the created file reference
118
121
  const { data: fileRef, error: fetchError } = await this.supabase
119
122
  .from('file_references')
120
- .select('*')
123
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
121
124
  .eq('id', data)
122
125
  .single();
123
126
 
@@ -146,7 +149,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
146
149
  try {
147
150
  const { data, error } = await this.supabase
148
151
  .from('file_references')
149
- .select('*')
152
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
150
153
  .eq('table_name', table_name)
151
154
  .eq('record_id', record_id)
152
155
  .eq('organisation_id', organisation_id)
@@ -294,24 +297,50 @@ export class FileReferenceServiceImpl implements FileReferenceService {
294
297
  throw new Error(`Failed to list file references: ${error.message}`);
295
298
  }
296
299
 
297
- // 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
298
303
  if (!data || data.length === 0) {
299
304
  return [];
300
305
  }
301
306
 
302
- // Fetch full file reference data for each ID
303
- // RPC returns array of objects with at least an 'id' property
304
- const ids = data.map((item: { id: string }) => item.id);
305
- const { data: fullData, error: fetchError } = await this.supabase
306
- .from('file_references')
307
- .select('*')
308
- .in('id', ids);
309
-
310
- if (fetchError) {
311
- 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;
312
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
+ });
313
342
 
314
- return (fullData || []) as FileReference[];
343
+ return fileReferences;
315
344
  } catch (error) {
316
345
  log.error('Error listing file references:', error);
317
346
  throw error;