@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.
- package/CHANGELOG.md +21 -0
- package/audit-tool/00-dependencies.cjs +46 -13
- package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
- package/audit-tool/audits/02-project-structure.cjs +13 -3
- package/audit-tool/audits/03-architecture.cjs +78 -4
- package/audit-tool/audits/04-code-quality.cjs +9 -2
- package/audit-tool/audits/05-styling.cjs +19 -7
- package/audit-tool/audits/06-security-rbac.cjs +105 -14
- package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
- package/audit-tool/audits/08-testing-documentation.cjs +11 -3
- package/audit-tool/audits/09-operations.cjs +19 -7
- package/audit-tool/index.cjs +22 -11
- package/audit-tool/utils/report-utils.cjs +4 -0
- package/cursor-rules/01-pace-core-compliance.mdc +1 -0
- package/cursor-rules/02-project-structure.mdc +1 -0
- package/cursor-rules/03-architecture.mdc +3 -1
- package/cursor-rules/04-code-quality.mdc +1 -0
- package/cursor-rules/05-styling.mdc +41 -7
- package/cursor-rules/06-security-rbac.mdc +2 -1
- package/cursor-rules/07-api-tech-stack.mdc +1 -0
- package/cursor-rules/08-testing-documentation.mdc +1 -0
- package/cursor-rules/09-operations.mdc +1 -0
- package/dist/{DataTable-SAXFG4XI.js → DataTable-EFYP2QLE.js} +10 -7
- package/dist/{InactivityServiceProvider-DHryoh6K.d.ts → InactivityServiceProvider-BbxwwDz1.d.ts} +10 -1
- package/dist/{UnifiedAuthProvider-CiBAl9-s.d.ts → UnifiedAuthProvider-Bkt_tzdS.d.ts} +56 -24
- package/dist/{api-F47QJ7FX.js → api-BZR2CYXL.js} +3 -2
- package/dist/api-result-USV1Czr-.d.ts +51 -0
- package/dist/{audit-Z6ZZBWLU.js → audit-HI2DHUVU.js} +2 -1
- package/dist/{auth-BZOJqrdd.d.ts → auth-JvdRVaud.d.ts} +1 -1
- package/dist/{chunk-KSNLMI7N.js → chunk-2DL2WSOE.js} +1 -155
- package/dist/{chunk-MPY44PWB.js → chunk-2OEVOGGR.js} +4648 -3560
- package/dist/chunk-44CNXN4P.js +15 -0
- package/dist/{chunk-Y4PF6HIM.js → chunk-4R3T5ENU.js} +867 -786
- package/dist/{chunk-LNHFAF4X.js → chunk-7A6IMHH2.js} +289 -247
- package/dist/chunk-CU2BU2MQ.js +2 -0
- package/dist/{chunk-JJEYZ3DX.js → chunk-D6BMFMQZ.js} +37 -2
- package/dist/{chunk-BCTXBU6U.js → chunk-ENLXB7GP.js} +88 -71
- package/dist/{chunk-FBZ7U3ID.js → chunk-J2KQK6DG.js} +937 -987
- package/dist/{chunk-TFIPNIPE.js → chunk-KJXRL3XE.js} +3300 -2245
- package/dist/{chunk-3GWSPISD.js → chunk-L5LFKKLJ.js} +1 -1
- package/dist/{chunk-X5EAU5G7.js → chunk-PCSHBLPB.js} +132 -114
- package/dist/{chunk-NIU6DPQV.js → chunk-QRYSEPHB.js} +2 -0
- package/dist/{chunk-KYURMOQM.js → chunk-V7FTM2LU.js} +423 -320
- package/dist/chunk-WY6Y7KC3.js +264 -0
- package/dist/{chunk-FN52B75D.js → chunk-XOJME5T7.js} +176 -15
- package/dist/{chunk-7YDC7LMU.js → chunk-XPFVT3GN.js} +71 -66
- package/dist/{chunk-66R6RLUZ.js → chunk-YFTFFJIV.js} +3 -3
- package/dist/{chunk-W46INAVW.js → chunk-YYTWKVHO.js} +688 -570
- package/dist/components.d.ts +8 -7
- package/dist/components.js +17 -15
- package/dist/{database.generated-DT8JTZiP.d.ts → database.generated-qkdoiVrJ.d.ts} +45 -10
- package/dist/eslint-rules/index.cjs +3 -0
- package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +74 -0
- package/dist/{event-WTAQuGcq.d.ts → event-BfCox3N2.d.ts} +36 -10
- package/dist/{file-reference-BavO2eQj.d.ts → file-reference-DU1hcawx.d.ts} +29 -13
- package/dist/hooks.d.ts +22 -9
- package/dist/hooks.js +34 -25
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +66 -177
- package/dist/index.js +316 -340
- package/dist/pagination-BW1mqywp.d.ts +201 -0
- package/dist/providers.d.ts +6 -5
- package/dist/providers.js +5 -3
- package/dist/rbac/index.d.ts +123 -138
- package/dist/rbac/index.js +10 -8
- package/dist/theming/runtime.d.ts +19 -2
- package/dist/theming/runtime.js +1 -1
- package/dist/{timezone-K-ptz3HO.d.ts → timezone-BTWWXKVY.d.ts} +1 -1
- package/dist/types.d.ts +17 -10
- package/dist/types.js +1 -0
- package/dist/{usePublicPageContext-vxBlEHO9.d.ts → usePublicPageContext-B91dGYW1.d.ts} +433 -356
- package/dist/{usePublicRouteParams-G3Ks53mk.d.ts → usePublicRouteParams-BgV6VhMi.d.ts} +73 -4
- package/dist/utils.d.ts +163 -145
- package/dist/utils.js +42 -25
- package/docs/api/modules.md +782 -643
- package/docs/api-reference/rpc-functions.md +12 -3
- package/docs/core-concepts/rbac-system.md +8 -0
- package/docs/getting-started/cursor-rules.md +17 -20
- package/docs/getting-started/dependencies.md +1 -1
- package/docs/getting-started/setup.md +235 -0
- package/docs/implementation-guides/authentication.md +27 -0
- package/docs/implementation-guides/data-tables.md +176 -3
- package/docs/migration/ApiResult-migration.md +25 -0
- package/docs/rbac/api-reference.md +33 -31
- package/docs/standards/0-standards-overview.md +50 -15
- package/docs/standards/1-pace-core-compliance-standards.md +62 -57
- package/docs/standards/2-project-structure-standards.md +33 -16
- package/docs/standards/3-architecture-standards.md +41 -1
- package/docs/standards/4-code-quality-standards.md +26 -6
- package/docs/standards/5-styling-standards.md +35 -1
- package/docs/standards/6-security-rbac-standards.md +66 -0
- package/docs/standards/7-api-tech-stack-standards.md +25 -14
- package/docs/standards/8-testing-documentation-standards.md +31 -0
- package/docs/standards/9-operations-standards.md +19 -0
- package/docs/standards/README.md +20 -201
- package/docs/testing/test-setup-for-consumers.md +2 -0
- package/docs/troubleshooting/common-issues.md +17 -1
- package/docs/troubleshooting/organisation-context-setup.md +8 -0
- package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
- package/eslint-config-pace-core.cjs +20 -0
- package/package.json +14 -20
- package/scripts/{build-docs-incremental.js → build-docs.js} +3 -2
- package/scripts/setup.cjs +536 -0
- package/scripts/validate.cjs +480 -0
- package/src/__tests__/helpers/{__tests__/component-test-utils.test.tsx → component-test-utils.test.tsx} +3 -3
- package/src/__tests__/helpers/{__tests__/optimized-test-setup.test.ts → optimized-test-setup.test.ts} +2 -2
- package/src/__tests__/helpers/{__tests__/supabaseMock.test.ts → supabaseMock.test.ts} +2 -2
- package/src/__tests__/helpers/{__tests__/test-providers.test.tsx → test-providers.test.tsx} +1 -1
- package/src/__tests__/helpers/test-providers.tsx +37 -39
- package/src/__tests__/helpers/{__tests__/test-utils.test.tsx → test-utils.test.tsx} +4 -3
- package/src/__tests__/helpers/{__tests__/timer-utils.test.ts → timer-utils.test.ts} +2 -2
- package/src/assets/app-icons/index.test.ts +304 -0
- package/src/components/AddressField/AddressField.test.tsx +1 -1
- package/src/components/AddressField/AddressField.tsx +238 -212
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.test.tsx +172 -17
- package/src/components/Card/Card.tsx +19 -10
- package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
- package/src/components/ContextSelector/{__tests__/ContextSelector.test.tsx → ContextSelector.test.tsx} +6 -6
- package/src/components/ContextSelector/ContextSelector.tsx +66 -280
- package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
- package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
- package/src/components/DataTable/AUDIT_REPORT.md +59 -44
- package/src/components/DataTable/{__tests__/DataTable.comprehensive.test.tsx → DataTable.comprehensive.test.tsx} +6 -6
- package/src/components/DataTable/{__tests__/DataTable.default-state.test.tsx → DataTable.default-state.test.tsx} +5 -5
- package/src/components/DataTable/{__tests__/DataTable.export.test.tsx → DataTable.export.test.tsx} +10 -10
- package/src/components/DataTable/{__tests__/DataTable.grouping-aggregation.test.tsx → DataTable.grouping-aggregation.test.tsx} +6 -6
- package/src/components/DataTable/{__tests__/DataTable.hooks.test.tsx → DataTable.hooks.test.tsx} +6 -6
- package/src/components/DataTable/{__tests__/DataTable.select-label-display.test.tsx → DataTable.select-label-display.test.tsx} +6 -6
- package/src/components/DataTable/DataTable.test.tsx +787 -416
- package/src/components/DataTable/DataTable.tsx +12 -12
- package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
- package/src/components/DataTable/{__tests__/DataTableCore.test-setup.ts → DataTableCore.test-setup.ts} +10 -9
- package/src/components/DataTable/{__tests__/DataTableCore.test.tsx → DataTableCore.test.tsx} +8 -8
- package/src/components/DataTable/{__tests__/README.md → README.md} +17 -7
- package/src/components/DataTable/TESTING.md +101 -0
- package/src/components/DataTable/{__tests__/a11y.basic.test.tsx → a11y.basic.test.tsx} +34 -34
- package/src/components/DataTable/components/DataTableCore.tsx +104 -864
- package/src/components/DataTable/components/{__tests__/GroupingDropdown.test.tsx → GroupingDropdown.test.tsx} +17 -8
- package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
- package/src/components/DataTable/components/ImportModal.tsx +61 -559
- package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/context/{__tests__/DataTableContext.test.tsx → DataTableContext.test.tsx} +2 -2
- package/src/components/DataTable/context/DataTableContext.tsx +7 -6
- package/src/components/DataTable/core/{__tests__/ColumnFactory.test.ts → ColumnFactory.test.ts} +2 -2
- package/src/components/DataTable/hooks/{__tests__/useColumnOrderPersistence.test.ts → useColumnOrderPersistence.test.ts} +2 -2
- package/src/components/DataTable/hooks/{__tests__/useColumnVisibilityPersistence.test.ts → useColumnVisibilityPersistence.test.ts} +2 -2
- package/src/components/DataTable/hooks/{__tests__/useDataTableConfiguration.test.ts → useDataTableConfiguration.test.ts} +3 -3
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +14 -2
- package/src/components/DataTable/hooks/{__tests__/useDataTableDataPipeline.test.ts → useDataTableDataPipeline.test.ts} +6 -6
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +238 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
- package/src/components/DataTable/hooks/{__tests__/useDataTablePermissions.test.ts → useDataTablePermissions.test.ts} +11 -11
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +79 -247
- package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
- package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
- package/src/components/DataTable/hooks/useDataTableScope.test.ts +110 -0
- package/src/components/DataTable/hooks/useDataTableScope.ts +123 -0
- package/src/components/DataTable/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +47 -5
- package/src/components/DataTable/hooks/useDataTableState.ts +145 -94
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
- package/src/components/DataTable/hooks/{__tests__/useEffectiveColumnOrder.test.ts → useEffectiveColumnOrder.test.ts} +2 -2
- package/src/components/DataTable/hooks/{__tests__/useHierarchicalState.test.ts → useHierarchicalState.test.ts} +2 -2
- package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.test.ts +3 -3
- package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.ts +2 -2
- package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
- package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
- package/src/components/DataTable/hooks/{__tests__/useKeyboardNavigation.test.ts → useKeyboardNavigation.test.ts} +3 -3
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +309 -269
- package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.test.ts +3 -3
- package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.ts +3 -3
- package/src/components/DataTable/hooks/{__tests__/useServerSideDataEffect.test.ts → useServerSideDataEffect.test.ts} +2 -2
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +14 -3
- package/src/components/DataTable/hooks/{__tests__/useTableColumns.test.ts → useTableColumns.test.ts} +2 -2
- package/src/components/DataTable/hooks/{__tests__/useTableHandlers.test.ts → useTableHandlers.test.ts} +25 -4
- package/src/components/DataTable/hooks/useTableHandlers.ts +5 -2
- package/src/components/DataTable/index.ts +18 -17
- package/src/components/DataTable/{__tests__/keyboard.test.tsx → keyboard.test.tsx} +41 -63
- package/src/components/DataTable/{__tests__/mocks → mocks}/MockRBACProvider.tsx +1 -1
- package/src/components/DataTable/{__tests__/pagination.modes.test.tsx → pagination.modes.test.tsx} +6 -6
- package/src/components/DataTable/{__tests__/ssr.strict-mode.test.tsx → ssr.strict-mode.test.tsx} +2 -2
- package/src/components/DataTable/{__tests__/styles.test.ts → styles.test.ts} +1 -4
- package/src/components/DataTable/styles.ts +0 -1
- package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
- package/src/components/DataTable/{__tests__/test-utils → test-utils}/dataFactories.ts +2 -2
- package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
- package/src/components/DataTable/{__tests__/test-utils/sharedTestUtils.tsx → test-utils/sharedTestUtils.ts} +97 -66
- package/src/components/DataTable/{__tests__/test-utils.ts → test-utils.ts} +1 -1
- package/src/components/DataTable/types/actions.ts +71 -0
- package/src/components/DataTable/types/base.ts +39 -0
- package/src/components/DataTable/types/columns.ts +125 -0
- package/src/components/DataTable/types/export.ts +32 -0
- package/src/components/DataTable/types/features.ts +81 -0
- package/src/components/DataTable/types/hierarchical.ts +44 -0
- package/src/components/DataTable/types/index.ts +43 -0
- package/src/components/DataTable/types/pagination.ts +85 -0
- package/src/components/DataTable/types/performance.ts +47 -0
- package/src/components/DataTable/types/props.ts +62 -0
- package/src/components/DataTable/types/rbac.ts +45 -0
- package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableCore.test.tsx +430 -28
- package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
- package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableErrorBoundary.test.tsx +4 -4
- package/src/components/DataTable/{components → ui/layout}/DataTableErrorBoundary.tsx +7 -7
- package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
- package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
- package/src/components/DataTable/{components → ui/modals}/DataTableModals.tsx +36 -28
- package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
- package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
- package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
- package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
- package/src/components/DataTable/{components/__tests__ → ui/shared}/AccessDeniedPage.test.tsx +2 -2
- package/src/components/DataTable/{components → ui/shared}/AccessDeniedPage.tsx +2 -2
- package/src/components/DataTable/{components/__tests__ → ui/shared}/ActionButtons.test.tsx +6 -4
- package/src/components/DataTable/{components → ui/shared}/ActionButtons.tsx +4 -4
- package/src/components/DataTable/{components/__tests__ → ui/shared}/ColumnFilter.test.tsx +29 -16
- package/src/components/DataTable/{components → ui/shared}/ColumnFilter.tsx +4 -4
- package/src/components/DataTable/{components/__tests__ → ui/shared}/PaginationControls.test.tsx +38 -16
- package/src/components/DataTable/{components → ui/shared}/PaginationControls.tsx +21 -15
- package/src/components/DataTable/{components/__tests__ → ui/shared}/SortIndicator.test.tsx +2 -2
- package/src/components/DataTable/{components → ui/shared}/SortIndicator.tsx +1 -1
- package/src/components/DataTable/{components/__tests__ → ui/table}/EditFields.test.tsx +3 -3
- package/src/components/DataTable/{components → ui/table}/EditFields.tsx +138 -69
- package/src/components/DataTable/{components/__tests__ → ui/table}/EditableRow.test.tsx +36 -27
- package/src/components/DataTable/{components → ui/table}/EditableRow.tsx +86 -104
- package/src/components/DataTable/{components/__tests__ → ui/table}/EmptyState.test.tsx +2 -62
- package/src/components/DataTable/{components → ui/table}/EmptyState.tsx +7 -15
- package/src/components/DataTable/{components/__tests__ → ui/table}/FilterRow.test.tsx +5 -4
- package/src/components/DataTable/{components → ui/table}/FilterRow.tsx +3 -3
- package/src/components/DataTable/{components/__tests__ → ui/table}/LoadingState.test.tsx +6 -10
- package/src/components/DataTable/{components → ui/table}/LoadingState.tsx +4 -4
- package/src/components/DataTable/{components/__tests__ → ui/table}/RowComponent.test.tsx +412 -17
- package/src/components/DataTable/{components → ui/table}/RowComponent.tsx +183 -177
- package/src/components/DataTable/{components/__tests__ → ui/table}/UnifiedTableBody.test.tsx +425 -16
- package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
- package/src/components/DataTable/{components/__tests__ → ui/table}/cellValueUtils.test.ts +2 -2
- package/src/components/DataTable/{components → ui/table}/cellValueUtils.ts +1 -1
- package/src/components/DataTable/{components/__tests__ → ui/toolbar}/BulkOperationsDropdown.test.tsx +12 -5
- package/src/components/DataTable/{components → ui/toolbar}/BulkOperationsDropdown.tsx +3 -3
- package/src/components/DataTable/{components/__tests__ → ui/toolbar}/ColumnVisibilityDropdown.test.tsx +7 -4
- package/src/components/DataTable/{components → ui/toolbar}/ColumnVisibilityDropdown.tsx +7 -7
- package/src/components/DataTable/{components/__tests__ → ui/toolbar}/DataTableToolbar.test.tsx +4 -4
- package/src/components/DataTable/{components → ui/toolbar}/DataTableToolbar.tsx +4 -4
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
- package/src/components/DataTable/utils/{__tests__/a11yUtils.test.ts → a11yUtils.test.ts} +2 -2
- package/src/components/DataTable/utils/{__tests__/aggregationUtils.test.ts → aggregationUtils.test.ts} +3 -3
- package/src/components/DataTable/utils/{__tests__/columnUtils.test.ts → columnUtils.test.ts} +2 -2
- package/src/components/DataTable/utils/csvParse.test.ts +74 -0
- package/src/components/DataTable/utils/csvParse.ts +65 -0
- package/src/components/DataTable/utils/{__tests__/errorHandling.test.ts → errorHandling.test.ts} +2 -2
- package/src/components/DataTable/utils/{__tests__/exportUtils.test.ts → exportUtils.test.ts} +3 -3
- package/src/components/DataTable/utils/{__tests__/flexibleImport.test.ts → flexibleImport.test.ts} +2 -2
- package/src/components/DataTable/utils/flexibleImport.ts +3 -186
- package/src/components/DataTable/utils/{__tests__/hierarchicalSorting.test.ts → hierarchicalSorting.test.ts} +3 -3
- package/src/components/DataTable/utils/{__tests__/hierarchicalUtils.test.ts → hierarchicalUtils.test.ts} +3 -3
- package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
- package/src/components/DataTable/utils/importDateParser.ts +114 -0
- package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
- package/src/components/DataTable/utils/importValueParser.ts +91 -0
- package/src/components/DataTable/utils/{__tests__/paginationUtils.test.ts → paginationUtils.test.ts} +2 -2
- package/src/components/DataTable/utils/paginationUtils.ts +6 -3
- package/src/components/DataTable/utils/{__tests__/performanceUtils.test.ts → performanceUtils.test.ts} +3 -3
- package/src/components/DataTable/utils/{__tests__/rowUtils.test.ts → rowUtils.test.ts} +3 -3
- package/src/components/DataTable/utils/{__tests__/selectFieldUtils.test.ts → selectFieldUtils.test.ts} +66 -3
- package/src/components/DataTable/utils/selectFieldUtils.ts +97 -60
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.test.tsx +1 -1
- package/src/components/Dialog/Dialog.test-utils.ts +49 -0
- package/src/components/Dialog/Dialog.test.tsx +896 -89
- package/src/components/Dialog/Dialog.tsx +174 -882
- package/src/components/Dialog/dialogLock.test.ts +238 -0
- package/src/components/Dialog/dialogLock.ts +98 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
- package/src/components/Dialog/useDialogDimensions.ts +140 -0
- package/src/components/Dialog/useDialogLifecycle.test.ts +358 -0
- package/src/components/Dialog/useDialogLifecycle.ts +135 -0
- package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
- package/src/components/Dialog/useDialogPersistence.ts +357 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +40 -40
- package/src/components/FileDisplay/FileDisplay.tsx +24 -656
- package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
- package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
- package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
- package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
- package/src/components/FileDisplay/fallbackUtils.ts +44 -0
- package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
- package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
- package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
- package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
- package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.test.ts +40 -42
- package/src/components/FileDisplay/useFileDisplay.ts +515 -0
- package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.unit.test.ts +406 -77
- package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
- package/src/{hooks/public → components/FileDisplay}/usePublicFileDisplay.test.ts +94 -88
- package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
- package/src/components/FileUpload/FileUpload.test.tsx +16 -10
- package/src/components/FileUpload/FileUpload.tsx +107 -525
- package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
- package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
- package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
- package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
- package/src/components/FileUpload/useFileUploadManager.ts +454 -0
- package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
- package/src/components/FileUpload/useResolvedAppId.ts +77 -0
- package/src/components/Footer/Footer.test.tsx +6 -292
- package/src/components/Footer/Footer.tsx +8 -125
- package/src/components/Form/Form.test.tsx +44 -27
- package/src/components/Form/Form.tsx +64 -287
- package/src/components/Form/useFormPersistence.ts +257 -0
- package/src/components/Header/Header.test.tsx +17 -18
- package/src/components/Header/Header.tsx +10 -1
- package/src/components/Input/Input.tsx +1 -1
- package/src/components/Label/Label.test.tsx +1 -1
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +1 -1
- package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +1029 -26
- package/src/components/NavigationMenu/NavigationMenu.tsx +61 -361
- package/src/components/NavigationMenu/index.ts +6 -1
- package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
- package/src/components/NavigationMenu/{__tests__/useNavigationFiltering.test.ts → useNavigationFiltering.test.ts} +68 -53
- package/src/components/NavigationMenu/useNavigationFiltering.ts +197 -296
- package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
- package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +77 -62
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +3 -3
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +16 -19
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +529 -5
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +280 -756
- package/src/components/PaceAppLayout/useFilteredNavItems.ts +304 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +142 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +79 -0
- package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
- package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +31 -25
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +31 -122
- package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
- package/src/components/Progress/Progress.tsx +1 -2
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -235
- package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
- package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
- package/src/components/PublicLayout/PublicLayout.test.tsx +217 -36
- package/src/components/PublicLayout/PublicPageLayout.tsx +132 -73
- package/src/components/PublicLayout/PublicPageProvider.tsx +5 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +28 -18
- package/src/components/Select/{__tests__/context.test.tsx → context.test.tsx} +3 -3
- package/src/components/Select/{utils/__tests__/text.test.tsx → text.test.tsx} +2 -2
- package/src/components/Select/{utils/text.ts → text.ts} +1 -1
- package/src/components/Select/{hooks/__tests__/useSelectEvents.test.ts → useSelectEvents.test.ts} +5 -5
- package/src/components/Select/{hooks/useSelectEvents.ts → useSelectEvents.ts} +2 -2
- package/src/components/Select/{hooks/__tests__/useSelectSearch.test.tsx → useSelectSearch.test.tsx} +7 -7
- package/src/components/Select/{hooks/useSelectSearch.ts → useSelectSearch.ts} +2 -2
- package/src/components/Select/{hooks/__tests__/useSelectState.test.ts → useSelectState.test.ts} +16 -2
- package/src/components/Select/{hooks/useSelectState.ts → useSelectState.ts} +3 -3
- package/src/components/Table/Table.test.tsx +348 -0
- package/src/components/Tabs/Tabs.test.tsx +270 -0
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +420 -0
- package/src/components/{__tests__/index.test.ts → index.test.ts} +2 -2
- package/src/constants/{__tests__/performance.test.ts → performance.test.ts} +2 -2
- package/src/hooks/{__tests__/ServiceHooks.test.tsx → ServiceHooks.test.tsx} +8 -8
- package/src/hooks/{__tests__/hooks.integration.test.tsx → hooks.integration.test.tsx} +11 -11
- package/src/hooks/index.ts +7 -4
- package/src/hooks/{__tests__/index.unit.test.ts → index.unit.test.ts} +2 -2
- package/src/hooks/public/usePublicEvent.test.ts +1 -1
- package/src/hooks/public/usePublicEventLogo.test.ts +1 -1
- package/src/hooks/public/usePublicRouteParams.test.ts +1 -1
- package/src/hooks/services/useAuth.ts +9 -7
- package/src/hooks/useAddressAutocomplete.test.ts +22 -22
- package/src/hooks/useAddressAutocomplete.ts +90 -75
- package/src/hooks/{__tests__/useAppConfig.unit.test.ts → useAppConfig.unit.test.ts} +328 -22
- package/src/hooks/{__tests__/useComponentPerformance.unit.test.tsx → useComponentPerformance.unit.test.tsx} +27 -41
- package/src/hooks/useDataTablePerformance.ts +100 -120
- package/src/hooks/{__tests__/useDataTablePerformance.unit.test.ts → useDataTablePerformance.unit.test.ts} +5 -5
- package/src/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +2 -2
- package/src/hooks/{__tests__/useDebounce.unit.test.ts → useDebounce.unit.test.ts} +2 -2
- package/src/hooks/useEventTheme.test.ts +4 -1
- package/src/hooks/useEventTheme.ts +49 -21
- package/src/hooks/useEvents.ts +41 -1
- package/src/hooks/{__tests__/useEvents.unit.test.ts → useEvents.unit.test.ts} +5 -5
- package/src/hooks/useFileReference.test.ts +44 -41
- package/src/hooks/useFileReference.ts +182 -173
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/{__tests__/useFileUrl.unit.test.ts → useFileUrl.unit.test.ts} +26 -36
- package/src/hooks/{__tests__/useFileUrlCache.test.ts → useFileUrlCache.test.ts} +8 -8
- package/src/hooks/useFileUrlCache.ts +1 -1
- package/src/hooks/{__tests__/useFocusManagement.unit.test.ts → useFocusManagement.unit.test.ts} +2 -2
- package/src/hooks/{__tests__/useFocusTrap.unit.test.tsx → useFocusTrap.unit.test.tsx} +2 -2
- package/src/hooks/{__tests__/useFormDialog.test.ts → useFormDialog.test.ts} +2 -2
- package/src/hooks/useInactivityTracker.ts +138 -131
- package/src/hooks/{__tests__/useInactivityTracker.unit.test.ts → useInactivityTracker.unit.test.ts} +3 -3
- package/src/hooks/{__tests__/useIsMobile.unit.test.ts → useIsMobile.unit.test.ts} +2 -2
- package/src/hooks/useIsPrint.ts +62 -0
- package/src/hooks/useIsPrint.unit.test.ts +545 -0
- package/src/hooks/{__tests__/useKeyboardShortcuts.unit.test.ts → useKeyboardShortcuts.unit.test.ts} +2 -2
- package/src/hooks/{__tests__/useOrganisationPermissions.unit.test.tsx → useOrganisationPermissions.unit.test.tsx} +4 -4
- package/src/hooks/useOrganisationSecurity.test.ts +3 -3
- package/src/hooks/useOrganisationSecurity.ts +190 -201
- package/src/hooks/{__tests__/useOrganisationSecurity.unit.test.tsx → useOrganisationSecurity.unit.test.tsx} +61 -63
- package/src/hooks/{__tests__/useOrganisations.unit.test.ts → useOrganisations.unit.test.ts} +5 -5
- package/src/hooks/{__tests__/usePerformanceMonitor.unit.test.ts → usePerformanceMonitor.unit.test.ts} +13 -14
- package/src/hooks/{__tests__/usePermissionCache.test.ts → usePermissionCache.test.ts} +26 -27
- package/src/hooks/usePermissionCache.ts +276 -271
- package/src/hooks/{__tests__/usePreventTabReload.test.ts → usePreventTabReload.test.ts} +2 -2
- package/src/hooks/{__tests__/usePublicEvent.simple.test.ts → usePublicEvent.simple.test.ts} +4 -4
- package/src/hooks/{__tests__/usePublicEvent.test.ts → usePublicEvent.test.ts} +4 -4
- package/src/hooks/{__tests__/usePublicEvent.unit.test.ts → usePublicEvent.unit.test.ts} +4 -4
- package/src/hooks/{__tests__/usePublicFileDisplay.test.ts → usePublicFileDisplay.test.ts} +12 -12
- package/src/hooks/{__tests__/usePublicRouteParams.unit.test.ts → usePublicRouteParams.unit.test.ts} +3 -3
- package/src/hooks/{__tests__/useQueryCache.test.ts → useQueryCache.test.ts} +2 -2
- package/src/hooks/useQueryCache.ts +0 -2
- package/src/hooks/{__tests__/useRBAC.unit.test.ts → useRBAC.unit.test.ts} +55 -38
- package/src/hooks/{__tests__/useSessionDraft.test.ts → useSessionDraft.test.ts} +2 -2
- package/src/hooks/{__tests__/useSessionRestoration.unit.test.tsx → useSessionRestoration.unit.test.tsx} +10 -19
- package/src/hooks/useStorage.ts +21 -16
- package/src/hooks/{__tests__/useStorage.unit.test.ts → useStorage.unit.test.ts} +38 -75
- package/src/hooks/{__tests__/useToast.test.ts → useToast.test.ts} +2 -2
- package/src/hooks/{__tests__/useToast.unit.test.tsx → useToast.unit.test.tsx} +2 -2
- package/src/hooks/{__tests__/useZodForm.unit.test.tsx → useZodForm.unit.test.tsx} +2 -2
- package/src/icons/{__tests__/index.test.ts → index.test.ts} +2 -2
- package/src/icons/index.ts +2 -0
- package/src/{__tests__/index.test.ts → index.test.ts} +3 -7
- package/src/index.ts +15 -7
- package/src/providers/{__tests__/AuthProvider.test.tsx → AuthProvider.test.tsx} +3 -3
- package/src/providers/{__tests__/EventProvider.test.tsx → EventProvider.test.tsx} +3 -3
- package/src/providers/InactivityProvider.test-helper.tsx +40 -0
- package/src/providers/{__tests__/InactivityProvider.test.tsx → InactivityProvider.test.tsx} +14 -21
- package/src/providers/{__tests__/ProviderLifecycle.test.tsx → ProviderLifecycle.test.tsx} +4 -4
- package/src/providers/{__tests__/UnifiedAuthProvider.test.tsx → UnifiedAuthProvider.test.tsx} +1 -1
- package/src/providers/{__tests__/index.test.ts → index.test.ts} +2 -2
- package/src/providers/services/{__tests__/AuthServiceProvider.integration.test.tsx → AuthServiceProvider.integration.test.tsx} +4 -4
- package/src/providers/services/{__tests__/AuthServiceProvider.test.tsx → AuthServiceProvider.test.tsx} +7 -7
- package/src/providers/services/{__tests__/EventServiceProvider.test.tsx → EventServiceProvider.test.tsx} +7 -7
- package/src/providers/services/{__tests__/InactivityServiceProvider.test.tsx → InactivityServiceProvider.test.tsx} +5 -5
- package/src/providers/services/{__tests__/OrganisationServiceProvider.test.tsx → OrganisationServiceProvider.test.tsx} +6 -6
- package/src/providers/services/UnifiedAuthContext.ts +30 -27
- package/src/providers/services/{__tests__/UnifiedAuthProvider.advanced.test.tsx → UnifiedAuthProvider.advanced.test.tsx} +8 -9
- package/src/providers/services/{__tests__/UnifiedAuthProvider.appId.test.tsx → UnifiedAuthProvider.appId.test.tsx} +25 -25
- package/src/providers/services/{__tests__/UnifiedAuthProvider.integration.test.tsx → UnifiedAuthProvider.integration.test.tsx} +14 -11
- package/src/providers/services/UnifiedAuthProvider.tsx +115 -360
- package/src/providers/services/{__tests__/contexts.test.tsx → contexts.test.tsx} +6 -6
- package/src/providers/services/{__tests__/useUnifiedAuth.test.tsx → useUnifiedAuth.test.tsx} +6 -6
- package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
- package/src/providers/useInactivity.test-helper.ts +27 -0
- package/src/rbac/{__tests__/adapters.comprehensive.test.tsx → adapters.comprehensive.test.tsx} +24 -24
- package/src/rbac/adapters.test.tsx +22 -22
- package/src/rbac/adapters.tsx +29 -29
- package/src/rbac/api.test.ts +973 -42
- package/src/rbac/api.ts +228 -253
- package/src/rbac/{__tests__/audit-batched.test.ts → audit-batched.test.ts} +6 -6
- package/src/rbac/audit.ts +4 -1
- package/src/rbac/{__tests__/auth-rbac-security.integration.test.tsx → auth-rbac-security.integration.test.tsx} +1 -1
- package/src/rbac/{__tests__/auth-rbac.e2e.test.tsx → auth-rbac.e2e.test.tsx} +27 -34
- package/src/rbac/cache-invalidation.test.ts +715 -0
- package/src/rbac/components/{__tests__/AccessDenied.test.tsx → AccessDenied.test.tsx} +3 -3
- package/src/rbac/components/{__tests__/NavigationGuard.test.tsx → NavigationGuard.test.tsx} +13 -11
- package/src/{__tests__/rbac/PagePermissionGuard.test.tsx → rbac/components/PagePermissionGuard.guard.test.tsx} +33 -19
- package/src/rbac/components/{__tests__/PagePermissionGuard.performance.test.tsx → PagePermissionGuard.performance.test.tsx} +30 -9
- package/src/rbac/components/{__tests__/PagePermissionGuard.race-condition.test.tsx → PagePermissionGuard.race-condition.test.tsx} +7 -7
- package/src/rbac/components/{__tests__/PagePermissionGuard.test.tsx → PagePermissionGuard.test.tsx} +10 -10
- package/src/rbac/components/PagePermissionGuard.tsx +177 -372
- package/src/rbac/components/{__tests__/PagePermissionGuard.verification.test.tsx → PagePermissionGuard.verification.test.tsx} +7 -7
- package/src/rbac/config.ts +58 -18
- package/src/rbac/{__tests__/engine.comprehensive.test.ts → engine.comprehensive.test.ts} +3 -3
- package/src/rbac/engine.test.ts +494 -0
- package/src/rbac/errors.ts +89 -55
- package/src/rbac/hooks/permissions/runPermissionCheck.ts +77 -0
- package/src/rbac/hooks/permissions/{__tests__/useAccessLevel.test.ts → useAccessLevel.test.ts} +40 -40
- package/src/rbac/hooks/permissions/useAccessLevel.ts +16 -6
- package/src/rbac/hooks/permissions/{__tests__/useCan.test.ts → useCan.test.ts} +41 -41
- package/src/rbac/hooks/permissions/useCan.ts +170 -252
- package/src/rbac/hooks/permissions/{__tests__/useMultiplePermissions.test.ts → useMultiplePermissions.test.ts} +49 -49
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +6 -2
- package/src/rbac/hooks/permissions/{__tests__/usePermissions.test.ts → usePermissions.test.ts} +10 -12
- package/src/rbac/hooks/permissions/usePermissions.ts +36 -65
- package/src/rbac/hooks/useCan.test.ts +42 -42
- package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
- package/src/rbac/hooks/usePageGuardScope.ts +117 -0
- package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
- package/src/rbac/hooks/{__tests__/usePermissions.integration.test.ts → usePermissions.integration.test.ts} +9 -9
- package/src/{__tests__/hooks/usePermissions.test.ts → rbac/hooks/usePermissions.stability.test.ts} +18 -18
- package/src/rbac/hooks/usePermissions.test.ts +54 -54
- package/src/rbac/hooks/useRBAC.test.ts +313 -217
- package/src/rbac/hooks/useRBAC.ts +145 -81
- package/src/rbac/hooks/useResourcePermissions.test.ts +25 -25
- package/src/rbac/hooks/useResourcePermissions.ts +68 -134
- package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +27 -112
- package/src/rbac/hooks/useRoleManagement.ts +153 -585
- package/src/rbac/hooks/{__tests__/useSecureSupabase.test.ts → useSecureSupabase.test.ts} +17 -17
- package/src/rbac/hooks/useSecureSupabase.ts +10 -2
- package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
- package/src/rbac/{__tests__/performance.test.ts → performance.test.ts} +1 -1
- package/src/rbac/{__tests__/rbac-core.test.tsx → rbac-core.test.tsx} +3 -3
- package/src/rbac/{__tests__/rbac-engine-core-logic.test.ts → rbac-engine-core-logic.test.ts} +2 -2
- package/src/rbac/{__tests__/rbac-engine-simplified.test.ts → rbac-engine-simplified.test.ts} +3 -3
- package/src/rbac/{__tests__/rbac-functions.test.ts → rbac-functions.test.ts} +57 -0
- package/src/rbac/{__tests__/rbac-role-isolation.test.ts → rbac-role-isolation.test.ts} +2 -2
- package/src/rbac/request-deduplication.test.ts +14 -9
- package/src/rbac/request-deduplication.ts +5 -4
- package/src/rbac/{__tests__/scenarios.user-role.test.tsx → scenarios.user-role.test.tsx} +23 -23
- package/src/rbac/secureClient.test.ts +514 -83
- package/src/rbac/secureClient.ts +8 -2
- package/src/rbac/security.test.ts +323 -0
- package/src/rbac/types/roleManagement.ts +66 -0
- package/src/rbac/utils/{__tests__/clientSecurity.test.ts → clientSecurity.test.ts} +4 -4
- package/src/rbac/utils/{__tests__/contextValidator.test.ts → contextValidator.test.ts} +4 -4
- package/src/rbac/utils/contextValidator.ts +5 -1
- package/src/rbac/utils/{__tests__/deep-equal.test.ts → deep-equal.test.ts} +1 -1
- package/src/rbac/utils/{__tests__/eventContext.test.ts → eventContext.test.ts} +36 -21
- package/src/rbac/utils/eventContext.ts +37 -33
- package/src/rbac/utils/fetchPermissionMap.ts +13 -0
- package/src/rbac/utils/permissionMapHelpers.ts +34 -0
- package/src/rbac/utils/roleManagementRpc.ts +303 -0
- package/src/services/{__tests__/AuthService.edge-cases.test.ts → AuthService.edge-cases.test.ts} +19 -19
- package/src/services/{__tests__/AuthService.restoreSession.test.ts → AuthService.restoreSession.test.ts} +2 -2
- package/src/services/{__tests__/AuthService.test.ts → AuthService.test.ts} +89 -55
- package/src/services/AuthService.ts +184 -205
- package/src/services/{__tests__/BaseService.edge-cases.test.ts → BaseService.edge-cases.test.ts} +3 -3
- package/src/services/{__tests__/BaseService.test.ts → BaseService.test.ts} +2 -2
- package/src/services/{__tests__/EventService.edge-cases.test.ts → EventService.edge-cases.test.ts} +27 -24
- package/src/services/{__tests__/EventService.eventColours.test.ts → EventService.eventColours.test.ts} +1 -1
- package/src/services/{__tests__/EventService.test.ts → EventService.test.ts} +256 -24
- package/src/services/EventService.ts +242 -312
- package/src/services/{__tests__/InactivityService.edge-cases.test.ts → InactivityService.edge-cases.test.ts} +3 -3
- package/src/services/{__tests__/InactivityService.lifecycle.test.ts → InactivityService.lifecycle.test.ts} +2 -2
- package/src/services/{__tests__/InactivityService.test.ts → InactivityService.test.ts} +179 -4
- package/src/services/InactivityService.ts +172 -213
- package/src/services/{__tests__/OrganisationService.edge-cases.test.ts → OrganisationService.edge-cases.test.ts} +5 -5
- package/src/services/{__tests__/OrganisationService.pagination.test.ts → OrganisationService.pagination.test.ts} +4 -4
- package/src/services/{__tests__/OrganisationService.test.ts → OrganisationService.test.ts} +410 -7
- package/src/services/OrganisationService.ts +184 -238
- package/src/services/base/BaseService.test.ts +1 -1
- package/src/services/interfaces/{__tests__/IAuthService.test.ts → IAuthService.test.ts} +21 -27
- package/src/services/interfaces/IAuthService.ts +10 -9
- package/src/services/interfaces/{__tests__/IEventService.test.ts → IEventService.test.ts} +4 -4
- package/src/services/interfaces/{__tests__/IInactivityService.test.ts → IInactivityService.test.ts} +3 -3
- package/src/services/interfaces/{__tests__/IOrganisationService.test.ts → IOrganisationService.test.ts} +3 -3
- package/src/styles/core.css +243 -12
- package/src/theming/{__tests__/parseEventColours.test.ts → parseEventColours.test.ts} +1 -1
- package/src/theming/{__tests__/runtime.test.ts → runtime.test.ts} +8 -17
- package/src/theming/runtime.ts +71 -2
- package/src/types/api-result.ts +53 -0
- package/src/types/{__tests__/core.test.ts → core.test.ts} +2 -2
- package/src/types/{__tests__/database-generated.test.ts → database-generated.test.ts} +3 -3
- package/src/types/database.generated.ts +45 -10
- package/src/types/event.ts +38 -18
- package/src/types/{__tests__/file-reference.test.ts → file-reference.test.ts} +13 -13
- package/src/types/file-reference.ts +37 -12
- package/src/types/{__tests__/guards.test.ts → guards.test.ts} +2 -2
- package/src/types/{__tests__/index.test.ts → index.test.ts} +2 -2
- package/src/types/index.ts +3 -0
- package/src/types/{__tests__/organisation.roles.test.ts → organisation.roles.test.ts} +1 -1
- package/src/types/{__tests__/organisation.test.ts → organisation.test.ts} +3 -31
- package/src/types/organisation.ts +15 -15
- package/src/types/supabase.ts +13 -4
- package/src/types/{__tests__/theme.test.ts → theme.test.ts} +1 -1
- package/src/types/{__tests__/type-validation.test.ts → type-validation.test.ts} +1 -1
- package/src/types/{__tests__/validation.test.ts → validation.test.ts} +2 -2
- package/src/utils/app/appIdResolver.test.ts +98 -71
- package/src/utils/app/appIdResolver.ts +31 -20
- package/src/utils/{__tests__/appConfig.unit.test.ts → appConfig.unit.test.ts} +1 -1
- package/src/utils/{__tests__/audit.unit.test.ts → audit.unit.test.ts} +1 -1
- package/src/utils/{__tests__/auth-utils.unit.test.ts → auth-utils.unit.test.ts} +16 -17
- package/src/utils/{__tests__/bundleAnalysis.unit.test.ts → bundleAnalysis.unit.test.ts} +35 -35
- package/src/utils/{__tests__/cn.unit.test.ts → cn.unit.test.ts} +1 -1
- package/src/utils/context/organisationContext.test.ts +105 -91
- package/src/utils/context/organisationContext.ts +29 -40
- package/src/utils/core/{__tests__/cn.test.ts → cn.test.ts} +3 -3
- package/src/utils/core/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +2 -2
- package/src/utils/core/{__tests__/logger.test.ts → logger.test.ts} +2 -2
- package/src/utils/core/mergeRefs.ts +24 -0
- package/src/utils/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +1 -1
- package/src/utils/{__tests__/deviceFingerprint.unit.test.ts → deviceFingerprint.unit.test.ts} +1 -1
- package/src/utils/dynamic/createLazyComponent.tsx +9 -1
- package/src/utils/dynamic/{__tests__/dynamicUtils.test.ts → dynamicUtils.test.ts} +2 -2
- package/src/utils/dynamic/{__tests__/lazyLoad.test.tsx → lazyLoad.test.tsx} +2 -2
- package/src/utils/{__tests__/dynamicUtils.unit.test.ts → dynamicUtils.unit.test.ts} +1 -1
- package/src/utils/file-reference/{__tests__/file-reference.test.ts → file-reference.test.ts} +214 -289
- package/src/utils/file-reference/index.ts +330 -347
- package/src/utils/{__tests__/formatDate.unit.test.ts → formatDate.unit.test.ts} +2 -2
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +1 -1
- package/src/utils/formatting/formatNumber.test.ts +1 -1
- package/src/utils/{__tests__/formatting.unit.test.ts → formatting.unit.test.ts} +1 -1
- package/src/utils/google-places/googlePlacesUtils.test.ts +70 -48
- package/src/utils/google-places/googlePlacesUtils.ts +67 -99
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +25 -22
- package/src/utils/google-places/loadGoogleMapsScript.ts +138 -117
- package/src/utils/{__tests__/index.unit.test.ts → index.unit.test.ts} +1 -1
- package/src/utils/{__tests__/lazyLoad.unit.test.tsx → lazyLoad.unit.test.tsx} +13 -14
- package/src/utils/location/location.test.ts +1 -1
- package/src/utils/{__tests__/logger.unit.test.ts → logger.unit.test.ts} +1 -1
- package/src/utils/{__tests__/organisationContext.unit.test.ts → organisationContext.unit.test.ts} +37 -48
- package/src/utils/performance/{__tests__/bundleAnalysis.test.ts → bundleAnalysis.test.ts} +2 -2
- package/src/utils/performance/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
- package/src/utils/performance/{__tests__/performanceBudgets.test.ts → performanceBudgets.test.ts} +2 -2
- package/src/utils/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
- package/src/utils/{__tests__/performanceBudgets.unit.test.ts → performanceBudgets.unit.test.ts} +2 -2
- package/src/utils/{__tests__/permissionTypes.unit.test.ts → permissionTypes.unit.test.ts} +1 -1
- package/src/utils/{__tests__/permissionUtils.unit.test.ts → permissionUtils.unit.test.ts} +1 -1
- package/src/utils/permissions/{__tests__/permissionTypes.test.ts → permissionTypes.test.ts} +2 -2
- package/src/utils/persistence/{__tests__/keyDerivation.test.ts → keyDerivation.test.ts} +2 -2
- package/src/utils/persistence/{__tests__/sensitiveFieldDetection.test.ts → sensitiveFieldDetection.test.ts} +2 -2
- package/src/utils/{__tests__/request-deduplication.test.ts → request-deduplication.test.ts} +2 -2
- package/src/utils/{__tests__/sanitization.unit.test.ts → sanitization.unit.test.ts} +1 -1
- package/src/utils/{__tests__/schemaUtils.unit.test.ts → schemaUtils.unit.test.ts} +1 -1
- package/src/utils/{__tests__/secureDataAccess.unit.test.ts → secureDataAccess.unit.test.ts} +2 -2
- package/src/utils/{__tests__/secureErrors.unit.test.ts → secureErrors.unit.test.ts} +4 -4
- package/src/utils/{__tests__/secureStorage.unit.test.ts → secureStorage.unit.test.ts} +1 -1
- package/src/utils/security/auth-utils.ts +34 -23
- package/src/utils/security/secureDataAccess.ts +241 -281
- package/src/utils/security/secureErrors.test.ts +1 -1
- package/src/utils/security/secureStorage.test.ts +1 -1
- package/src/utils/security/security.test.ts +25 -17
- package/src/utils/security/security.ts +15 -18
- package/src/utils/security/securityMonitor.test.ts +1 -1
- package/src/utils/{__tests__/security.unit.test.ts → security.unit.test.ts} +21 -15
- package/src/utils/{__tests__/securityMonitor.unit.test.ts → securityMonitor.unit.test.ts} +1 -1
- package/src/utils/{__tests__/sessionTracking.unit.test.ts → sessionTracking.unit.test.ts} +12 -12
- package/src/utils/storage/{__tests__/config.unit.test.ts → config.unit.test.ts} +2 -2
- package/src/utils/storage/helpers.test.ts +88 -102
- package/src/utils/storage/helpers.ts +173 -251
- package/src/utils/storage/{__tests__/index.unit.test.ts → index.unit.test.ts} +3 -3
- package/src/utils/storage/types.ts +7 -0
- package/src/utils/supabase/createBaseClient.test.ts +1 -1
- package/src/utils/timezone/timezone.test.ts +1 -1
- package/src/utils/{__tests__/timezone.test.ts → timezone.test.ts} +2 -2
- package/src/utils/validation/{__tests__/common.test.ts → common.test.ts} +2 -2
- package/src/utils/validation/{__tests__/csrf.test.ts → csrf.test.ts} +56 -28
- package/src/utils/validation/csrf.ts +42 -41
- package/src/utils/validation/{__tests__/htmlSanitization.unit.test.ts → htmlSanitization.unit.test.ts} +2 -2
- package/src/utils/validation/{__tests__/passwordSchema.test.ts → passwordSchema.test.ts} +2 -2
- package/src/utils/validation/{__tests__/schema.test.ts → schema.test.ts} +2 -2
- package/src/utils/validation/{__tests__/sqlInjectionProtection.test.ts → sqlInjectionProtection.test.ts} +2 -2
- package/src/utils/validation/{__tests__/user.test.ts → user.test.ts} +2 -2
- package/src/utils/validation/{__tests__/validation.test.ts → validation.test.ts} +2 -2
- package/src/utils/validation/{__tests__/validationUtils.test.ts → validationUtils.test.ts} +2 -2
- package/src/utils/{__tests__/validation.unit.test.ts → validation.unit.test.ts} +1 -1
- package/src/utils/{__tests__/validationUtils.unit.test.ts → validationUtils.unit.test.ts} +5 -2
- package/dist/UnifiedAuthProvider-BBD2PS3Q.js +0 -7
- package/dist/chunk-KPYQWGFQ.js +0 -183
- package/dist/types-D05dCGma.d.ts +0 -521
- package/scripts/eslint-audit.cjs +0 -222
- package/scripts/generate-docs.js +0 -157
- package/scripts/install-cursor-rules.cjs +0 -255
- package/scripts/install-eslint-config.cjs +0 -349
- package/scripts/setup-build-cache.js +0 -73
- package/scripts/validate-pre-publish.js +0 -145
- package/src/__tests__/integration/UserProfile.test.tsx +0 -124
- package/src/__tests__/public-recipe-view.test.ts +0 -228
- package/src/__tests__/rls-policies.test.ts +0 -472
- package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
- package/src/components/DataTable/components/DataTableLayout.tsx +0 -584
- package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -395
- package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +0 -467
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -358
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -957
- package/src/components/DataTable/core/ActionManager.ts +0 -235
- package/src/components/DataTable/core/ColumnManager.ts +0 -204
- package/src/components/DataTable/core/DataManager.ts +0 -190
- package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
- package/src/components/DataTable/core/PluginRegistry.ts +0 -229
- package/src/components/DataTable/core/StateManager.ts +0 -312
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -235
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -141
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -178
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -133
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -142
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -158
- package/src/components/DataTable/core/interfaces.ts +0 -338
- package/src/components/DataTable/types.ts +0 -764
- package/src/hooks/public/usePublicFileDisplay.ts +0 -534
- package/src/hooks/useFileDisplay.ts +0 -748
- package/src/providers/OrganisationProvider.test.tsx +0 -40
- package/src/providers/OrganisationProvider.tsx +0 -92
- package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
- package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -616
- package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +0 -591
- package/src/rbac/__tests__/cache-invalidation.test.ts +0 -393
- /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
- /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
- /package/src/hooks/{__tests__/useApiFetch.unit.test.ts → useApiFetch.unit.test.ts} +0 -0
- /package/src/providers/{__tests__/README.md → README.md} +0 -0
- /package/src/rbac/{__tests__/index.test.ts → index.test.ts} +0 -0
- /package/src/rbac/{__tests__/rbac-integration.test.ts → rbac-integration.test.ts} +0 -0
- /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 '../../
|
|
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
|
-
|
|
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
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
267
|
+
const rpcUserId = await this.resolveRpcUserId(authenticatedUserId, options);
|
|
128
268
|
|
|
129
|
-
|
|
130
|
-
const
|
|
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
|
|
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
|
-
|
|
191
|
-
await
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
297
|
+
return ok(fetchResult.data);
|
|
236
298
|
} catch (error) {
|
|
237
299
|
log.error('Error creating file reference:', error);
|
|
238
|
-
|
|
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;
|
|
319
|
+
return ok(null);
|
|
262
320
|
}
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
const fileRef =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(''),
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
}
|