@jmruthers/pace-core 0.6.10 → 0.6.11

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 (726) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/audit-tool/00-dependencies.cjs +46 -13
  3. package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
  4. package/audit-tool/audits/02-project-structure.cjs +13 -3
  5. package/audit-tool/audits/03-architecture.cjs +78 -4
  6. package/audit-tool/audits/04-code-quality.cjs +9 -2
  7. package/audit-tool/audits/05-styling.cjs +19 -7
  8. package/audit-tool/audits/06-security-rbac.cjs +105 -14
  9. package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
  10. package/audit-tool/audits/08-testing-documentation.cjs +11 -3
  11. package/audit-tool/audits/09-operations.cjs +19 -7
  12. package/audit-tool/index.cjs +22 -11
  13. package/audit-tool/utils/report-utils.cjs +4 -0
  14. package/cursor-rules/01-pace-core-compliance.mdc +1 -0
  15. package/cursor-rules/02-project-structure.mdc +1 -0
  16. package/cursor-rules/03-architecture.mdc +3 -1
  17. package/cursor-rules/04-code-quality.mdc +1 -0
  18. package/cursor-rules/05-styling.mdc +41 -7
  19. package/cursor-rules/06-security-rbac.mdc +2 -1
  20. package/cursor-rules/07-api-tech-stack.mdc +1 -0
  21. package/cursor-rules/08-testing-documentation.mdc +1 -0
  22. package/cursor-rules/09-operations.mdc +1 -0
  23. package/dist/{DataTable-SAXFG4XI.js → DataTable-EFYP2QLE.js} +10 -7
  24. package/dist/{InactivityServiceProvider-DHryoh6K.d.ts → InactivityServiceProvider-BbxwwDz1.d.ts} +10 -1
  25. package/dist/{UnifiedAuthProvider-CiBAl9-s.d.ts → UnifiedAuthProvider-Bkt_tzdS.d.ts} +56 -24
  26. package/dist/{api-F47QJ7FX.js → api-BZR2CYXL.js} +3 -2
  27. package/dist/api-result-USV1Czr-.d.ts +51 -0
  28. package/dist/{audit-Z6ZZBWLU.js → audit-HI2DHUVU.js} +2 -1
  29. package/dist/{auth-BZOJqrdd.d.ts → auth-JvdRVaud.d.ts} +1 -1
  30. package/dist/{chunk-KSNLMI7N.js → chunk-2DL2WSOE.js} +1 -155
  31. package/dist/{chunk-MPY44PWB.js → chunk-2OEVOGGR.js} +4648 -3560
  32. package/dist/chunk-44CNXN4P.js +15 -0
  33. package/dist/{chunk-Y4PF6HIM.js → chunk-4R3T5ENU.js} +867 -786
  34. package/dist/{chunk-LNHFAF4X.js → chunk-7A6IMHH2.js} +289 -247
  35. package/dist/chunk-CU2BU2MQ.js +2 -0
  36. package/dist/{chunk-JJEYZ3DX.js → chunk-D6BMFMQZ.js} +37 -2
  37. package/dist/{chunk-BCTXBU6U.js → chunk-ENLXB7GP.js} +88 -71
  38. package/dist/{chunk-FBZ7U3ID.js → chunk-J2KQK6DG.js} +937 -987
  39. package/dist/{chunk-TFIPNIPE.js → chunk-KJXRL3XE.js} +3300 -2245
  40. package/dist/{chunk-3GWSPISD.js → chunk-L5LFKKLJ.js} +1 -1
  41. package/dist/{chunk-X5EAU5G7.js → chunk-PCSHBLPB.js} +132 -114
  42. package/dist/{chunk-NIU6DPQV.js → chunk-QRYSEPHB.js} +2 -0
  43. package/dist/{chunk-KYURMOQM.js → chunk-V7FTM2LU.js} +423 -320
  44. package/dist/chunk-WY6Y7KC3.js +264 -0
  45. package/dist/{chunk-FN52B75D.js → chunk-XOJME5T7.js} +176 -15
  46. package/dist/{chunk-7YDC7LMU.js → chunk-XPFVT3GN.js} +71 -66
  47. package/dist/{chunk-66R6RLUZ.js → chunk-YFTFFJIV.js} +3 -3
  48. package/dist/{chunk-W46INAVW.js → chunk-YYTWKVHO.js} +688 -570
  49. package/dist/components.d.ts +8 -7
  50. package/dist/components.js +17 -15
  51. package/dist/{database.generated-DT8JTZiP.d.ts → database.generated-qkdoiVrJ.d.ts} +45 -10
  52. package/dist/eslint-rules/index.cjs +3 -0
  53. package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
  54. package/dist/eslint-rules/rules/06-security-rbac.cjs +74 -0
  55. package/dist/{event-WTAQuGcq.d.ts → event-BfCox3N2.d.ts} +36 -10
  56. package/dist/{file-reference-BavO2eQj.d.ts → file-reference-DU1hcawx.d.ts} +29 -13
  57. package/dist/hooks.d.ts +22 -9
  58. package/dist/hooks.js +34 -25
  59. package/dist/icons/index.d.ts +1 -0
  60. package/dist/icons/index.js +1 -0
  61. package/dist/index.d.ts +66 -177
  62. package/dist/index.js +316 -340
  63. package/dist/pagination-BW1mqywp.d.ts +201 -0
  64. package/dist/providers.d.ts +6 -5
  65. package/dist/providers.js +5 -3
  66. package/dist/rbac/index.d.ts +123 -138
  67. package/dist/rbac/index.js +10 -8
  68. package/dist/theming/runtime.d.ts +19 -2
  69. package/dist/theming/runtime.js +1 -1
  70. package/dist/{timezone-K-ptz3HO.d.ts → timezone-BTWWXKVY.d.ts} +1 -1
  71. package/dist/types.d.ts +17 -10
  72. package/dist/types.js +1 -0
  73. package/dist/{usePublicPageContext-vxBlEHO9.d.ts → usePublicPageContext-B91dGYW1.d.ts} +433 -356
  74. package/dist/{usePublicRouteParams-G3Ks53mk.d.ts → usePublicRouteParams-BgV6VhMi.d.ts} +73 -4
  75. package/dist/utils.d.ts +163 -145
  76. package/dist/utils.js +42 -25
  77. package/docs/api/modules.md +782 -643
  78. package/docs/api-reference/rpc-functions.md +12 -3
  79. package/docs/core-concepts/rbac-system.md +8 -0
  80. package/docs/getting-started/cursor-rules.md +17 -20
  81. package/docs/getting-started/dependencies.md +1 -1
  82. package/docs/getting-started/setup.md +235 -0
  83. package/docs/implementation-guides/authentication.md +27 -0
  84. package/docs/implementation-guides/data-tables.md +176 -3
  85. package/docs/migration/ApiResult-migration.md +25 -0
  86. package/docs/rbac/api-reference.md +33 -31
  87. package/docs/standards/0-standards-overview.md +50 -15
  88. package/docs/standards/1-pace-core-compliance-standards.md +62 -57
  89. package/docs/standards/2-project-structure-standards.md +33 -16
  90. package/docs/standards/3-architecture-standards.md +41 -1
  91. package/docs/standards/4-code-quality-standards.md +26 -6
  92. package/docs/standards/5-styling-standards.md +35 -1
  93. package/docs/standards/6-security-rbac-standards.md +66 -0
  94. package/docs/standards/7-api-tech-stack-standards.md +25 -14
  95. package/docs/standards/8-testing-documentation-standards.md +31 -0
  96. package/docs/standards/9-operations-standards.md +19 -0
  97. package/docs/standards/README.md +20 -201
  98. package/docs/testing/test-setup-for-consumers.md +2 -0
  99. package/docs/troubleshooting/common-issues.md +17 -1
  100. package/docs/troubleshooting/organisation-context-setup.md +8 -0
  101. package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
  102. package/eslint-config-pace-core.cjs +20 -0
  103. package/package.json +14 -20
  104. package/scripts/{build-docs-incremental.js → build-docs.js} +3 -2
  105. package/scripts/setup.cjs +536 -0
  106. package/scripts/validate.cjs +480 -0
  107. package/src/__tests__/helpers/{__tests__/component-test-utils.test.tsx → component-test-utils.test.tsx} +3 -3
  108. package/src/__tests__/helpers/{__tests__/optimized-test-setup.test.ts → optimized-test-setup.test.ts} +2 -2
  109. package/src/__tests__/helpers/{__tests__/supabaseMock.test.ts → supabaseMock.test.ts} +2 -2
  110. package/src/__tests__/helpers/{__tests__/test-providers.test.tsx → test-providers.test.tsx} +1 -1
  111. package/src/__tests__/helpers/test-providers.tsx +37 -39
  112. package/src/__tests__/helpers/{__tests__/test-utils.test.tsx → test-utils.test.tsx} +4 -3
  113. package/src/__tests__/helpers/{__tests__/timer-utils.test.ts → timer-utils.test.ts} +2 -2
  114. package/src/assets/app-icons/index.test.ts +304 -0
  115. package/src/components/AddressField/AddressField.test.tsx +1 -1
  116. package/src/components/AddressField/AddressField.tsx +238 -212
  117. package/src/components/Button/Button.tsx +1 -1
  118. package/src/components/Card/Card.test.tsx +172 -17
  119. package/src/components/Card/Card.tsx +19 -10
  120. package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
  121. package/src/components/ContextSelector/{__tests__/ContextSelector.test.tsx → ContextSelector.test.tsx} +6 -6
  122. package/src/components/ContextSelector/ContextSelector.tsx +66 -280
  123. package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
  124. package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
  125. package/src/components/DataTable/AUDIT_REPORT.md +59 -44
  126. package/src/components/DataTable/{__tests__/DataTable.comprehensive.test.tsx → DataTable.comprehensive.test.tsx} +6 -6
  127. package/src/components/DataTable/{__tests__/DataTable.default-state.test.tsx → DataTable.default-state.test.tsx} +5 -5
  128. package/src/components/DataTable/{__tests__/DataTable.export.test.tsx → DataTable.export.test.tsx} +10 -10
  129. package/src/components/DataTable/{__tests__/DataTable.grouping-aggregation.test.tsx → DataTable.grouping-aggregation.test.tsx} +6 -6
  130. package/src/components/DataTable/{__tests__/DataTable.hooks.test.tsx → DataTable.hooks.test.tsx} +6 -6
  131. package/src/components/DataTable/{__tests__/DataTable.select-label-display.test.tsx → DataTable.select-label-display.test.tsx} +6 -6
  132. package/src/components/DataTable/DataTable.test.tsx +787 -416
  133. package/src/components/DataTable/DataTable.tsx +12 -12
  134. package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
  135. package/src/components/DataTable/{__tests__/DataTableCore.test-setup.ts → DataTableCore.test-setup.ts} +10 -9
  136. package/src/components/DataTable/{__tests__/DataTableCore.test.tsx → DataTableCore.test.tsx} +8 -8
  137. package/src/components/DataTable/{__tests__/README.md → README.md} +17 -7
  138. package/src/components/DataTable/TESTING.md +101 -0
  139. package/src/components/DataTable/{__tests__/a11y.basic.test.tsx → a11y.basic.test.tsx} +34 -34
  140. package/src/components/DataTable/components/DataTableCore.tsx +104 -864
  141. package/src/components/DataTable/components/{__tests__/GroupingDropdown.test.tsx → GroupingDropdown.test.tsx} +17 -8
  142. package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
  143. package/src/components/DataTable/components/ImportModal.tsx +61 -559
  144. package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
  145. package/src/components/DataTable/context/{__tests__/DataTableContext.test.tsx → DataTableContext.test.tsx} +2 -2
  146. package/src/components/DataTable/context/DataTableContext.tsx +7 -6
  147. package/src/components/DataTable/core/{__tests__/ColumnFactory.test.ts → ColumnFactory.test.ts} +2 -2
  148. package/src/components/DataTable/hooks/{__tests__/useColumnOrderPersistence.test.ts → useColumnOrderPersistence.test.ts} +2 -2
  149. package/src/components/DataTable/hooks/{__tests__/useColumnVisibilityPersistence.test.ts → useColumnVisibilityPersistence.test.ts} +2 -2
  150. package/src/components/DataTable/hooks/{__tests__/useDataTableConfiguration.test.ts → useDataTableConfiguration.test.ts} +3 -3
  151. package/src/components/DataTable/hooks/useDataTableConfiguration.ts +14 -2
  152. package/src/components/DataTable/hooks/{__tests__/useDataTableDataPipeline.test.ts → useDataTableDataPipeline.test.ts} +6 -6
  153. package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
  154. package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
  155. package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
  156. package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +238 -0
  157. package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
  158. package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
  159. package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
  160. package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
  161. package/src/components/DataTable/hooks/{__tests__/useDataTablePermissions.test.ts → useDataTablePermissions.test.ts} +11 -11
  162. package/src/components/DataTable/hooks/useDataTablePermissions.ts +79 -247
  163. package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
  164. package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
  165. package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
  166. package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
  167. package/src/components/DataTable/hooks/useDataTableScope.test.ts +110 -0
  168. package/src/components/DataTable/hooks/useDataTableScope.ts +123 -0
  169. package/src/components/DataTable/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +47 -5
  170. package/src/components/DataTable/hooks/useDataTableState.ts +145 -94
  171. package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
  172. package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
  173. package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
  174. package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
  175. package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
  176. package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
  177. package/src/components/DataTable/hooks/{__tests__/useEffectiveColumnOrder.test.ts → useEffectiveColumnOrder.test.ts} +2 -2
  178. package/src/components/DataTable/hooks/{__tests__/useHierarchicalState.test.ts → useHierarchicalState.test.ts} +2 -2
  179. package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.test.ts +3 -3
  180. package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.ts +2 -2
  181. package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
  182. package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
  183. package/src/components/DataTable/hooks/{__tests__/useKeyboardNavigation.test.ts → useKeyboardNavigation.test.ts} +3 -3
  184. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +309 -269
  185. package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.test.ts +3 -3
  186. package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.ts +3 -3
  187. package/src/components/DataTable/hooks/{__tests__/useServerSideDataEffect.test.ts → useServerSideDataEffect.test.ts} +2 -2
  188. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +14 -3
  189. package/src/components/DataTable/hooks/{__tests__/useTableColumns.test.ts → useTableColumns.test.ts} +2 -2
  190. package/src/components/DataTable/hooks/{__tests__/useTableHandlers.test.ts → useTableHandlers.test.ts} +25 -4
  191. package/src/components/DataTable/hooks/useTableHandlers.ts +5 -2
  192. package/src/components/DataTable/index.ts +18 -17
  193. package/src/components/DataTable/{__tests__/keyboard.test.tsx → keyboard.test.tsx} +41 -63
  194. package/src/components/DataTable/{__tests__/mocks → mocks}/MockRBACProvider.tsx +1 -1
  195. package/src/components/DataTable/{__tests__/pagination.modes.test.tsx → pagination.modes.test.tsx} +6 -6
  196. package/src/components/DataTable/{__tests__/ssr.strict-mode.test.tsx → ssr.strict-mode.test.tsx} +2 -2
  197. package/src/components/DataTable/{__tests__/styles.test.ts → styles.test.ts} +1 -4
  198. package/src/components/DataTable/styles.ts +0 -1
  199. package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
  200. package/src/components/DataTable/{__tests__/test-utils → test-utils}/dataFactories.ts +2 -2
  201. package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
  202. package/src/components/DataTable/{__tests__/test-utils/sharedTestUtils.tsx → test-utils/sharedTestUtils.ts} +97 -66
  203. package/src/components/DataTable/{__tests__/test-utils.ts → test-utils.ts} +1 -1
  204. package/src/components/DataTable/types/actions.ts +71 -0
  205. package/src/components/DataTable/types/base.ts +39 -0
  206. package/src/components/DataTable/types/columns.ts +125 -0
  207. package/src/components/DataTable/types/export.ts +32 -0
  208. package/src/components/DataTable/types/features.ts +81 -0
  209. package/src/components/DataTable/types/hierarchical.ts +44 -0
  210. package/src/components/DataTable/types/index.ts +43 -0
  211. package/src/components/DataTable/types/pagination.ts +85 -0
  212. package/src/components/DataTable/types/performance.ts +47 -0
  213. package/src/components/DataTable/types/props.ts +62 -0
  214. package/src/components/DataTable/types/rbac.ts +45 -0
  215. package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableCore.test.tsx +430 -28
  216. package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
  217. package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableErrorBoundary.test.tsx +4 -4
  218. package/src/components/DataTable/{components → ui/layout}/DataTableErrorBoundary.tsx +7 -7
  219. package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
  220. package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
  221. package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
  222. package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
  223. package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
  224. package/src/components/DataTable/{components → ui/modals}/DataTableModals.tsx +36 -28
  225. package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
  226. package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
  227. package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
  228. package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
  229. package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
  230. package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
  231. package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
  232. package/src/components/DataTable/{components/__tests__ → ui/shared}/AccessDeniedPage.test.tsx +2 -2
  233. package/src/components/DataTable/{components → ui/shared}/AccessDeniedPage.tsx +2 -2
  234. package/src/components/DataTable/{components/__tests__ → ui/shared}/ActionButtons.test.tsx +6 -4
  235. package/src/components/DataTable/{components → ui/shared}/ActionButtons.tsx +4 -4
  236. package/src/components/DataTable/{components/__tests__ → ui/shared}/ColumnFilter.test.tsx +29 -16
  237. package/src/components/DataTable/{components → ui/shared}/ColumnFilter.tsx +4 -4
  238. package/src/components/DataTable/{components/__tests__ → ui/shared}/PaginationControls.test.tsx +38 -16
  239. package/src/components/DataTable/{components → ui/shared}/PaginationControls.tsx +21 -15
  240. package/src/components/DataTable/{components/__tests__ → ui/shared}/SortIndicator.test.tsx +2 -2
  241. package/src/components/DataTable/{components → ui/shared}/SortIndicator.tsx +1 -1
  242. package/src/components/DataTable/{components/__tests__ → ui/table}/EditFields.test.tsx +3 -3
  243. package/src/components/DataTable/{components → ui/table}/EditFields.tsx +138 -69
  244. package/src/components/DataTable/{components/__tests__ → ui/table}/EditableRow.test.tsx +36 -27
  245. package/src/components/DataTable/{components → ui/table}/EditableRow.tsx +86 -104
  246. package/src/components/DataTable/{components/__tests__ → ui/table}/EmptyState.test.tsx +2 -62
  247. package/src/components/DataTable/{components → ui/table}/EmptyState.tsx +7 -15
  248. package/src/components/DataTable/{components/__tests__ → ui/table}/FilterRow.test.tsx +5 -4
  249. package/src/components/DataTable/{components → ui/table}/FilterRow.tsx +3 -3
  250. package/src/components/DataTable/{components/__tests__ → ui/table}/LoadingState.test.tsx +6 -10
  251. package/src/components/DataTable/{components → ui/table}/LoadingState.tsx +4 -4
  252. package/src/components/DataTable/{components/__tests__ → ui/table}/RowComponent.test.tsx +412 -17
  253. package/src/components/DataTable/{components → ui/table}/RowComponent.tsx +183 -177
  254. package/src/components/DataTable/{components/__tests__ → ui/table}/UnifiedTableBody.test.tsx +425 -16
  255. package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
  256. package/src/components/DataTable/{components/__tests__ → ui/table}/cellValueUtils.test.ts +2 -2
  257. package/src/components/DataTable/{components → ui/table}/cellValueUtils.ts +1 -1
  258. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/BulkOperationsDropdown.test.tsx +12 -5
  259. package/src/components/DataTable/{components → ui/toolbar}/BulkOperationsDropdown.tsx +3 -3
  260. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/ColumnVisibilityDropdown.test.tsx +7 -4
  261. package/src/components/DataTable/{components → ui/toolbar}/ColumnVisibilityDropdown.tsx +7 -7
  262. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/DataTableToolbar.test.tsx +4 -4
  263. package/src/components/DataTable/{components → ui/toolbar}/DataTableToolbar.tsx +4 -4
  264. package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
  265. package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
  266. package/src/components/DataTable/utils/{__tests__/a11yUtils.test.ts → a11yUtils.test.ts} +2 -2
  267. package/src/components/DataTable/utils/{__tests__/aggregationUtils.test.ts → aggregationUtils.test.ts} +3 -3
  268. package/src/components/DataTable/utils/{__tests__/columnUtils.test.ts → columnUtils.test.ts} +2 -2
  269. package/src/components/DataTable/utils/csvParse.test.ts +74 -0
  270. package/src/components/DataTable/utils/csvParse.ts +65 -0
  271. package/src/components/DataTable/utils/{__tests__/errorHandling.test.ts → errorHandling.test.ts} +2 -2
  272. package/src/components/DataTable/utils/{__tests__/exportUtils.test.ts → exportUtils.test.ts} +3 -3
  273. package/src/components/DataTable/utils/{__tests__/flexibleImport.test.ts → flexibleImport.test.ts} +2 -2
  274. package/src/components/DataTable/utils/flexibleImport.ts +3 -186
  275. package/src/components/DataTable/utils/{__tests__/hierarchicalSorting.test.ts → hierarchicalSorting.test.ts} +3 -3
  276. package/src/components/DataTable/utils/{__tests__/hierarchicalUtils.test.ts → hierarchicalUtils.test.ts} +3 -3
  277. package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
  278. package/src/components/DataTable/utils/importDateParser.ts +114 -0
  279. package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
  280. package/src/components/DataTable/utils/importValueParser.ts +91 -0
  281. package/src/components/DataTable/utils/{__tests__/paginationUtils.test.ts → paginationUtils.test.ts} +2 -2
  282. package/src/components/DataTable/utils/paginationUtils.ts +6 -3
  283. package/src/components/DataTable/utils/{__tests__/performanceUtils.test.ts → performanceUtils.test.ts} +3 -3
  284. package/src/components/DataTable/utils/{__tests__/rowUtils.test.ts → rowUtils.test.ts} +3 -3
  285. package/src/components/DataTable/utils/{__tests__/selectFieldUtils.test.ts → selectFieldUtils.test.ts} +66 -3
  286. package/src/components/DataTable/utils/selectFieldUtils.ts +97 -60
  287. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
  288. package/src/components/DateTimeField/DateTimeField.test.tsx +1 -1
  289. package/src/components/Dialog/Dialog.test-utils.ts +49 -0
  290. package/src/components/Dialog/Dialog.test.tsx +896 -89
  291. package/src/components/Dialog/Dialog.tsx +174 -882
  292. package/src/components/Dialog/dialogLock.test.ts +238 -0
  293. package/src/components/Dialog/dialogLock.ts +98 -0
  294. package/src/components/Dialog/index.ts +2 -0
  295. package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
  296. package/src/components/Dialog/useDialogDimensions.ts +140 -0
  297. package/src/components/Dialog/useDialogLifecycle.test.ts +358 -0
  298. package/src/components/Dialog/useDialogLifecycle.ts +135 -0
  299. package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
  300. package/src/components/Dialog/useDialogPersistence.ts +357 -0
  301. package/src/components/FileDisplay/FileDisplay.test.tsx +40 -40
  302. package/src/components/FileDisplay/FileDisplay.tsx +24 -656
  303. package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
  304. package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
  305. package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
  306. package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
  307. package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
  308. package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
  309. package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
  310. package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
  311. package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
  312. package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
  313. package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
  314. package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
  315. package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
  316. package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
  317. package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
  318. package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
  319. package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
  320. package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
  321. package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
  322. package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
  323. package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
  324. package/src/components/FileDisplay/fallbackUtils.ts +44 -0
  325. package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
  326. package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
  327. package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
  328. package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
  329. package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.test.ts +40 -42
  330. package/src/components/FileDisplay/useFileDisplay.ts +515 -0
  331. package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.unit.test.ts +406 -77
  332. package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
  333. package/src/{hooks/public → components/FileDisplay}/usePublicFileDisplay.test.ts +94 -88
  334. package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
  335. package/src/components/FileUpload/FileUpload.test.tsx +16 -10
  336. package/src/components/FileUpload/FileUpload.tsx +107 -525
  337. package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
  338. package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
  339. package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
  340. package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
  341. package/src/components/FileUpload/useFileUploadManager.ts +454 -0
  342. package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
  343. package/src/components/FileUpload/useResolvedAppId.ts +77 -0
  344. package/src/components/Footer/Footer.test.tsx +6 -292
  345. package/src/components/Footer/Footer.tsx +8 -125
  346. package/src/components/Form/Form.test.tsx +44 -27
  347. package/src/components/Form/Form.tsx +64 -287
  348. package/src/components/Form/useFormPersistence.ts +257 -0
  349. package/src/components/Header/Header.test.tsx +17 -18
  350. package/src/components/Header/Header.tsx +10 -1
  351. package/src/components/Input/Input.tsx +1 -1
  352. package/src/components/Label/Label.test.tsx +1 -1
  353. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +1 -1
  354. package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
  355. package/src/components/NavigationMenu/NavigationMenu.test.tsx +1029 -26
  356. package/src/components/NavigationMenu/NavigationMenu.tsx +61 -361
  357. package/src/components/NavigationMenu/index.ts +6 -1
  358. package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
  359. package/src/components/NavigationMenu/{__tests__/useNavigationFiltering.test.ts → useNavigationFiltering.test.ts} +68 -53
  360. package/src/components/NavigationMenu/useNavigationFiltering.ts +197 -296
  361. package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
  362. package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +77 -62
  363. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +3 -3
  364. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +16 -19
  365. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +529 -5
  366. package/src/components/PaceAppLayout/PaceAppLayout.tsx +280 -756
  367. package/src/components/PaceAppLayout/useFilteredNavItems.ts +304 -0
  368. package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +142 -0
  369. package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
  370. package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
  371. package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +79 -0
  372. package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
  373. package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
  374. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +31 -25
  375. package/src/components/PaceLoginPage/PaceLoginPage.tsx +31 -122
  376. package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
  377. package/src/components/Progress/Progress.tsx +1 -2
  378. package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -235
  379. package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
  380. package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
  381. package/src/components/PublicLayout/PublicLayout.test.tsx +217 -36
  382. package/src/components/PublicLayout/PublicPageLayout.tsx +132 -73
  383. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -1
  384. package/src/components/Select/Select.test.tsx +1 -1
  385. package/src/components/Select/Select.tsx +28 -18
  386. package/src/components/Select/{__tests__/context.test.tsx → context.test.tsx} +3 -3
  387. package/src/components/Select/{utils/__tests__/text.test.tsx → text.test.tsx} +2 -2
  388. package/src/components/Select/{utils/text.ts → text.ts} +1 -1
  389. package/src/components/Select/{hooks/__tests__/useSelectEvents.test.ts → useSelectEvents.test.ts} +5 -5
  390. package/src/components/Select/{hooks/useSelectEvents.ts → useSelectEvents.ts} +2 -2
  391. package/src/components/Select/{hooks/__tests__/useSelectSearch.test.tsx → useSelectSearch.test.tsx} +7 -7
  392. package/src/components/Select/{hooks/useSelectSearch.ts → useSelectSearch.ts} +2 -2
  393. package/src/components/Select/{hooks/__tests__/useSelectState.test.ts → useSelectState.test.ts} +16 -2
  394. package/src/components/Select/{hooks/useSelectState.ts → useSelectState.ts} +3 -3
  395. package/src/components/Table/Table.test.tsx +348 -0
  396. package/src/components/Tabs/Tabs.test.tsx +270 -0
  397. package/src/components/Tabs/Tabs.tsx +1 -1
  398. package/src/components/Toast/Toast.test.tsx +420 -0
  399. package/src/components/{__tests__/index.test.ts → index.test.ts} +2 -2
  400. package/src/constants/{__tests__/performance.test.ts → performance.test.ts} +2 -2
  401. package/src/hooks/{__tests__/ServiceHooks.test.tsx → ServiceHooks.test.tsx} +8 -8
  402. package/src/hooks/{__tests__/hooks.integration.test.tsx → hooks.integration.test.tsx} +11 -11
  403. package/src/hooks/index.ts +7 -4
  404. package/src/hooks/{__tests__/index.unit.test.ts → index.unit.test.ts} +2 -2
  405. package/src/hooks/public/usePublicEvent.test.ts +1 -1
  406. package/src/hooks/public/usePublicEventLogo.test.ts +1 -1
  407. package/src/hooks/public/usePublicRouteParams.test.ts +1 -1
  408. package/src/hooks/services/useAuth.ts +9 -7
  409. package/src/hooks/useAddressAutocomplete.test.ts +22 -22
  410. package/src/hooks/useAddressAutocomplete.ts +90 -75
  411. package/src/hooks/{__tests__/useAppConfig.unit.test.ts → useAppConfig.unit.test.ts} +328 -22
  412. package/src/hooks/{__tests__/useComponentPerformance.unit.test.tsx → useComponentPerformance.unit.test.tsx} +27 -41
  413. package/src/hooks/useDataTablePerformance.ts +100 -120
  414. package/src/hooks/{__tests__/useDataTablePerformance.unit.test.ts → useDataTablePerformance.unit.test.ts} +5 -5
  415. package/src/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +2 -2
  416. package/src/hooks/{__tests__/useDebounce.unit.test.ts → useDebounce.unit.test.ts} +2 -2
  417. package/src/hooks/useEventTheme.test.ts +4 -1
  418. package/src/hooks/useEventTheme.ts +49 -21
  419. package/src/hooks/useEvents.ts +41 -1
  420. package/src/hooks/{__tests__/useEvents.unit.test.ts → useEvents.unit.test.ts} +5 -5
  421. package/src/hooks/useFileReference.test.ts +44 -41
  422. package/src/hooks/useFileReference.ts +182 -173
  423. package/src/hooks/useFileUrl.ts +1 -1
  424. package/src/hooks/{__tests__/useFileUrl.unit.test.ts → useFileUrl.unit.test.ts} +26 -36
  425. package/src/hooks/{__tests__/useFileUrlCache.test.ts → useFileUrlCache.test.ts} +8 -8
  426. package/src/hooks/useFileUrlCache.ts +1 -1
  427. package/src/hooks/{__tests__/useFocusManagement.unit.test.ts → useFocusManagement.unit.test.ts} +2 -2
  428. package/src/hooks/{__tests__/useFocusTrap.unit.test.tsx → useFocusTrap.unit.test.tsx} +2 -2
  429. package/src/hooks/{__tests__/useFormDialog.test.ts → useFormDialog.test.ts} +2 -2
  430. package/src/hooks/useInactivityTracker.ts +138 -131
  431. package/src/hooks/{__tests__/useInactivityTracker.unit.test.ts → useInactivityTracker.unit.test.ts} +3 -3
  432. package/src/hooks/{__tests__/useIsMobile.unit.test.ts → useIsMobile.unit.test.ts} +2 -2
  433. package/src/hooks/useIsPrint.ts +62 -0
  434. package/src/hooks/useIsPrint.unit.test.ts +545 -0
  435. package/src/hooks/{__tests__/useKeyboardShortcuts.unit.test.ts → useKeyboardShortcuts.unit.test.ts} +2 -2
  436. package/src/hooks/{__tests__/useOrganisationPermissions.unit.test.tsx → useOrganisationPermissions.unit.test.tsx} +4 -4
  437. package/src/hooks/useOrganisationSecurity.test.ts +3 -3
  438. package/src/hooks/useOrganisationSecurity.ts +190 -201
  439. package/src/hooks/{__tests__/useOrganisationSecurity.unit.test.tsx → useOrganisationSecurity.unit.test.tsx} +61 -63
  440. package/src/hooks/{__tests__/useOrganisations.unit.test.ts → useOrganisations.unit.test.ts} +5 -5
  441. package/src/hooks/{__tests__/usePerformanceMonitor.unit.test.ts → usePerformanceMonitor.unit.test.ts} +13 -14
  442. package/src/hooks/{__tests__/usePermissionCache.test.ts → usePermissionCache.test.ts} +26 -27
  443. package/src/hooks/usePermissionCache.ts +276 -271
  444. package/src/hooks/{__tests__/usePreventTabReload.test.ts → usePreventTabReload.test.ts} +2 -2
  445. package/src/hooks/{__tests__/usePublicEvent.simple.test.ts → usePublicEvent.simple.test.ts} +4 -4
  446. package/src/hooks/{__tests__/usePublicEvent.test.ts → usePublicEvent.test.ts} +4 -4
  447. package/src/hooks/{__tests__/usePublicEvent.unit.test.ts → usePublicEvent.unit.test.ts} +4 -4
  448. package/src/hooks/{__tests__/usePublicFileDisplay.test.ts → usePublicFileDisplay.test.ts} +12 -12
  449. package/src/hooks/{__tests__/usePublicRouteParams.unit.test.ts → usePublicRouteParams.unit.test.ts} +3 -3
  450. package/src/hooks/{__tests__/useQueryCache.test.ts → useQueryCache.test.ts} +2 -2
  451. package/src/hooks/useQueryCache.ts +0 -2
  452. package/src/hooks/{__tests__/useRBAC.unit.test.ts → useRBAC.unit.test.ts} +55 -38
  453. package/src/hooks/{__tests__/useSessionDraft.test.ts → useSessionDraft.test.ts} +2 -2
  454. package/src/hooks/{__tests__/useSessionRestoration.unit.test.tsx → useSessionRestoration.unit.test.tsx} +10 -19
  455. package/src/hooks/useStorage.ts +21 -16
  456. package/src/hooks/{__tests__/useStorage.unit.test.ts → useStorage.unit.test.ts} +38 -75
  457. package/src/hooks/{__tests__/useToast.test.ts → useToast.test.ts} +2 -2
  458. package/src/hooks/{__tests__/useToast.unit.test.tsx → useToast.unit.test.tsx} +2 -2
  459. package/src/hooks/{__tests__/useZodForm.unit.test.tsx → useZodForm.unit.test.tsx} +2 -2
  460. package/src/icons/{__tests__/index.test.ts → index.test.ts} +2 -2
  461. package/src/icons/index.ts +2 -0
  462. package/src/{__tests__/index.test.ts → index.test.ts} +3 -7
  463. package/src/index.ts +15 -7
  464. package/src/providers/{__tests__/AuthProvider.test.tsx → AuthProvider.test.tsx} +3 -3
  465. package/src/providers/{__tests__/EventProvider.test.tsx → EventProvider.test.tsx} +3 -3
  466. package/src/providers/InactivityProvider.test-helper.tsx +40 -0
  467. package/src/providers/{__tests__/InactivityProvider.test.tsx → InactivityProvider.test.tsx} +14 -21
  468. package/src/providers/{__tests__/ProviderLifecycle.test.tsx → ProviderLifecycle.test.tsx} +4 -4
  469. package/src/providers/{__tests__/UnifiedAuthProvider.test.tsx → UnifiedAuthProvider.test.tsx} +1 -1
  470. package/src/providers/{__tests__/index.test.ts → index.test.ts} +2 -2
  471. package/src/providers/services/{__tests__/AuthServiceProvider.integration.test.tsx → AuthServiceProvider.integration.test.tsx} +4 -4
  472. package/src/providers/services/{__tests__/AuthServiceProvider.test.tsx → AuthServiceProvider.test.tsx} +7 -7
  473. package/src/providers/services/{__tests__/EventServiceProvider.test.tsx → EventServiceProvider.test.tsx} +7 -7
  474. package/src/providers/services/{__tests__/InactivityServiceProvider.test.tsx → InactivityServiceProvider.test.tsx} +5 -5
  475. package/src/providers/services/{__tests__/OrganisationServiceProvider.test.tsx → OrganisationServiceProvider.test.tsx} +6 -6
  476. package/src/providers/services/UnifiedAuthContext.ts +30 -27
  477. package/src/providers/services/{__tests__/UnifiedAuthProvider.advanced.test.tsx → UnifiedAuthProvider.advanced.test.tsx} +8 -9
  478. package/src/providers/services/{__tests__/UnifiedAuthProvider.appId.test.tsx → UnifiedAuthProvider.appId.test.tsx} +25 -25
  479. package/src/providers/services/{__tests__/UnifiedAuthProvider.integration.test.tsx → UnifiedAuthProvider.integration.test.tsx} +14 -11
  480. package/src/providers/services/UnifiedAuthProvider.tsx +115 -360
  481. package/src/providers/services/{__tests__/contexts.test.tsx → contexts.test.tsx} +6 -6
  482. package/src/providers/services/{__tests__/useUnifiedAuth.test.tsx → useUnifiedAuth.test.tsx} +6 -6
  483. package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
  484. package/src/providers/useInactivity.test-helper.ts +27 -0
  485. package/src/rbac/{__tests__/adapters.comprehensive.test.tsx → adapters.comprehensive.test.tsx} +24 -24
  486. package/src/rbac/adapters.test.tsx +22 -22
  487. package/src/rbac/adapters.tsx +29 -29
  488. package/src/rbac/api.test.ts +973 -42
  489. package/src/rbac/api.ts +228 -253
  490. package/src/rbac/{__tests__/audit-batched.test.ts → audit-batched.test.ts} +6 -6
  491. package/src/rbac/audit.ts +4 -1
  492. package/src/rbac/{__tests__/auth-rbac-security.integration.test.tsx → auth-rbac-security.integration.test.tsx} +1 -1
  493. package/src/rbac/{__tests__/auth-rbac.e2e.test.tsx → auth-rbac.e2e.test.tsx} +27 -34
  494. package/src/rbac/cache-invalidation.test.ts +715 -0
  495. package/src/rbac/components/{__tests__/AccessDenied.test.tsx → AccessDenied.test.tsx} +3 -3
  496. package/src/rbac/components/{__tests__/NavigationGuard.test.tsx → NavigationGuard.test.tsx} +13 -11
  497. package/src/{__tests__/rbac/PagePermissionGuard.test.tsx → rbac/components/PagePermissionGuard.guard.test.tsx} +33 -19
  498. package/src/rbac/components/{__tests__/PagePermissionGuard.performance.test.tsx → PagePermissionGuard.performance.test.tsx} +30 -9
  499. package/src/rbac/components/{__tests__/PagePermissionGuard.race-condition.test.tsx → PagePermissionGuard.race-condition.test.tsx} +7 -7
  500. package/src/rbac/components/{__tests__/PagePermissionGuard.test.tsx → PagePermissionGuard.test.tsx} +10 -10
  501. package/src/rbac/components/PagePermissionGuard.tsx +177 -372
  502. package/src/rbac/components/{__tests__/PagePermissionGuard.verification.test.tsx → PagePermissionGuard.verification.test.tsx} +7 -7
  503. package/src/rbac/config.ts +58 -18
  504. package/src/rbac/{__tests__/engine.comprehensive.test.ts → engine.comprehensive.test.ts} +3 -3
  505. package/src/rbac/engine.test.ts +494 -0
  506. package/src/rbac/errors.ts +89 -55
  507. package/src/rbac/hooks/permissions/runPermissionCheck.ts +77 -0
  508. package/src/rbac/hooks/permissions/{__tests__/useAccessLevel.test.ts → useAccessLevel.test.ts} +40 -40
  509. package/src/rbac/hooks/permissions/useAccessLevel.ts +16 -6
  510. package/src/rbac/hooks/permissions/{__tests__/useCan.test.ts → useCan.test.ts} +41 -41
  511. package/src/rbac/hooks/permissions/useCan.ts +170 -252
  512. package/src/rbac/hooks/permissions/{__tests__/useMultiplePermissions.test.ts → useMultiplePermissions.test.ts} +49 -49
  513. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +6 -2
  514. package/src/rbac/hooks/permissions/{__tests__/usePermissions.test.ts → usePermissions.test.ts} +10 -12
  515. package/src/rbac/hooks/permissions/usePermissions.ts +36 -65
  516. package/src/rbac/hooks/useCan.test.ts +42 -42
  517. package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
  518. package/src/rbac/hooks/usePageGuardScope.ts +117 -0
  519. package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
  520. package/src/rbac/hooks/{__tests__/usePermissions.integration.test.ts → usePermissions.integration.test.ts} +9 -9
  521. package/src/{__tests__/hooks/usePermissions.test.ts → rbac/hooks/usePermissions.stability.test.ts} +18 -18
  522. package/src/rbac/hooks/usePermissions.test.ts +54 -54
  523. package/src/rbac/hooks/useRBAC.test.ts +313 -217
  524. package/src/rbac/hooks/useRBAC.ts +145 -81
  525. package/src/rbac/hooks/useResourcePermissions.test.ts +25 -25
  526. package/src/rbac/hooks/useResourcePermissions.ts +68 -134
  527. package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
  528. package/src/rbac/hooks/useRoleManagement.test.ts +27 -112
  529. package/src/rbac/hooks/useRoleManagement.ts +153 -585
  530. package/src/rbac/hooks/{__tests__/useSecureSupabase.test.ts → useSecureSupabase.test.ts} +17 -17
  531. package/src/rbac/hooks/useSecureSupabase.ts +10 -2
  532. package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
  533. package/src/rbac/{__tests__/performance.test.ts → performance.test.ts} +1 -1
  534. package/src/rbac/{__tests__/rbac-core.test.tsx → rbac-core.test.tsx} +3 -3
  535. package/src/rbac/{__tests__/rbac-engine-core-logic.test.ts → rbac-engine-core-logic.test.ts} +2 -2
  536. package/src/rbac/{__tests__/rbac-engine-simplified.test.ts → rbac-engine-simplified.test.ts} +3 -3
  537. package/src/rbac/{__tests__/rbac-functions.test.ts → rbac-functions.test.ts} +57 -0
  538. package/src/rbac/{__tests__/rbac-role-isolation.test.ts → rbac-role-isolation.test.ts} +2 -2
  539. package/src/rbac/request-deduplication.test.ts +14 -9
  540. package/src/rbac/request-deduplication.ts +5 -4
  541. package/src/rbac/{__tests__/scenarios.user-role.test.tsx → scenarios.user-role.test.tsx} +23 -23
  542. package/src/rbac/secureClient.test.ts +514 -83
  543. package/src/rbac/secureClient.ts +8 -2
  544. package/src/rbac/security.test.ts +323 -0
  545. package/src/rbac/types/roleManagement.ts +66 -0
  546. package/src/rbac/utils/{__tests__/clientSecurity.test.ts → clientSecurity.test.ts} +4 -4
  547. package/src/rbac/utils/{__tests__/contextValidator.test.ts → contextValidator.test.ts} +4 -4
  548. package/src/rbac/utils/contextValidator.ts +5 -1
  549. package/src/rbac/utils/{__tests__/deep-equal.test.ts → deep-equal.test.ts} +1 -1
  550. package/src/rbac/utils/{__tests__/eventContext.test.ts → eventContext.test.ts} +36 -21
  551. package/src/rbac/utils/eventContext.ts +37 -33
  552. package/src/rbac/utils/fetchPermissionMap.ts +13 -0
  553. package/src/rbac/utils/permissionMapHelpers.ts +34 -0
  554. package/src/rbac/utils/roleManagementRpc.ts +303 -0
  555. package/src/services/{__tests__/AuthService.edge-cases.test.ts → AuthService.edge-cases.test.ts} +19 -19
  556. package/src/services/{__tests__/AuthService.restoreSession.test.ts → AuthService.restoreSession.test.ts} +2 -2
  557. package/src/services/{__tests__/AuthService.test.ts → AuthService.test.ts} +89 -55
  558. package/src/services/AuthService.ts +184 -205
  559. package/src/services/{__tests__/BaseService.edge-cases.test.ts → BaseService.edge-cases.test.ts} +3 -3
  560. package/src/services/{__tests__/BaseService.test.ts → BaseService.test.ts} +2 -2
  561. package/src/services/{__tests__/EventService.edge-cases.test.ts → EventService.edge-cases.test.ts} +27 -24
  562. package/src/services/{__tests__/EventService.eventColours.test.ts → EventService.eventColours.test.ts} +1 -1
  563. package/src/services/{__tests__/EventService.test.ts → EventService.test.ts} +256 -24
  564. package/src/services/EventService.ts +242 -312
  565. package/src/services/{__tests__/InactivityService.edge-cases.test.ts → InactivityService.edge-cases.test.ts} +3 -3
  566. package/src/services/{__tests__/InactivityService.lifecycle.test.ts → InactivityService.lifecycle.test.ts} +2 -2
  567. package/src/services/{__tests__/InactivityService.test.ts → InactivityService.test.ts} +179 -4
  568. package/src/services/InactivityService.ts +172 -213
  569. package/src/services/{__tests__/OrganisationService.edge-cases.test.ts → OrganisationService.edge-cases.test.ts} +5 -5
  570. package/src/services/{__tests__/OrganisationService.pagination.test.ts → OrganisationService.pagination.test.ts} +4 -4
  571. package/src/services/{__tests__/OrganisationService.test.ts → OrganisationService.test.ts} +410 -7
  572. package/src/services/OrganisationService.ts +184 -238
  573. package/src/services/base/BaseService.test.ts +1 -1
  574. package/src/services/interfaces/{__tests__/IAuthService.test.ts → IAuthService.test.ts} +21 -27
  575. package/src/services/interfaces/IAuthService.ts +10 -9
  576. package/src/services/interfaces/{__tests__/IEventService.test.ts → IEventService.test.ts} +4 -4
  577. package/src/services/interfaces/{__tests__/IInactivityService.test.ts → IInactivityService.test.ts} +3 -3
  578. package/src/services/interfaces/{__tests__/IOrganisationService.test.ts → IOrganisationService.test.ts} +3 -3
  579. package/src/styles/core.css +243 -12
  580. package/src/theming/{__tests__/parseEventColours.test.ts → parseEventColours.test.ts} +1 -1
  581. package/src/theming/{__tests__/runtime.test.ts → runtime.test.ts} +8 -17
  582. package/src/theming/runtime.ts +71 -2
  583. package/src/types/api-result.ts +53 -0
  584. package/src/types/{__tests__/core.test.ts → core.test.ts} +2 -2
  585. package/src/types/{__tests__/database-generated.test.ts → database-generated.test.ts} +3 -3
  586. package/src/types/database.generated.ts +45 -10
  587. package/src/types/event.ts +38 -18
  588. package/src/types/{__tests__/file-reference.test.ts → file-reference.test.ts} +13 -13
  589. package/src/types/file-reference.ts +37 -12
  590. package/src/types/{__tests__/guards.test.ts → guards.test.ts} +2 -2
  591. package/src/types/{__tests__/index.test.ts → index.test.ts} +2 -2
  592. package/src/types/index.ts +3 -0
  593. package/src/types/{__tests__/organisation.roles.test.ts → organisation.roles.test.ts} +1 -1
  594. package/src/types/{__tests__/organisation.test.ts → organisation.test.ts} +3 -31
  595. package/src/types/organisation.ts +15 -15
  596. package/src/types/supabase.ts +13 -4
  597. package/src/types/{__tests__/theme.test.ts → theme.test.ts} +1 -1
  598. package/src/types/{__tests__/type-validation.test.ts → type-validation.test.ts} +1 -1
  599. package/src/types/{__tests__/validation.test.ts → validation.test.ts} +2 -2
  600. package/src/utils/app/appIdResolver.test.ts +98 -71
  601. package/src/utils/app/appIdResolver.ts +31 -20
  602. package/src/utils/{__tests__/appConfig.unit.test.ts → appConfig.unit.test.ts} +1 -1
  603. package/src/utils/{__tests__/audit.unit.test.ts → audit.unit.test.ts} +1 -1
  604. package/src/utils/{__tests__/auth-utils.unit.test.ts → auth-utils.unit.test.ts} +16 -17
  605. package/src/utils/{__tests__/bundleAnalysis.unit.test.ts → bundleAnalysis.unit.test.ts} +35 -35
  606. package/src/utils/{__tests__/cn.unit.test.ts → cn.unit.test.ts} +1 -1
  607. package/src/utils/context/organisationContext.test.ts +105 -91
  608. package/src/utils/context/organisationContext.ts +29 -40
  609. package/src/utils/core/{__tests__/cn.test.ts → cn.test.ts} +3 -3
  610. package/src/utils/core/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +2 -2
  611. package/src/utils/core/{__tests__/logger.test.ts → logger.test.ts} +2 -2
  612. package/src/utils/core/mergeRefs.ts +24 -0
  613. package/src/utils/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +1 -1
  614. package/src/utils/{__tests__/deviceFingerprint.unit.test.ts → deviceFingerprint.unit.test.ts} +1 -1
  615. package/src/utils/dynamic/createLazyComponent.tsx +9 -1
  616. package/src/utils/dynamic/{__tests__/dynamicUtils.test.ts → dynamicUtils.test.ts} +2 -2
  617. package/src/utils/dynamic/{__tests__/lazyLoad.test.tsx → lazyLoad.test.tsx} +2 -2
  618. package/src/utils/{__tests__/dynamicUtils.unit.test.ts → dynamicUtils.unit.test.ts} +1 -1
  619. package/src/utils/file-reference/{__tests__/file-reference.test.ts → file-reference.test.ts} +214 -289
  620. package/src/utils/file-reference/index.ts +330 -347
  621. package/src/utils/{__tests__/formatDate.unit.test.ts → formatDate.unit.test.ts} +2 -2
  622. package/src/utils/formatting/formatDateTimeTimezone.test.ts +1 -1
  623. package/src/utils/formatting/formatNumber.test.ts +1 -1
  624. package/src/utils/{__tests__/formatting.unit.test.ts → formatting.unit.test.ts} +1 -1
  625. package/src/utils/google-places/googlePlacesUtils.test.ts +70 -48
  626. package/src/utils/google-places/googlePlacesUtils.ts +67 -99
  627. package/src/utils/google-places/loadGoogleMapsScript.test.ts +25 -22
  628. package/src/utils/google-places/loadGoogleMapsScript.ts +138 -117
  629. package/src/utils/{__tests__/index.unit.test.ts → index.unit.test.ts} +1 -1
  630. package/src/utils/{__tests__/lazyLoad.unit.test.tsx → lazyLoad.unit.test.tsx} +13 -14
  631. package/src/utils/location/location.test.ts +1 -1
  632. package/src/utils/{__tests__/logger.unit.test.ts → logger.unit.test.ts} +1 -1
  633. package/src/utils/{__tests__/organisationContext.unit.test.ts → organisationContext.unit.test.ts} +37 -48
  634. package/src/utils/performance/{__tests__/bundleAnalysis.test.ts → bundleAnalysis.test.ts} +2 -2
  635. package/src/utils/performance/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
  636. package/src/utils/performance/{__tests__/performanceBudgets.test.ts → performanceBudgets.test.ts} +2 -2
  637. package/src/utils/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
  638. package/src/utils/{__tests__/performanceBudgets.unit.test.ts → performanceBudgets.unit.test.ts} +2 -2
  639. package/src/utils/{__tests__/permissionTypes.unit.test.ts → permissionTypes.unit.test.ts} +1 -1
  640. package/src/utils/{__tests__/permissionUtils.unit.test.ts → permissionUtils.unit.test.ts} +1 -1
  641. package/src/utils/permissions/{__tests__/permissionTypes.test.ts → permissionTypes.test.ts} +2 -2
  642. package/src/utils/persistence/{__tests__/keyDerivation.test.ts → keyDerivation.test.ts} +2 -2
  643. package/src/utils/persistence/{__tests__/sensitiveFieldDetection.test.ts → sensitiveFieldDetection.test.ts} +2 -2
  644. package/src/utils/{__tests__/request-deduplication.test.ts → request-deduplication.test.ts} +2 -2
  645. package/src/utils/{__tests__/sanitization.unit.test.ts → sanitization.unit.test.ts} +1 -1
  646. package/src/utils/{__tests__/schemaUtils.unit.test.ts → schemaUtils.unit.test.ts} +1 -1
  647. package/src/utils/{__tests__/secureDataAccess.unit.test.ts → secureDataAccess.unit.test.ts} +2 -2
  648. package/src/utils/{__tests__/secureErrors.unit.test.ts → secureErrors.unit.test.ts} +4 -4
  649. package/src/utils/{__tests__/secureStorage.unit.test.ts → secureStorage.unit.test.ts} +1 -1
  650. package/src/utils/security/auth-utils.ts +34 -23
  651. package/src/utils/security/secureDataAccess.ts +241 -281
  652. package/src/utils/security/secureErrors.test.ts +1 -1
  653. package/src/utils/security/secureStorage.test.ts +1 -1
  654. package/src/utils/security/security.test.ts +25 -17
  655. package/src/utils/security/security.ts +15 -18
  656. package/src/utils/security/securityMonitor.test.ts +1 -1
  657. package/src/utils/{__tests__/security.unit.test.ts → security.unit.test.ts} +21 -15
  658. package/src/utils/{__tests__/securityMonitor.unit.test.ts → securityMonitor.unit.test.ts} +1 -1
  659. package/src/utils/{__tests__/sessionTracking.unit.test.ts → sessionTracking.unit.test.ts} +12 -12
  660. package/src/utils/storage/{__tests__/config.unit.test.ts → config.unit.test.ts} +2 -2
  661. package/src/utils/storage/helpers.test.ts +88 -102
  662. package/src/utils/storage/helpers.ts +173 -251
  663. package/src/utils/storage/{__tests__/index.unit.test.ts → index.unit.test.ts} +3 -3
  664. package/src/utils/storage/types.ts +7 -0
  665. package/src/utils/supabase/createBaseClient.test.ts +1 -1
  666. package/src/utils/timezone/timezone.test.ts +1 -1
  667. package/src/utils/{__tests__/timezone.test.ts → timezone.test.ts} +2 -2
  668. package/src/utils/validation/{__tests__/common.test.ts → common.test.ts} +2 -2
  669. package/src/utils/validation/{__tests__/csrf.test.ts → csrf.test.ts} +56 -28
  670. package/src/utils/validation/csrf.ts +42 -41
  671. package/src/utils/validation/{__tests__/htmlSanitization.unit.test.ts → htmlSanitization.unit.test.ts} +2 -2
  672. package/src/utils/validation/{__tests__/passwordSchema.test.ts → passwordSchema.test.ts} +2 -2
  673. package/src/utils/validation/{__tests__/schema.test.ts → schema.test.ts} +2 -2
  674. package/src/utils/validation/{__tests__/sqlInjectionProtection.test.ts → sqlInjectionProtection.test.ts} +2 -2
  675. package/src/utils/validation/{__tests__/user.test.ts → user.test.ts} +2 -2
  676. package/src/utils/validation/{__tests__/validation.test.ts → validation.test.ts} +2 -2
  677. package/src/utils/validation/{__tests__/validationUtils.test.ts → validationUtils.test.ts} +2 -2
  678. package/src/utils/{__tests__/validation.unit.test.ts → validation.unit.test.ts} +1 -1
  679. package/src/utils/{__tests__/validationUtils.unit.test.ts → validationUtils.unit.test.ts} +5 -2
  680. package/dist/UnifiedAuthProvider-BBD2PS3Q.js +0 -7
  681. package/dist/chunk-KPYQWGFQ.js +0 -183
  682. package/dist/types-D05dCGma.d.ts +0 -521
  683. package/scripts/eslint-audit.cjs +0 -222
  684. package/scripts/generate-docs.js +0 -157
  685. package/scripts/install-cursor-rules.cjs +0 -255
  686. package/scripts/install-eslint-config.cjs +0 -349
  687. package/scripts/setup-build-cache.js +0 -73
  688. package/scripts/validate-pre-publish.js +0 -145
  689. package/src/__tests__/integration/UserProfile.test.tsx +0 -124
  690. package/src/__tests__/public-recipe-view.test.ts +0 -228
  691. package/src/__tests__/rls-policies.test.ts +0 -472
  692. package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
  693. package/src/components/DataTable/components/DataTableLayout.tsx +0 -584
  694. package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -395
  695. package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +0 -467
  696. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -358
  697. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -957
  698. package/src/components/DataTable/core/ActionManager.ts +0 -235
  699. package/src/components/DataTable/core/ColumnManager.ts +0 -204
  700. package/src/components/DataTable/core/DataManager.ts +0 -190
  701. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
  702. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  703. package/src/components/DataTable/core/StateManager.ts +0 -312
  704. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -235
  705. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -141
  706. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -178
  707. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -133
  708. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -142
  709. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -158
  710. package/src/components/DataTable/core/interfaces.ts +0 -338
  711. package/src/components/DataTable/types.ts +0 -764
  712. package/src/hooks/public/usePublicFileDisplay.ts +0 -534
  713. package/src/hooks/useFileDisplay.ts +0 -748
  714. package/src/providers/OrganisationProvider.test.tsx +0 -40
  715. package/src/providers/OrganisationProvider.tsx +0 -92
  716. package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
  717. package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -616
  718. package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +0 -591
  719. package/src/rbac/__tests__/cache-invalidation.test.ts +0 -393
  720. /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
  721. /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
  722. /package/src/hooks/{__tests__/useApiFetch.unit.test.ts → useApiFetch.unit.test.ts} +0 -0
  723. /package/src/providers/{__tests__/README.md → README.md} +0 -0
  724. /package/src/rbac/{__tests__/index.test.ts → index.test.ts} +0 -0
  725. /package/src/rbac/{__tests__/rbac-integration.test.ts → rbac-integration.test.ts} +0 -0
  726. /package/src/types/{__tests__/README.md → README.md} +0 -0
@@ -8,16 +8,57 @@ import {
8
8
  FileReferenceService,
9
9
  FileUploadResult,
10
10
  FileCategory,
11
- FileMetadata
11
+ FileMetadata,
12
+ BulkUploadResult
12
13
  } from '../../types/file-reference';
14
+ import type { Database } from '../../types/database';
15
+ import { ok, err, type ApiResult, type ApiError } from '../../types/api-result';
13
16
  import { uploadFile, getPublicUrl, getSignedUrl, deleteFile, extractFileMetadata } from '../storage/helpers';
14
17
  import { setOrganisationContext } from '../context/organisationContext';
15
- import { invalidateFileDisplayCache } from '../../hooks/useFileDisplay';
18
+ import { invalidateFileDisplayCache } from '../../components/FileDisplay/useFileDisplay';
16
19
  import { createLogger } from '../core/logger';
17
20
  import { assertAppId } from '../../types/core';
18
21
 
19
22
  const log = createLogger('FileReferenceService');
20
23
 
24
+ function toApiError(error: unknown): ApiError {
25
+ const message = error instanceof Error ? error.message : 'Unknown error';
26
+ const code = error instanceof Error && (error as Error & { code?: string }).code
27
+ ? (error as Error & { code: string }).code
28
+ : 'FILE_REFERENCE_ERROR';
29
+ return { code, message };
30
+ }
31
+
32
+ /** Row type from core_file_references table */
33
+ export type CoreFileReferencesRow = Database['public']['Tables']['core_file_references']['Row'];
34
+
35
+ /**
36
+ * Maps a core_file_references DB row to FileReference.
37
+ * Used by useFileDisplay and fetchFileDisplayData when reading from direct queries.
38
+ */
39
+ export function mapFileReferenceRowToFileReference(f: CoreFileReferencesRow): FileReference {
40
+ const fileName = f.file_path.split('/').pop() || 'unknown';
41
+ const fileType = fileName.split('.').pop() || 'unknown';
42
+ const metadata = f.file_metadata as { fileName?: string; fileType?: string; category?: FileCategory; [key: string]: unknown } | null;
43
+ return {
44
+ id: f.id,
45
+ table_name: f.table_name,
46
+ record_id: f.record_id,
47
+ file_path: f.file_path,
48
+ file_metadata: {
49
+ fileName: metadata?.fileName || fileName,
50
+ fileType: metadata?.fileType || fileType,
51
+ category: metadata?.category || FileCategory.GENERAL_DOCUMENTS,
52
+ ...(metadata || {})
53
+ } as FileReference['file_metadata'],
54
+ organisation_id: f.organisation_id,
55
+ app_id: f.app_id as FileReference['app_id'],
56
+ is_public: f.is_public ?? false,
57
+ created_at: f.created_at || new Date().toISOString(),
58
+ updated_at: f.updated_at || new Date().toISOString()
59
+ };
60
+ }
61
+
21
62
  export class FileReferenceServiceImpl implements FileReferenceService {
22
63
  constructor(private supabase: SupabaseClient) {}
23
64
 
@@ -31,17 +72,163 @@ export class FileReferenceServiceImpl implements FileReferenceService {
31
72
  .select('name')
32
73
  .eq('id', appId)
33
74
  .single();
34
-
75
+
35
76
  if (error || !data) {
36
77
  return 'unknown';
37
78
  }
38
-
79
+
39
80
  return data.name || 'unknown';
40
81
  } catch {
41
82
  return 'unknown';
42
83
  }
43
84
  }
44
85
 
86
+ private validateCreateOptions(options: FileUploadOptions): ApiResult<{ isUserScoped: boolean }> {
87
+ const isUserScoped = !options.organisation_id && !!options.userId;
88
+ if (!isUserScoped && !options.organisation_id) {
89
+ return err({ code: 'VALIDATION_ERROR', message: 'organisation_id is required for file upload, or userId must be provided for user-scoped files' });
90
+ }
91
+ if (!options.table_name) {
92
+ return err({ code: 'VALIDATION_ERROR', message: 'table_name is required for file upload' });
93
+ }
94
+ if (!options.record_id) {
95
+ return err({ code: 'VALIDATION_ERROR', message: 'record_id is required for file upload' });
96
+ }
97
+ if (!options.folder) {
98
+ return err({ code: 'VALIDATION_ERROR', message: 'folder is required for file upload. The folder prop determines the storage path.' });
99
+ }
100
+ return ok({ isUserScoped });
101
+ }
102
+
103
+ private async resolveAuthenticatedUserIdForUserScoped(): Promise<ApiResult<string>> {
104
+ const { data: { user: authUser }, error: authError } = await this.supabase.auth.getUser();
105
+ if (authError || !authUser) {
106
+ return err({ code: 'UNAUTHORIZED', message: 'User must be authenticated to upload user-scoped files' });
107
+ }
108
+ log.debug('Using authenticated user ID for user-scoped file upload', { userId: authUser.id });
109
+ return ok(authUser.id);
110
+ }
111
+
112
+ private async checkSuperAdminUser(userId: string | undefined): Promise<boolean> {
113
+ if (!userId) return false;
114
+ try {
115
+ const { isSuperAdmin } = await import('../../rbac/api');
116
+ const superResult = await isSuperAdmin(userId);
117
+ if (superResult.ok && superResult.data) {
118
+ log.debug('Super admin detected - bypassing permission checks', { userId });
119
+ return true;
120
+ }
121
+ return false;
122
+ } catch (superAdminCheckError) {
123
+ log.warn('Failed to check super-admin status, proceeding with normal permission checks', superAdminCheckError);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ private async uploadFileAndExtractMetadata(
129
+ options: FileUploadOptions,
130
+ file: File,
131
+ authenticatedUserId: string | undefined,
132
+ isUserScoped: boolean
133
+ ): Promise<ApiResult<{ path: string; metadata: Record<string, unknown> }>> {
134
+ const userId = authenticatedUserId || (isUserScoped ? undefined : options.userId);
135
+ const uploadResult = await uploadFile(this.supabase, file, {
136
+ appName: 'file-reference',
137
+ orgId: options.organisation_id || undefined,
138
+ userId,
139
+ isPublic: options.is_public || false,
140
+ customPath: options.folder
141
+ });
142
+ if (!uploadResult.ok) return err(uploadResult.error);
143
+ const metadataResult = await extractFileMetadata(file, {
144
+ appName: 'file-reference',
145
+ orgId: options.organisation_id || undefined,
146
+ userId,
147
+ isPublic: options.is_public || false
148
+ }, 'system');
149
+ if (!metadataResult.ok) return err(metadataResult.error);
150
+ return ok({ path: uploadResult.data.path, metadata: metadataResult.data as unknown as Record<string, unknown> });
151
+ }
152
+
153
+ private async setOrgContextIfNeeded(isUserScoped: boolean, organisation_id: string | undefined): Promise<void> {
154
+ if (isUserScoped || !organisation_id) return;
155
+ const ctxResult = await setOrganisationContext(this.supabase, organisation_id);
156
+ if (!ctxResult.ok) {
157
+ log.warn('setOrganisationContext failed (non-fatal):', ctxResult.error.message);
158
+ }
159
+ }
160
+
161
+ private async resolveRpcUserId(
162
+ authenticatedUserId: string | undefined,
163
+ options: FileUploadOptions
164
+ ): Promise<string | null> {
165
+ if (authenticatedUserId) return authenticatedUserId;
166
+ if (options.userId) return options.userId;
167
+ const { data: { user: authUser } } = await this.supabase.auth.getUser();
168
+ return authUser?.id ?? null;
169
+ }
170
+
171
+ private async buildPermissionDeniedMessage(
172
+ options: FileUploadOptions,
173
+ isSuperAdminUser: boolean
174
+ ): Promise<string> {
175
+ const appName = await this.getAppName(options.app_id).catch(() => 'unknown');
176
+ const pageContextDisplay = options.pageContext || 'undefined';
177
+ return isSuperAdminUser
178
+ ? `File upload failed for super-admin user. Page context: '${pageContextDisplay}', App: '${appName}'.`
179
+ : `File upload denied: insufficient permissions. You need 'create:page.${pageContextDisplay}' or 'update:page.${pageContextDisplay}' for the '${pageContextDisplay}' page.`;
180
+ }
181
+
182
+ private async rollbackUploadAndErr(
183
+ filePath: string,
184
+ isPublic: boolean,
185
+ code: string,
186
+ message: string
187
+ ): Promise<ApiResult<FileReference>> {
188
+ await deleteFile(this.supabase, filePath, isPublic);
189
+ return err({ code, message });
190
+ }
191
+
192
+ private async fetchCreatedFileReference(createdId: string): Promise<ApiResult<FileReference>> {
193
+ const { data: fileRef, error: fetchError } = await this.supabase
194
+ .from('core_file_references')
195
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
196
+ .eq('id', createdId)
197
+ .single();
198
+ if (fetchError || !fileRef) {
199
+ return err({ code: 'FETCH_FAILED', message: fetchError?.message ?? 'Failed to fetch created file reference' });
200
+ }
201
+ return ok(fileRef as FileReference);
202
+ }
203
+
204
+ private buildCreateRpcPayload(
205
+ options: FileUploadOptions,
206
+ filePath: string,
207
+ file: File,
208
+ metadata: Record<string, unknown>,
209
+ rpcUserId: string | null
210
+ ): Record<string, unknown> {
211
+ return {
212
+ p_table_name: options.table_name,
213
+ p_record_id: options.record_id,
214
+ p_file_path: filePath,
215
+ p_organisation_id: options.organisation_id ?? null,
216
+ p_app_id: options.app_id,
217
+ p_page_context: options.pageContext,
218
+ p_event_id: options.event_id || null,
219
+ p_file_metadata: {
220
+ fileName: file.name,
221
+ fileType: file.type,
222
+ fileSize: file.size,
223
+ category: options.category,
224
+ ...metadata,
225
+ ...options.custom_metadata
226
+ },
227
+ p_is_public: options.is_public || false,
228
+ p_user_id: rpcUserId
229
+ };
230
+ }
231
+
45
232
  /**
46
233
  * Creates a file reference by uploading a file to storage and linking it in the database.
47
234
  *
@@ -56,175 +243,50 @@ export class FileReferenceServiceImpl implements FileReferenceService {
56
243
  *
57
244
  * This ensures atomicity: either both storage and DB succeed, or both are cleaned up.
58
245
  */
59
- async createFileReference(options: FileUploadOptions, file: File): Promise<FileReference> {
246
+ async createFileReference(options: FileUploadOptions, file: File): Promise<ApiResult<FileReference>> {
60
247
  try {
248
+ const validation = this.validateCreateOptions(options);
249
+ if (!validation.ok) return validation;
250
+ const { isUserScoped } = validation.data;
61
251
 
62
- // organisation_id is optional for user-scoped files (e.g., profile photos)
63
- const isUserScoped = !options.organisation_id && options.userId;
64
- if (!isUserScoped && !options.organisation_id) {
65
- throw new Error('organisation_id is required for file upload, or userId must be provided for user-scoped files');
66
- }
67
- if (!options.table_name) {
68
- throw new Error('table_name is required for file upload');
69
- }
70
- if (!options.record_id) {
71
- throw new Error('record_id is required for file upload');
72
- }
73
- if (!options.folder) {
74
- throw new Error('folder is required for file upload. The folder prop determines the storage path.');
75
- }
76
-
77
- // For user-scoped files, we MUST use auth.uid() for the path to match RLS policies
78
- // Get the authenticated user ID from the Supabase session
79
- let authenticatedUserId: string | undefined = undefined;
252
+ let authenticatedUserId: string | undefined;
80
253
  if (isUserScoped) {
81
- const { data: { user: authUser }, error: authError } = await this.supabase.auth.getUser();
82
- if (authError || !authUser) {
83
- throw new Error('User must be authenticated to upload user-scoped files');
84
- }
85
- authenticatedUserId = authUser.id;
86
- log.debug('Using authenticated user ID for user-scoped file upload', { userId: authenticatedUserId });
254
+ const authResult = await this.resolveAuthenticatedUserIdForUserScoped();
255
+ if (!authResult.ok) return authResult;
256
+ authenticatedUserId = authResult.data;
87
257
  }
88
258
 
89
- // CRITICAL: Check super admin status in application layer (consistent with pace-core pattern)
90
- // Super admins bypass all permission checks - this is handled in the application layer,
91
- // not in RLS policies. The RPC function still validates input and handles the insert,
92
- // but permission checks are bypassed for super admins.
93
- let isSuperAdminUser = false;
94
- const userIdForCheck = authenticatedUserId || options.userId;
95
- if (userIdForCheck) {
96
- try {
97
- // Import isSuperAdmin from rbac/api - this is the standard way to check super admin
98
- const { isSuperAdmin } = await import('../../rbac/api');
99
- isSuperAdminUser = await isSuperAdmin(userIdForCheck);
100
- if (isSuperAdminUser) {
101
- log.debug('Super admin detected - bypassing permission checks', { userId: userIdForCheck });
102
- }
103
- } catch (superAdminCheckError) {
104
- // If super admin check fails, continue with normal permission flow
105
- log.warn('Failed to check super-admin status, proceeding with normal permission checks', superAdminCheckError);
106
- }
107
- }
259
+ const isSuperAdminUser = await this.checkSuperAdminUser(authenticatedUserId || options.userId);
108
260
 
109
- // Step 1: Upload file to storage bucket first
110
- // This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename} or users/{auth.uid()}/{folder}/{timestamp-uuid-filename}
111
- // Bucket is automatically selected based on is_public flag
112
- const uploadResult = await uploadFile(this.supabase, file, {
113
- appName: 'file-reference',
114
- orgId: options.organisation_id || undefined,
115
- userId: authenticatedUserId || (isUserScoped ? undefined : options.userId), // Use auth.uid() for user-scoped files
116
- isPublic: options.is_public || false,
117
- customPath: options.folder // Use folder prop as the custom path segment
118
- });
119
- if (!uploadResult.success) {
120
- throw new Error(`Failed to upload file: ${uploadResult.error}`);
121
- }
261
+ const uploadResult = await this.uploadFileAndExtractMetadata(options, file, authenticatedUserId, isUserScoped);
262
+ if (!uploadResult.ok) return uploadResult;
263
+ const { path: filePath, metadata } = uploadResult.data;
122
264
 
123
- if (!uploadResult.path) {
124
- throw new Error('File upload did not return a path');
125
- }
265
+ await this.setOrgContextIfNeeded(isUserScoped, options.organisation_id ?? undefined);
126
266
 
127
- const filePath = uploadResult.path;
267
+ const rpcUserId = await this.resolveRpcUserId(authenticatedUserId, options);
128
268
 
129
- // Step 2: Extract file metadata (dimensions, hash, etc.)
130
- const metadata = await extractFileMetadata(file, {
131
- appName: 'file-reference',
132
- orgId: options.organisation_id || undefined,
133
- userId: authenticatedUserId || (isUserScoped ? undefined : options.userId), // Use auth.uid() for user-scoped files
134
- isPublic: options.is_public || false
135
- }, 'system');
136
-
137
- // Step 3: Set organisation context in database session before creating file reference
138
- // Skip for user-scoped files (no org context needed)
139
- if (!isUserScoped && options.organisation_id) {
140
- await setOrganisationContext(this.supabase, options.organisation_id);
141
- }
269
+ const payload = this.buildCreateRpcPayload(options, filePath, file, metadata, rpcUserId);
270
+ const { data, error } = await this.supabase.rpc('data_file_reference_create', payload as Parameters<SupabaseClient['rpc']>[1] & object);
142
271
 
143
- // Step 4: Create file reference in database using RPC function
144
- // This links the storage path to the record in core_file_references table
145
- // CRITICAL: Always pass the authenticated user ID to SECURITY DEFINER functions
146
- // In SECURITY DEFINER functions, auth.uid() returns the function owner's ID,
147
- // not the caller's ID, so we must explicitly pass the user ID
148
- let rpcUserId: string | null = null;
149
- if (authenticatedUserId) {
150
- rpcUserId = authenticatedUserId;
151
- } else if (options.userId) {
152
- rpcUserId = options.userId;
153
- } else {
154
- // Get authenticated user ID from session as fallback
155
- const { data: { user: authUser } } = await this.supabase.auth.getUser();
156
- if (authUser) {
157
- rpcUserId = authUser.id;
158
- }
159
- }
160
-
161
- const { data, error } = await this.supabase
162
- .rpc('data_file_reference_create', {
163
- p_table_name: options.table_name,
164
- p_record_id: options.record_id,
165
- p_file_path: filePath, // Storage path from step 1
166
- p_organisation_id: options.organisation_id ?? null,
167
- p_app_id: options.app_id,
168
- p_page_context: options.pageContext,
169
- p_event_id: options.event_id || null, // Pass event_id for event-based apps
170
- p_file_metadata: {
171
- fileName: file.name,
172
- fileType: file.type,
173
- fileSize: file.size,
174
- category: options.category,
175
- ...metadata,
176
- ...options.custom_metadata
177
- },
178
- p_is_public: options.is_public || false,
179
- p_user_id: rpcUserId // Always pass authenticated user ID for SECURITY DEFINER functions
180
- });
181
-
182
- // Step 5: Rollback - if database insert fails, clean up uploaded file
183
272
  if (error) {
184
- await deleteFile(this.supabase, filePath, options.is_public || false);
185
- throw new Error(`Failed to create file reference: ${error.message}`);
273
+ return await this.rollbackUploadAndErr(filePath, options.is_public ?? false, 'CREATE_FAILED', error.message);
186
274
  }
187
-
188
- // Check if RPC returned null (permission denied or other failure)
189
275
  if (!data || data === null) {
190
- // Clean up the uploaded file since DB insert failed
191
- await deleteFile(this.supabase, filePath, options.is_public || false);
192
-
193
- // Provide more helpful error message
194
- const appName = await this.getAppName(options.app_id).catch(() => 'unknown');
195
- const pageContextDisplay = options.pageContext || 'undefined';
196
-
197
- if (isSuperAdminUser) {
198
- // Super-admin should have been allowed - this suggests a database or RPC function issue
199
- // Since we already checked super admin in the application layer, this is unexpected
200
- throw new Error(
201
- `File upload failed for super-admin user. This may indicate a database issue. ` +
202
- `Page context: '${pageContextDisplay}', App: '${appName}'. ` +
203
- `Please check that the page '${pageContextDisplay}' exists in rbac_app_pages table for app '${appName}'.`
204
- );
205
- } else {
206
- throw new Error(
207
- `File upload denied: insufficient permissions. You need 'create:page.${pageContextDisplay}' or 'update:page.${pageContextDisplay}' permission for the '${pageContextDisplay}' page. ` +
208
- `Make sure the page exists in rbac_app_pages table for app '${appName}'.`
209
- );
210
- }
276
+ const message = await this.buildPermissionDeniedMessage(options, isSuperAdminUser);
277
+ return await this.rollbackUploadAndErr(filePath, options.is_public ?? false, 'PERMISSION_DENIED', message);
211
278
  }
212
279
 
213
- // Get the created file reference
214
- const { data: fileRef, error: fetchError } = await this.supabase
215
- .from('core_file_references')
216
- .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
217
- .eq('id', data)
218
- .single();
219
-
220
- if (fetchError || !fileRef) {
221
- // Clean up uploaded file if we can't fetch the reference
222
- await deleteFile(this.supabase, filePath, options.is_public || false);
223
- throw new Error(`Failed to fetch created file reference: ${fetchError?.message}`);
280
+ const fetchResult = await this.fetchCreatedFileReference(data as string);
281
+ if (!fetchResult.ok) {
282
+ return await this.rollbackUploadAndErr(
283
+ filePath,
284
+ options.is_public ?? false,
285
+ 'FETCH_FAILED',
286
+ fetchResult.error.message
287
+ );
224
288
  }
225
289
 
226
- // Invalidate cache for this file display entry so newly uploaded files appear immediately
227
- // For user-scoped files, pass null for organisation_id
228
290
  invalidateFileDisplayCache(
229
291
  options.table_name,
230
292
  options.record_id,
@@ -232,53 +294,49 @@ export class FileReferenceServiceImpl implements FileReferenceService {
232
294
  options.category
233
295
  );
234
296
 
235
- return fileRef as FileReference;
297
+ return ok(fetchResult.data);
236
298
  } catch (error) {
237
299
  log.error('Error creating file reference:', error);
238
- throw error;
300
+ return err(toApiError(error));
239
301
  }
240
302
  }
241
303
 
242
- async getFileReference(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference | null> {
304
+ async getFileReference(table_name: string, record_id: string, organisation_id?: string): Promise<ApiResult<FileReference | null>> {
243
305
  try {
244
306
  let query = this.supabase
245
307
  .from('core_file_references')
246
308
  .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
247
309
  .eq('table_name', table_name)
248
310
  .eq('record_id', record_id);
249
-
250
- // Handle NULL organisation_id for user-owned files
251
311
  if (organisation_id === null || organisation_id === undefined) {
252
312
  query = query.is('organisation_id', null);
253
313
  } else {
254
314
  query = query.eq('organisation_id', organisation_id);
255
315
  }
256
-
257
316
  const { data, error } = await query.single();
258
-
259
317
  if (error) {
260
318
  if (error.code === 'PGRST116') {
261
- return null; // No rows found
319
+ return ok(null);
262
320
  }
263
- throw new Error(`Failed to get file reference: ${error.message}`);
321
+ return err({ code: 'FETCH_FAILED', message: error.message });
264
322
  }
265
-
266
- return data as FileReference;
323
+ return ok(data as FileReference);
267
324
  } catch (error) {
268
325
  log.error('Error getting file reference:', error);
269
- throw error;
326
+ return err(toApiError(error));
270
327
  }
271
328
  }
272
329
 
273
- async getFileUrl(table_name: string, record_id: string, organisation_id?: string): Promise<string | null> {
330
+ async getFileUrl(table_name: string, record_id: string, organisation_id?: string): Promise<ApiResult<string | null>> {
274
331
  try {
275
- // Get file reference to check if it's public
276
- const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
332
+ const refResult = await this.getFileReference(table_name, record_id, organisation_id);
333
+ if (!refResult.ok) {
334
+ return refResult;
335
+ }
336
+ const fileRef = refResult.data;
277
337
  if (!fileRef) {
278
- return null;
338
+ return ok(null);
279
339
  }
280
-
281
- // For public files, RPC returns file path - generate public URL client-side
282
340
  if (fileRef.is_public) {
283
341
  const { data: pathData } = await this.supabase
284
342
  .rpc('data_file_reference_url_get', {
@@ -286,26 +344,20 @@ export class FileReferenceServiceImpl implements FileReferenceService {
286
344
  p_record_id: record_id,
287
345
  p_organisation_id: organisation_id
288
346
  });
289
-
290
347
  if (!pathData) {
291
- return null;
348
+ return ok(null);
292
349
  }
293
-
294
- // Generate public URL using bucket-aware helper
295
- return getPublicUrl(this.supabase, pathData, true);
296
- } else {
297
- // For private files, use signed URL
298
- return await this.getSignedUrl(table_name, record_id, organisation_id);
350
+ return ok(getPublicUrl(this.supabase, pathData, true));
299
351
  }
352
+ return this.getSignedUrl(table_name, record_id, organisation_id);
300
353
  } catch (error) {
301
354
  log.error('Error getting file URL:', error);
302
- throw error;
355
+ return err(toApiError(error));
303
356
  }
304
357
  }
305
358
 
306
- async getSignedUrl(table_name: string, record_id: string, organisation_id?: string, expires_in: number = 3600): Promise<string | null> {
359
+ async getSignedUrl(table_name: string, record_id: string, organisation_id?: string, expires_in: number = 3600): Promise<ApiResult<string | null>> {
307
360
  try {
308
- // Get file path from RPC function
309
361
  const { data: filePath, error } = await this.supabase
310
362
  .rpc('data_file_reference_signed_url_get', {
311
363
  p_table_name: table_name,
@@ -313,31 +365,29 @@ export class FileReferenceServiceImpl implements FileReferenceService {
313
365
  p_organisation_id: organisation_id ?? null,
314
366
  p_expires_in: expires_in
315
367
  });
316
-
317
368
  if (error) {
318
- throw new Error(`Failed to get signed URL: ${error.message}`);
369
+ return err({ code: 'SIGNED_URL_FAILED', message: error.message });
319
370
  }
320
-
321
371
  if (!filePath) {
322
- return null;
372
+ return ok(null);
323
373
  }
324
-
325
- // Generate signed URL client-side using bucket-aware helper (files bucket for private files)
326
374
  const signedUrlResult = await getSignedUrl(this.supabase, filePath, {
327
375
  appName: 'file-reference',
328
376
  orgId: organisation_id,
329
377
  userId: organisation_id ? undefined : record_id,
330
378
  expiresIn: expires_in
331
379
  });
332
-
333
- return signedUrlResult?.url || null;
380
+ if (!signedUrlResult.ok) {
381
+ return err(signedUrlResult.error);
382
+ }
383
+ return ok(signedUrlResult.data.url ?? null);
334
384
  } catch (error) {
335
385
  log.error('Error getting signed URL:', error);
336
- throw error;
386
+ return err(toApiError(error));
337
387
  }
338
388
  }
339
389
 
340
- async updateFileReference(id: string, updates: Partial<FileReference>): Promise<FileReference> {
390
+ async updateFileReference(id: string, updates: Partial<FileReference>): Promise<ApiResult<FileReference>> {
341
391
  try {
342
392
  const { data, error } = await this.supabase
343
393
  .from('core_file_references')
@@ -345,23 +395,21 @@ export class FileReferenceServiceImpl implements FileReferenceService {
345
395
  .eq('id', id)
346
396
  .select()
347
397
  .single();
348
-
349
398
  if (error) {
350
- throw new Error(`Failed to update file reference: ${error.message}`);
399
+ return err({ code: 'UPDATE_FAILED', message: error.message });
351
400
  }
352
-
353
- return data as FileReference;
401
+ return ok(data as FileReference);
354
402
  } catch (error) {
355
403
  log.error('Error updating file reference:', error);
356
- throw error;
404
+ return err(toApiError(error));
357
405
  }
358
406
  }
359
407
 
360
- async deleteFileReference(table_name: string, record_id: string, organisation_id?: string, delete_file: boolean = false): Promise<boolean> {
408
+ async deleteFileReference(table_name: string, record_id: string, organisation_id?: string, delete_file: boolean = false): Promise<ApiResult<boolean>> {
361
409
  try {
362
- // Get file reference first to determine bucket
363
- const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
364
-
410
+ const refResult = await this.getFileReference(table_name, record_id, organisation_id);
411
+ const fileRef = refResult.ok ? refResult.data : null;
412
+
365
413
  const { error } = await this.supabase
366
414
  .rpc('data_file_reference_delete', {
367
415
  p_table_name: table_name,
@@ -369,24 +417,20 @@ export class FileReferenceServiceImpl implements FileReferenceService {
369
417
  p_organisation_id: organisation_id ?? null,
370
418
  p_delete_file: delete_file
371
419
  });
372
-
373
420
  if (error) {
374
- throw new Error(`Failed to delete file reference: ${error.message}`);
421
+ return err({ code: 'DELETE_FAILED', message: error.message });
375
422
  }
376
-
377
- // If delete_file is true and we have the file reference, delete from storage
378
423
  if (delete_file && fileRef) {
379
424
  await deleteFile(this.supabase, fileRef.file_path, fileRef.is_public || false);
380
425
  }
381
-
382
- return true;
426
+ return ok(true);
383
427
  } catch (error) {
384
428
  log.error('Error deleting file reference:', error);
385
- throw error;
429
+ return err(toApiError(error));
386
430
  }
387
431
  }
388
432
 
389
- async listFileReferences(table_name: string, record_id: string, organisation_id?: string): Promise<FileReference[]> {
433
+ async listFileReferences(table_name: string, record_id: string, organisation_id?: string): Promise<ApiResult<FileReference[]>> {
390
434
  try {
391
435
  const { data, error } = await this.supabase
392
436
  .rpc('data_file_reference_list', {
@@ -394,20 +438,12 @@ export class FileReferenceServiceImpl implements FileReferenceService {
394
438
  p_record_id: record_id,
395
439
  p_organisation_id: organisation_id ?? null
396
440
  });
397
-
398
441
  if (error) {
399
- throw new Error(`Failed to list file references: ${error.message}`);
442
+ return err({ code: 'LIST_FAILED', message: error.message });
400
443
  }
401
-
402
- // RPC returns: id, file_path, file_metadata, is_public, created_at
403
- // We can construct FileReference objects directly from RPC response + function parameters
404
- // This avoids a second query and reduces network requests
405
444
  if (!data || data.length === 0) {
406
- return [];
445
+ return ok([]);
407
446
  }
408
-
409
- // Construct FileReference objects from RPC response
410
- // This avoids RLS issues with direct queries - the RPC already validated permissions
411
447
  interface RpcFileItem {
412
448
  id: string;
413
449
  file_path: string;
@@ -418,38 +454,29 @@ export class FileReferenceServiceImpl implements FileReferenceService {
418
454
  const fileReferences: FileReference[] = data
419
455
  .filter((item: RpcFileItem) => item.id && item.file_path && item.file_metadata)
420
456
  .map((item: RpcFileItem) => {
421
- // Extract file name and type from file_path
422
457
  const fileName = item.file_path.split('/').pop() || 'unknown';
423
458
  const fileType = fileName.split('.').pop() || 'unknown';
424
-
425
- // Construct complete FileReference from RPC response + function parameters
426
- const fileRef: FileReference = {
459
+ return {
427
460
  id: item.id,
428
461
  table_name: table_name,
429
462
  record_id: record_id,
430
463
  file_path: item.file_path,
431
- file_metadata: {
432
- fileName,
433
- fileType,
434
- ...(item.file_metadata || {}),
435
- } as FileMetadata,
464
+ file_metadata: { fileName, fileType, ...(item.file_metadata || {}) } as FileMetadata,
436
465
  organisation_id: organisation_id ?? null,
437
- app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
466
+ app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''),
438
467
  is_public: item.is_public ?? false,
439
468
  created_at: item.created_at || new Date().toISOString(),
440
- updated_at: item.created_at || new Date().toISOString() // RPC doesn't return updated_at, use created_at
441
- };
442
- return fileRef;
469
+ updated_at: item.created_at || new Date().toISOString()
470
+ } as FileReference;
443
471
  });
444
-
445
- return fileReferences;
472
+ return ok(fileReferences);
446
473
  } catch (error) {
447
474
  log.error('Error listing file references:', error);
448
- throw error;
475
+ return err(toApiError(error));
449
476
  }
450
477
  }
451
478
 
452
- async getFileCount(table_name: string, record_id: string, organisation_id?: string): Promise<number> {
479
+ async getFileCount(table_name: string, record_id: string, organisation_id?: string): Promise<ApiResult<number>> {
453
480
  try {
454
481
  const { data, error } = await this.supabase
455
482
  .rpc('data_file_reference_count_get', {
@@ -457,51 +484,43 @@ export class FileReferenceServiceImpl implements FileReferenceService {
457
484
  p_record_id: record_id,
458
485
  p_organisation_id: organisation_id ?? null
459
486
  });
460
-
461
487
  if (error) {
462
- throw new Error(`Failed to get file count: ${error.message}`);
488
+ return err({ code: 'COUNT_FAILED', message: error.message });
463
489
  }
464
-
465
- return data || 0;
490
+ return ok(data ?? 0);
466
491
  } catch (error) {
467
492
  log.error('Error getting file count:', error);
468
- throw error;
493
+ return err(toApiError(error));
469
494
  }
470
495
  }
471
496
 
472
- async getFileReferenceById(id: string, organisation_id?: string): Promise<FileReference | null> {
497
+ async getFileReferenceById(id: string, organisation_id?: string): Promise<ApiResult<FileReference | null>> {
473
498
  try {
474
499
  const { data, error } = await this.supabase
475
500
  .rpc('data_file_reference_get', {
476
501
  p_file_reference_id: id,
477
502
  p_organisation_id: organisation_id ?? null
478
503
  });
479
-
480
504
  if (error) {
481
- throw new Error(`Failed to get file reference by ID: ${error.message}`);
505
+ return err({ code: 'FETCH_FAILED', message: error.message });
482
506
  }
483
-
484
507
  if (!data || data.length === 0) {
485
- return null;
508
+ return ok(null);
486
509
  }
487
-
488
- return data[0] as FileReference;
510
+ return ok(data[0] as FileReference);
489
511
  } catch (error) {
490
512
  log.error('Error getting file reference by ID:', error);
491
- throw error;
513
+ return err(toApiError(error));
492
514
  }
493
515
  }
494
516
 
495
517
  async getFilesByCategory(
496
- table_name: string,
497
- record_id: string,
498
- category: FileCategory,
518
+ table_name: string,
519
+ record_id: string,
520
+ category: FileCategory,
499
521
  organisation_id?: string
500
- ): Promise<FileReference[]> {
522
+ ): Promise<ApiResult<FileReference[]>> {
501
523
  try {
502
- // CRITICAL: Use RPC function to get files by category - this correctly filters on file_metadata->>'category'
503
- // NOTE: We MUST use RPC function. Direct queries with .eq('category', ...) will FAIL with HTTP 406
504
- // because there is NO 'category' column. The category is stored in the JSONB file_metadata field.
505
524
  const { data, error } = await this.supabase
506
525
  .rpc('data_file_reference_by_category_list', {
507
526
  p_table_name: table_name,
@@ -509,32 +528,13 @@ export class FileReferenceServiceImpl implements FileReferenceService {
509
528
  p_category: category,
510
529
  p_organisation_id: organisation_id ?? null
511
530
  });
512
-
513
531
  if (error) {
514
- // Provide clear error message about category filtering
515
- log.error('RPC ERROR getting files by category:', {
516
- error,
517
- errorCode: error.code,
518
- errorMessage: error.message,
519
- errorDetails: error.details,
520
- table_name,
521
- record_id,
522
- category,
523
- organisation_id,
524
- message: 'CRITICAL: Category filtering MUST use RPC function data_file_reference_by_category_list. Direct queries with .eq(\'category\', ...) will FAIL with HTTP 406 because category is stored in file_metadata JSONB field, not a direct column.'
525
- });
526
- throw new Error(`Failed to get files by category: ${error.message}. Category filtering uses file_metadata JSONB field, not a direct column. RPC function required.`);
532
+ log.error('RPC ERROR getting files by category:', { error, table_name, record_id, category });
533
+ return err({ code: 'LIST_FAILED', message: error.message });
527
534
  }
528
-
529
- // RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
530
- // We have table_name, record_id, organisation_id from function parameters
531
- // We can construct FileReference objects directly without another query (avoiding RLS issues)
532
535
  if (!data || data.length === 0) {
533
- return [];
536
+ return ok([]);
534
537
  }
535
-
536
- // Construct FileReference objects from RPC response
537
- // This avoids RLS issues with direct queries - the RPC already validated permissions
538
538
  interface RpcFileItem {
539
539
  id: string;
540
540
  file_path: string;
@@ -544,64 +544,45 @@ export class FileReferenceServiceImpl implements FileReferenceService {
544
544
  }
545
545
  const fileReferences: FileReference[] = data
546
546
  .filter((item: RpcFileItem) => {
547
- // Verify category matches (defensive check)
548
547
  const fileCategory = item.file_metadata?.category;
549
- const matches = fileCategory === category;
550
- if (!matches) {
551
- log.warn('File category mismatch in RPC response:', {
552
- fileId: item.id,
553
- expectedCategory: category,
554
- actualCategory: fileCategory
555
- });
556
- }
557
- return matches && item.id && item.file_path && item.file_metadata;
548
+ return fileCategory === category && item.id && item.file_path && item.file_metadata;
558
549
  })
559
- .map((item: RpcFileItem) => {
560
- // Extract file name and type from file_path
561
- const fileName = item.file_path.split('/').pop() || 'unknown';
562
- const fileType = fileName.split('.').pop() || 'unknown';
563
-
564
- // Construct complete FileReference from RPC response + function parameters
565
- const fileRef: FileReference = {
566
- id: item.id,
567
- table_name: table_name,
568
- record_id: record_id,
569
- file_path: item.file_path,
570
- file_metadata: {
571
- fileName,
572
- fileType,
573
- category: (item.file_metadata?.category as FileCategory) || FileCategory.GENERAL_DOCUMENTS,
574
- ...(item.file_metadata || {}),
575
- } as FileMetadata,
576
- organisation_id: organisation_id ?? null,
577
- app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
578
- is_public: item.is_public ?? false,
579
- created_at: item.created_at || new Date().toISOString(),
580
- updated_at: item.created_at || new Date().toISOString() // RPC doesn't return updated_at, use created_at
581
- };
582
- return fileRef;
583
- });
584
-
585
- return fileReferences;
550
+ .map((item: RpcFileItem) => ({
551
+ id: item.id,
552
+ table_name: table_name,
553
+ record_id: record_id,
554
+ file_path: item.file_path,
555
+ file_metadata: {
556
+ fileName: item.file_path.split('/').pop() || 'unknown',
557
+ fileType: (item.file_path.split('.').pop() || 'unknown'),
558
+ category: (item.file_metadata?.category as FileCategory) || FileCategory.GENERAL_DOCUMENTS,
559
+ ...(item.file_metadata || {}),
560
+ } as FileMetadata,
561
+ organisation_id: organisation_id ?? null,
562
+ app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''),
563
+ is_public: item.is_public ?? false,
564
+ created_at: item.created_at || new Date().toISOString(),
565
+ updated_at: item.created_at || new Date().toISOString(),
566
+ } as FileReference));
567
+ return ok(fileReferences);
586
568
  } catch (error) {
587
569
  log.error('Error getting files by category:', error);
588
- throw error;
570
+ return err(toApiError(error));
589
571
  }
590
572
  }
591
573
 
592
574
  async uploadMultipleFiles(
593
575
  options: FileUploadOptions,
594
576
  files: File[]
595
- ): Promise<import('../../types/file-reference').BulkUploadResult> {
577
+ ): Promise<ApiResult<BulkUploadResult>> {
596
578
  const success: FileReference[] = [];
597
579
  const failed: { file: File; error: string }[] = [];
598
580
  const results: Array<{ file: File; result: FileUploadResult | null; error?: string }> = [];
599
581
 
600
- // Upload files sequentially to avoid overwhelming the server
601
582
  for (const file of files) {
602
- try {
603
- const fileReference = await this.createFileReference(options, file);
604
-
583
+ const result = await this.createFileReference(options, file);
584
+ if (result.ok) {
585
+ const fileReference = result.data;
605
586
  success.push(fileReference);
606
587
  results.push({
607
588
  file,
@@ -612,20 +593,19 @@ export class FileReferenceServiceImpl implements FileReferenceService {
612
593
  : '',
613
594
  },
614
595
  });
615
- } catch (error) {
616
- const message = error instanceof Error ? error.message : 'Unknown error';
617
- failed.push({ file, error: message });
618
- results.push({ file, result: null, error: message });
596
+ } else {
597
+ failed.push({ file, error: result.error.message });
598
+ results.push({ file, result: null, error: result.error.message });
619
599
  }
620
600
  }
621
601
 
622
- return {
602
+ return ok({
623
603
  success,
624
604
  failed,
625
605
  total: results.length,
626
606
  successful: success.length,
627
607
  results,
628
- };
608
+ });
629
609
  }
630
610
  }
631
611
 
@@ -639,25 +619,28 @@ export async function uploadFileWithReference(
639
619
  supabase: SupabaseClient,
640
620
  options: FileUploadOptions,
641
621
  file: File
642
- ): Promise<FileUploadResult> {
643
-
622
+ ): Promise<ApiResult<FileUploadResult>> {
644
623
  const service = createFileReferenceService(supabase);
645
- const fileReference = await service.createFileReference(options, file);
646
-
647
- const fileUrl = options.is_public
648
- ? getPublicUrl(supabase, fileReference.file_path, true)
649
- : await getSignedUrl(supabase, fileReference.file_path, {
650
- appName: 'file-reference',
651
- orgId: options.organisation_id || undefined,
652
- userId: options.userId || undefined,
653
- expiresIn: 3600
654
- });
655
-
656
- const urlString = typeof fileUrl === 'string' ? fileUrl : fileUrl?.url || '';
657
-
658
- return {
624
+ const result = await service.createFileReference(options, file);
625
+ if (!result.ok) {
626
+ return result;
627
+ }
628
+ const fileReference = result.data;
629
+ let urlString: string;
630
+ if (options.is_public) {
631
+ urlString = getPublicUrl(supabase, fileReference.file_path, true);
632
+ } else {
633
+ const signedResult = await getSignedUrl(supabase, fileReference.file_path, {
634
+ appName: 'file-reference',
635
+ orgId: options.organisation_id || undefined,
636
+ userId: options.userId || undefined,
637
+ expiresIn: 3600
638
+ });
639
+ urlString = signedResult.ok ? signedResult.data.url ?? '' : '';
640
+ }
641
+ return ok({
659
642
  file_reference: fileReference,
660
643
  file_url: urlString,
661
644
  signed_url: options.is_public ? undefined : urlString || undefined
662
- };
645
+ });
663
646
  }