@jmruthers/pace-core 0.5.184 → 0.5.186

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 (319) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -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"
@@ -51,12 +51,21 @@ export enum FileCategory {
51
51
  TRAC_TRANSPORT = 'trac_transport'
52
52
  }
53
53
 
54
+ /**
55
+ * Options for uploading a file with a file reference
56
+ * @property pageContext - The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
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.
59
+ */
54
60
  export interface FileUploadOptions {
55
61
  table_name: string;
56
62
  record_id: string;
57
63
  organisation_id: string;
58
64
  app_id: AppId;
59
65
  category: FileCategory;
66
+ folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
67
+ pageContext: string;
68
+ event_id?: string;
60
69
  is_public?: boolean;
61
70
  custom_metadata?: Record<string, unknown>;
62
71
  }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @file Timezone Utilities Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/__tests__/Timezone
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for timezone utility functions covering all critical functionality,
8
+ * edge cases, and error handling.
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import {
13
+ formatInTimeZone,
14
+ getTimezoneAbbreviation,
15
+ formatTimeInTimeZone,
16
+ getUserTimeZone,
17
+ toZonedTime,
18
+ fromZonedTime,
19
+ roundToNearestMinutes,
20
+ getTimeZoneDifference
21
+ } from '../timezone';
22
+
23
+ describe('Timezone Utilities', () => {
24
+ describe('formatInTimeZone', () => {
25
+ it('should format date in specific timezone', () => {
26
+ const utcDate = new Date('2024-01-15T10:00:00Z');
27
+ const result = formatInTimeZone(utcDate, 'America/New_York', 'MMM dd, yyyy HH:mm');
28
+
29
+ // EST is UTC-5, so 10:00 UTC = 05:00 EST
30
+ expect(result).toContain('Jan 15, 2024');
31
+ expect(result).toContain('05:00');
32
+ });
33
+
34
+ it('should handle ISO string input', () => {
35
+ const result = formatInTimeZone('2024-01-15T10:00:00Z', 'America/New_York', 'MMM dd, yyyy');
36
+ expect(result).toContain('Jan 15, 2024');
37
+ });
38
+
39
+ it('should handle timestamp input', () => {
40
+ const timestamp = new Date('2024-01-15T10:00:00Z').getTime();
41
+ const result = formatInTimeZone(timestamp, 'America/New_York', 'MMM dd, yyyy');
42
+ expect(result).toContain('Jan 15, 2024');
43
+ });
44
+
45
+ it('should handle different format strings', () => {
46
+ const date = new Date('2024-01-15T10:00:00Z');
47
+ const result = formatInTimeZone(date, 'America/New_York', 'yyyy-MM-dd');
48
+ expect(result).toBe('2024-01-15');
49
+ });
50
+
51
+ it('should return "Invalid date" for invalid date input', () => {
52
+ const result = formatInTimeZone(new Date('invalid'), 'America/New_York', 'MMM dd, yyyy');
53
+ expect(result).toBe('Invalid date');
54
+ });
55
+
56
+ it('should return "Invalid date" for invalid timezone', () => {
57
+ const date = new Date('2024-01-15T10:00:00Z');
58
+ const result = formatInTimeZone(date, '', 'MMM dd, yyyy');
59
+ expect(result).toBe('Invalid date');
60
+ });
61
+
62
+ it('should handle DST transitions', () => {
63
+ // Summer date (DST)
64
+ const summerDate = new Date('2024-07-15T10:00:00Z');
65
+ const summerResult = formatInTimeZone(summerDate, 'America/New_York', 'HH:mm');
66
+
67
+ // Winter date (no DST)
68
+ const winterDate = new Date('2024-01-15T10:00:00Z');
69
+ const winterResult = formatInTimeZone(winterDate, 'America/New_York', 'HH:mm');
70
+
71
+ // Summer should be UTC-4 (EDT), winter should be UTC-5 (EST)
72
+ expect(summerResult).not.toBe(winterResult);
73
+ });
74
+ });
75
+
76
+ describe('getTimezoneAbbreviation', () => {
77
+ it('should get timezone abbreviation', () => {
78
+ const date = new Date('2024-01-15T10:00:00Z');
79
+ const result = getTimezoneAbbreviation(date, 'America/New_York');
80
+
81
+ // Should return EST or EDT depending on DST
82
+ expect(result).toMatch(/EST|EDT|America\/New_York/);
83
+ });
84
+
85
+ it('should handle different timezones', () => {
86
+ const date = new Date('2024-01-15T10:00:00Z');
87
+ const pst = getTimezoneAbbreviation(date, 'America/Los_Angeles');
88
+ const est = getTimezoneAbbreviation(date, 'America/New_York');
89
+
90
+ expect(pst).toBeTruthy();
91
+ expect(est).toBeTruthy();
92
+ });
93
+
94
+ it('should return timezone name on error', () => {
95
+ const date = new Date('invalid');
96
+ const result = getTimezoneAbbreviation(date, 'America/New_York');
97
+ expect(result).toBeTruthy();
98
+ });
99
+
100
+ it('should handle invalid timezone gracefully', () => {
101
+ const date = new Date('2024-01-15T10:00:00Z');
102
+ const result = getTimezoneAbbreviation(date, '');
103
+ // Empty string returns 'UTC' as fallback per implementation
104
+ expect(result).toBe('UTC');
105
+ });
106
+ });
107
+
108
+ describe('formatTimeInTimeZone', () => {
109
+ it('should format time only in timezone', () => {
110
+ const date = new Date('2024-01-15T10:00:00Z');
111
+ const result = formatTimeInTimeZone(date, 'America/New_York');
112
+
113
+ // Should be in HH:mm format
114
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
115
+ });
116
+
117
+ it('should handle string date input', () => {
118
+ const result = formatTimeInTimeZone('2024-01-15T10:00:00Z', 'America/New_York');
119
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
120
+ });
121
+ });
122
+
123
+ describe('getUserTimeZone', () => {
124
+ it('should return user timezone', () => {
125
+ const result = getUserTimeZone();
126
+ expect(typeof result).toBe('string');
127
+ expect(result.length).toBeGreaterThan(0);
128
+ });
129
+
130
+ it('should return UTC as fallback', () => {
131
+ // Mock Intl to be undefined
132
+ const originalIntl = global.Intl;
133
+ // @ts-expect-error - Testing error case
134
+ global.Intl = undefined;
135
+
136
+ const result = getUserTimeZone();
137
+ expect(result).toBe('UTC');
138
+
139
+ global.Intl = originalIntl;
140
+ });
141
+
142
+ it('should handle Intl.DateTimeFormat errors gracefully', () => {
143
+ const originalIntl = global.Intl;
144
+ // @ts-expect-error - Testing error case
145
+ global.Intl = {
146
+ DateTimeFormat: () => {
147
+ throw new Error('Test error');
148
+ }
149
+ };
150
+
151
+ const result = getUserTimeZone();
152
+ expect(result).toBe('UTC');
153
+
154
+ global.Intl = originalIntl;
155
+ });
156
+ });
157
+
158
+ describe('toZonedTime', () => {
159
+ it('should convert UTC to timezone local time', () => {
160
+ const utcDate = new Date('2024-01-15T10:00:00Z');
161
+ const result = toZonedTime(utcDate, 'America/New_York');
162
+
163
+ expect(result).toBeInstanceOf(Date);
164
+ expect(isNaN(result.getTime())).toBe(false);
165
+ });
166
+
167
+ it('should handle invalid date input', () => {
168
+ const result = toZonedTime(new Date('invalid'), 'America/New_York');
169
+ expect(isNaN(result.getTime())).toBe(true);
170
+ });
171
+
172
+ it('should handle invalid timezone', () => {
173
+ const date = new Date('2024-01-15T10:00:00Z');
174
+ const result = toZonedTime(date, '');
175
+ // Empty timezone returns original date per implementation
176
+ expect(result).toBe(date);
177
+ expect(result.getTime()).toBe(date.getTime());
178
+ });
179
+
180
+ it('should handle DST correctly', () => {
181
+ const summerDate = new Date('2024-07-15T10:00:00Z');
182
+ const winterDate = new Date('2024-01-15T10:00:00Z');
183
+
184
+ const summerResult = toZonedTime(summerDate, 'America/New_York');
185
+ const winterResult = toZonedTime(winterDate, 'America/New_York');
186
+
187
+ // Results should be different due to DST
188
+ expect(summerResult.getHours()).not.toBe(winterResult.getHours());
189
+ });
190
+ });
191
+
192
+ describe('fromZonedTime', () => {
193
+ it('should convert local time to UTC', () => {
194
+ const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM (local)
195
+ const result = fromZonedTime(localDate, 'America/New_York');
196
+
197
+ expect(result).toBeInstanceOf(Date);
198
+ expect(isNaN(result.getTime())).toBe(false);
199
+ });
200
+
201
+ it('should handle invalid date input', () => {
202
+ const result = fromZonedTime(new Date('invalid'), 'America/New_York');
203
+ expect(isNaN(result.getTime())).toBe(true);
204
+ });
205
+
206
+ it('should handle invalid timezone', () => {
207
+ const date = new Date(2024, 0, 15, 10, 0);
208
+ const result = fromZonedTime(date, '');
209
+ // Empty timezone returns original date per implementation
210
+ expect(result).toBe(date);
211
+ expect(result.getTime()).toBe(date.getTime());
212
+ });
213
+
214
+ it('should correctly convert to UTC', () => {
215
+ // 10:00 AM EST (UTC-5) should be 15:00 UTC
216
+ // Using a specific date in January (no DST) for predictable results
217
+ const localDate = new Date(2024, 0, 15, 10, 0, 0, 0);
218
+ const utcResult = fromZonedTime(localDate, 'America/New_York');
219
+
220
+ // Check that it's approximately 5 hours ahead (EST is UTC-5)
221
+ // The actual difference depends on how fromZonedTime interprets the local date
222
+ // We just verify it's a valid date and the conversion happened
223
+ expect(isNaN(utcResult.getTime())).toBe(false);
224
+ expect(utcResult).toBeInstanceOf(Date);
225
+ });
226
+ });
227
+
228
+ describe('roundToNearestMinutes', () => {
229
+ it('should round to nearest 5 minutes by default', () => {
230
+ const date = new Date('2024-01-15T10:23:00Z');
231
+ const result = roundToNearestMinutes(date);
232
+
233
+ expect(result).toBeInstanceOf(Date);
234
+ const minutes = result.getMinutes();
235
+ expect([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]).toContain(minutes);
236
+ });
237
+
238
+ it('should round to specified minutes step', () => {
239
+ const date = new Date('2024-01-15T10:23:00Z');
240
+ const result = roundToNearestMinutes(date, 15);
241
+
242
+ const minutes = result.getMinutes();
243
+ expect([0, 15, 30, 45]).toContain(minutes);
244
+ });
245
+
246
+ it('should handle invalid date input', () => {
247
+ const result = roundToNearestMinutes(new Date('invalid'));
248
+ expect(isNaN(result.getTime())).toBe(true);
249
+ });
250
+
251
+ it('should return original date for invalid minutesStep', () => {
252
+ const date = new Date('2024-01-15T10:23:00Z');
253
+ const result = roundToNearestMinutes(date, -5);
254
+ expect(result.getTime()).toBe(date.getTime());
255
+ });
256
+
257
+ it('should return original date for non-integer minutesStep', () => {
258
+ const date = new Date('2024-01-15T10:23:00Z');
259
+ const result = roundToNearestMinutes(date, 5.5);
260
+ expect(result.getTime()).toBe(date.getTime());
261
+ });
262
+
263
+ it('should round up correctly', () => {
264
+ const date = new Date('2024-01-15T10:23:00Z');
265
+ const result = roundToNearestMinutes(date, 5);
266
+ // Should round to 25 (closer to 25 than 20)
267
+ expect(result.getMinutes()).toBe(25);
268
+ });
269
+ });
270
+
271
+ describe('getTimeZoneDifference', () => {
272
+ it('should calculate timezone difference in hours', () => {
273
+ const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
274
+ // EST is 3 hours ahead of PST
275
+ expect(result).toBe(-3);
276
+ });
277
+
278
+ it('should handle UTC timezone', () => {
279
+ const result = getTimeZoneDifference('UTC', 'America/New_York');
280
+ // UTC to EST is -5 hours (or -4 during DST)
281
+ expect([-5, -4]).toContain(result);
282
+ });
283
+
284
+ it('should return 0 for same timezone', () => {
285
+ const result = getTimeZoneDifference('America/New_York', 'America/New_York');
286
+ expect(result).toBe(0);
287
+ });
288
+
289
+ it('should handle invalid timezones gracefully', () => {
290
+ const result = getTimeZoneDifference('', 'America/New_York');
291
+ expect(result).toBe(0);
292
+ });
293
+
294
+ it('should handle reversed timezones', () => {
295
+ const forward = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
296
+ const backward = getTimeZoneDifference('America/Los_Angeles', 'America/New_York');
297
+
298
+ expect(forward).toBe(-backward);
299
+ });
300
+
301
+ it('should account for DST in calculations', () => {
302
+ // Test with a summer date (DST active)
303
+ const summerDiff = getTimeZoneDifference('UTC', 'America/New_York');
304
+ // Should be -4 (EDT) or -5 (EST) depending on current date
305
+ expect([-4, -5]).toContain(summerDiff);
306
+ });
307
+ });
308
+
309
+ describe('Edge cases and error handling', () => {
310
+ it('should handle null and undefined gracefully', () => {
311
+ // @ts-expect-error - Testing error case
312
+ expect(formatInTimeZone(null, 'America/New_York', 'MMM dd, yyyy')).toBe('Invalid date');
313
+ // @ts-expect-error - Testing error case
314
+ expect(formatInTimeZone(undefined, 'America/New_York', 'MMM dd, yyyy')).toBe('Invalid date');
315
+ });
316
+
317
+ it('should handle various timezone formats', () => {
318
+ const date = new Date('2024-01-15T10:00:00Z');
319
+ const timezones = [
320
+ 'America/New_York',
321
+ 'Europe/London',
322
+ 'Asia/Tokyo',
323
+ 'Australia/Sydney',
324
+ 'UTC'
325
+ ];
326
+
327
+ timezones.forEach(tz => {
328
+ const result = formatInTimeZone(date, tz, 'MMM dd, yyyy');
329
+ expect(result).not.toBe('Invalid date');
330
+ });
331
+ });
332
+
333
+ it('should handle extreme dates', () => {
334
+ const farFuture = new Date('2099-12-31T23:59:59Z');
335
+ // Use a date that won't have timezone boundary issues
336
+ const farPast = new Date('1900-01-01T12:00:00Z');
337
+
338
+ expect(formatInTimeZone(farFuture, 'America/New_York', 'yyyy')).toContain('2099');
339
+ // The year might be 1899 or 1900 depending on timezone conversion, so just check it's valid
340
+ const pastResult = formatInTimeZone(farPast, 'America/New_York', 'yyyy');
341
+ expect(pastResult).not.toBe('Invalid date');
342
+ expect(pastResult.length).toBeGreaterThan(0);
343
+ });
344
+ });
345
+ });
@@ -50,6 +50,8 @@ 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',
54
+ pageContext: 'configuration',
53
55
  is_public: false
54
56
  };
55
57
 
@@ -104,7 +106,7 @@ describe('[service] FileReferenceServiceImpl', () => {
104
106
  testFile,
105
107
  expect.objectContaining({
106
108
  orgId: mockFileUploadOptions.organisation_id,
107
- customPath: mockFileUploadOptions.category,
109
+ customPath: mockFileUploadOptions.folder,
108
110
  isPublic: mockFileUploadOptions.is_public
109
111
  })
110
112
  );
@@ -116,6 +118,7 @@ describe('[service] FileReferenceServiceImpl', () => {
116
118
  p_record_id: mockFileUploadOptions.record_id,
117
119
  p_organisation_id: mockFileUploadOptions.organisation_id,
118
120
  p_app_id: mockFileUploadOptions.app_id,
121
+ p_page_context: mockFileUploadOptions.pageContext,
119
122
  p_file_metadata: expect.objectContaining({
120
123
  fileName: testFile.name,
121
124
  fileType: testFile.type,
@@ -755,22 +758,75 @@ describe('[utility] createFileReferenceService', () => {
755
758
  describe('[utility] uploadFileWithReference', () => {
756
759
  it('uploads file and creates reference successfully', async () => {
757
760
  const testFile = createTestFile();
761
+
762
+ // Mock successful RPC call that returns file reference ID
763
+ mockSupabase.rpc.mockResolvedValue({
764
+ data: 'file-ref-123',
765
+ error: null
766
+ });
767
+
768
+ // Mock successful file reference fetch - using the same pattern as other tests
769
+ (mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
770
+ data: mockFileReference,
771
+ error: null
772
+ });
773
+
774
+ // Mock storage signed URL generation
775
+ mockSupabase.storage = {
776
+ from: vi.fn().mockReturnValue({
777
+ createSignedUrl: vi.fn().mockResolvedValue({
778
+ data: { signedUrl: 'https://example.com/signed-url' },
779
+ error: null
780
+ })
781
+ })
782
+ } as any;
783
+
758
784
  const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
759
785
  expect(result).toHaveProperty('file_reference');
760
786
  expect('file_url' in result).toBe(true);
761
787
  });
762
788
 
763
789
  it('handles upload failures gracefully', async () => {
764
- // This path is covered by service tests; here just ensure function returns structured result on success path
790
+ // Mock upload failure
791
+ mockUploadFile.mockResolvedValue({
792
+ success: false,
793
+ error: 'Upload failed'
794
+ });
795
+
765
796
  const testFile = createTestFile();
766
- const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
767
- expect(result).toHaveProperty('file_reference');
797
+ await expect(uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile))
798
+ .rejects.toThrow('Upload failed');
768
799
  });
769
800
 
770
801
  it('handles URL generation failures gracefully', async () => {
771
802
  const testFile = createTestFile();
803
+
804
+ // Mock successful RPC call
805
+ mockSupabase.rpc.mockResolvedValue({
806
+ data: 'file-ref-123',
807
+ error: null
808
+ });
809
+
810
+ // Mock successful file reference fetch
811
+ (mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
812
+ data: mockFileReference,
813
+ error: null
814
+ });
815
+
816
+ // Mock storage to fail on signed URL generation
817
+ mockSupabase.storage = {
818
+ from: vi.fn().mockReturnValue({
819
+ createSignedUrl: vi.fn().mockResolvedValue({
820
+ data: null,
821
+ error: { message: 'URL generation failed' }
822
+ })
823
+ })
824
+ } as any;
825
+
772
826
  const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
773
827
  expect(result).toHaveProperty('file_reference');
828
+ // URL should be empty string when generation fails
829
+ expect(result.file_url).toBe('');
774
830
  });
775
831
 
776
832
  it('validates required parameters', async () => {
@@ -50,13 +50,13 @@ export class FileReferenceServiceImpl implements FileReferenceService {
50
50
  }
51
51
 
52
52
  // Step 1: Upload file to storage bucket first
53
- // This generates a unique path: {orgId}/{category}/{timestamp-uuid-filename}
53
+ // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename}
54
54
  // Bucket is automatically selected based on is_public flag
55
55
  const uploadResult = await uploadFile(this.supabase, file, {
56
56
  appName: 'file-reference',
57
57
  orgId: options.organisation_id,
58
58
  isPublic: options.is_public || false,
59
- customPath: options.category // Use category as the custom path segment
59
+ customPath: options.folder // Use folder prop as the custom path segment
60
60
  });
61
61
  if (!uploadResult.success) {
62
62
  throw new Error(`Failed to upload file: ${uploadResult.error}`);
@@ -88,6 +88,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
88
88
  p_file_path: filePath, // Storage path from step 1
89
89
  p_organisation_id: options.organisation_id,
90
90
  p_app_id: options.app_id,
91
+ p_page_context: options.pageContext,
92
+ p_event_id: options.event_id || null, // Pass event_id for event-based apps
91
93
  p_file_metadata: {
92
94
  fileName: file.name,
93
95
  fileType: file.type,
@@ -105,6 +107,13 @@ export class FileReferenceServiceImpl implements FileReferenceService {
105
107
  throw new Error(`Failed to create file reference: ${error.message}`);
106
108
  }
107
109
 
110
+ // Check if RPC returned null (permission denied or other failure)
111
+ if (!data || data === null) {
112
+ // Clean up the uploaded file since DB insert failed
113
+ await deleteFile(this.supabase, filePath, options.is_public || false);
114
+ 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.`);
115
+ }
116
+
108
117
  // Get the created file reference
109
118
  const { data: fileRef, error: fetchError } = await this.supabase
110
119
  .from('file_references')
@@ -113,6 +122,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
113
122
  .single();
114
123
 
115
124
  if (fetchError || !fileRef) {
125
+ // Clean up uploaded file if we can't fetch the reference
126
+ await deleteFile(this.supabase, filePath, options.is_public || false);
116
127
  throw new Error(`Failed to fetch created file reference: ${fetchError?.message}`);
117
128
  }
118
129