@jmruthers/pace-core 0.5.136 → 0.5.137
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/dist/{DataTable-CYOHOX3O.js → DataTable-6M4L6BI2.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-BCIBECNB.js} +5 -5
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BLCXZEYF.js} +3 -3
- package/dist/{chunk-HJGGOMQ6.js → chunk-HAWZXGR2.js} +147 -103
- package/dist/chunk-HAWZXGR2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-KYRHUBIU.js} +417 -319
- package/dist/chunk-KYRHUBIU.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +79 -10
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-6M4L6BI2.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-BCIBECNB.js.map} +0 -0
- /package/dist/{chunk-L6PGMCMD.js.map → chunk-BLCXZEYF.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /package/examples/{components → components 2}/index.ts +0 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// File Reference Service
|
|
2
|
+
// Provides CRUD operations for the centralized file reference system
|
|
3
|
+
|
|
4
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
5
|
+
import {
|
|
6
|
+
FileReference,
|
|
7
|
+
FileUploadOptions,
|
|
8
|
+
FileReferenceService,
|
|
9
|
+
FileUploadResult,
|
|
10
|
+
FileCategory
|
|
11
|
+
} from '../types/file-reference';
|
|
12
|
+
import { uploadFile, getPublicUrl, getSignedUrl, deleteFile, extractFileMetadata } from './storage/helpers';
|
|
13
|
+
import { setOrganisationContext } from './organisationContext';
|
|
14
|
+
import { invalidateFileDisplayCache } from '../hooks/useFileDisplay';
|
|
15
|
+
|
|
16
|
+
export class FileReferenceServiceImpl implements FileReferenceService {
|
|
17
|
+
constructor(private supabase: SupabaseClient) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a file reference by uploading a file to storage and linking it in the database.
|
|
21
|
+
*
|
|
22
|
+
* Storage Flow:
|
|
23
|
+
* 1. Upload file to storage bucket first (files or public-files based on is_public flag)
|
|
24
|
+
* - Path format: {orgId}/{category}/{timestamp-uuid-filename}
|
|
25
|
+
* - Bucket selection: 'files' (private) or 'public-files' (public)
|
|
26
|
+
* 2. Extract file metadata (dimensions, hash, etc.)
|
|
27
|
+
* 3. Set organisation context for RLS policies
|
|
28
|
+
* 4. Create database reference via RPC function
|
|
29
|
+
* 5. If DB insert fails, rollback by deleting uploaded file
|
|
30
|
+
*
|
|
31
|
+
* This ensures atomicity: either both storage and DB succeed, or both are cleaned up.
|
|
32
|
+
*/
|
|
33
|
+
async createFileReference(options: FileUploadOptions, file: File): Promise<FileReference> {
|
|
34
|
+
try {
|
|
35
|
+
|
|
36
|
+
// Validate required options
|
|
37
|
+
if (!options.organisation_id) {
|
|
38
|
+
throw new Error('organisation_id is required for file upload');
|
|
39
|
+
}
|
|
40
|
+
if (!options.table_name) {
|
|
41
|
+
throw new Error('table_name is required for file upload');
|
|
42
|
+
}
|
|
43
|
+
if (!options.record_id) {
|
|
44
|
+
throw new Error('record_id is required for file upload');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 1: Upload file to storage bucket first
|
|
48
|
+
// This generates a unique path: {orgId}/{category}/{timestamp-uuid-filename}
|
|
49
|
+
// Bucket is automatically selected based on is_public flag
|
|
50
|
+
const uploadResult = await uploadFile(this.supabase, file, {
|
|
51
|
+
appName: 'file-reference',
|
|
52
|
+
orgId: options.organisation_id,
|
|
53
|
+
isPublic: options.is_public || false,
|
|
54
|
+
customPath: options.category // Use category as the custom path segment
|
|
55
|
+
});
|
|
56
|
+
if (!uploadResult.success) {
|
|
57
|
+
throw new Error(`Failed to upload file: ${uploadResult.error}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!uploadResult.path) {
|
|
61
|
+
throw new Error('File upload did not return a path');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const filePath = uploadResult.path;
|
|
65
|
+
|
|
66
|
+
// Step 2: Extract file metadata (dimensions, hash, etc.)
|
|
67
|
+
const metadata = await extractFileMetadata(file, {
|
|
68
|
+
appName: 'file-reference',
|
|
69
|
+
orgId: options.organisation_id,
|
|
70
|
+
isPublic: options.is_public || false
|
|
71
|
+
}, 'system');
|
|
72
|
+
|
|
73
|
+
// Step 3: Set organisation context in database session before creating file reference
|
|
74
|
+
// This ensures RLS policies can check the organisation context
|
|
75
|
+
await setOrganisationContext(this.supabase, options.organisation_id);
|
|
76
|
+
|
|
77
|
+
// Step 4: Create file reference in database using RPC function
|
|
78
|
+
// This links the storage path to the record in file_references table
|
|
79
|
+
const { data, error } = await this.supabase
|
|
80
|
+
.rpc('data_file_reference_create', {
|
|
81
|
+
p_table_name: options.table_name,
|
|
82
|
+
p_record_id: options.record_id,
|
|
83
|
+
p_file_path: filePath, // Storage path from step 1
|
|
84
|
+
p_organisation_id: options.organisation_id,
|
|
85
|
+
p_app_id: options.app_id,
|
|
86
|
+
p_file_metadata: {
|
|
87
|
+
fileName: file.name,
|
|
88
|
+
fileType: file.type,
|
|
89
|
+
fileSize: file.size,
|
|
90
|
+
category: options.category,
|
|
91
|
+
...metadata,
|
|
92
|
+
...options.custom_metadata
|
|
93
|
+
},
|
|
94
|
+
p_is_public: options.is_public || false
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Step 5: Rollback - if database insert fails, clean up uploaded file
|
|
98
|
+
if (error) {
|
|
99
|
+
await deleteFile(this.supabase, filePath, options.is_public || false);
|
|
100
|
+
throw new Error(`Failed to create file reference: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get the created file reference
|
|
104
|
+
const { data: fileRef, error: fetchError } = await this.supabase
|
|
105
|
+
.from('file_references')
|
|
106
|
+
.select('*')
|
|
107
|
+
.eq('id', data)
|
|
108
|
+
.single();
|
|
109
|
+
|
|
110
|
+
if (fetchError || !fileRef) {
|
|
111
|
+
throw new Error(`Failed to fetch created file reference: ${fetchError?.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Invalidate cache for this file display entry so newly uploaded files appear immediately
|
|
115
|
+
invalidateFileDisplayCache(
|
|
116
|
+
options.table_name,
|
|
117
|
+
options.record_id,
|
|
118
|
+
options.organisation_id,
|
|
119
|
+
options.category
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return fileRef as FileReference;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error creating file reference:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getFileReference(table_name: string, record_id: string, organisation_id: string): Promise<FileReference | null> {
|
|
130
|
+
try {
|
|
131
|
+
const { data, error } = await this.supabase
|
|
132
|
+
.from('file_references')
|
|
133
|
+
.select('*')
|
|
134
|
+
.eq('table_name', table_name)
|
|
135
|
+
.eq('record_id', record_id)
|
|
136
|
+
.eq('organisation_id', organisation_id)
|
|
137
|
+
.single();
|
|
138
|
+
|
|
139
|
+
if (error) {
|
|
140
|
+
if (error.code === 'PGRST116') {
|
|
141
|
+
return null; // No rows found
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Failed to get file reference: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return data as FileReference;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error getting file reference:', error);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getFileUrl(table_name: string, record_id: string, organisation_id: string): Promise<string | null> {
|
|
154
|
+
try {
|
|
155
|
+
// Get file reference to check if it's public
|
|
156
|
+
const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
|
|
157
|
+
if (!fileRef) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// For public files, RPC returns file path - generate public URL client-side
|
|
162
|
+
if (fileRef.is_public) {
|
|
163
|
+
const { data: pathData } = await this.supabase
|
|
164
|
+
.rpc('data_file_reference_url_get', {
|
|
165
|
+
p_table_name: table_name,
|
|
166
|
+
p_record_id: record_id,
|
|
167
|
+
p_organisation_id: organisation_id
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!pathData) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate public URL using bucket-aware helper
|
|
175
|
+
return getPublicUrl(this.supabase, pathData, true);
|
|
176
|
+
} else {
|
|
177
|
+
// For private files, use signed URL
|
|
178
|
+
return await this.getSignedUrl(table_name, record_id, organisation_id);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Error getting file URL:', error);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getSignedUrl(table_name: string, record_id: string, organisation_id: string, expires_in: number = 3600): Promise<string | null> {
|
|
187
|
+
try {
|
|
188
|
+
// Get file path from RPC function
|
|
189
|
+
const { data: filePath, error } = await this.supabase
|
|
190
|
+
.rpc('data_file_reference_signed_url_get', {
|
|
191
|
+
p_table_name: table_name,
|
|
192
|
+
p_record_id: record_id,
|
|
193
|
+
p_organisation_id: organisation_id,
|
|
194
|
+
p_expires_in: expires_in
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (error) {
|
|
198
|
+
throw new Error(`Failed to get signed URL: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!filePath) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Generate signed URL client-side using bucket-aware helper (files bucket for private files)
|
|
206
|
+
const signedUrlResult = await getSignedUrl(this.supabase, filePath, {
|
|
207
|
+
appName: 'file-reference',
|
|
208
|
+
orgId: organisation_id,
|
|
209
|
+
expiresIn: expires_in
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return signedUrlResult?.url || null;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Error getting signed URL:', error);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async updateFileReference(id: string, updates: Partial<FileReference>): Promise<FileReference> {
|
|
220
|
+
try {
|
|
221
|
+
const { data, error } = await this.supabase
|
|
222
|
+
.from('file_references')
|
|
223
|
+
.update(updates)
|
|
224
|
+
.eq('id', id)
|
|
225
|
+
.select()
|
|
226
|
+
.single();
|
|
227
|
+
|
|
228
|
+
if (error) {
|
|
229
|
+
throw new Error(`Failed to update file reference: ${error.message}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return data as FileReference;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Error updating file reference:', error);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async deleteFileReference(table_name: string, record_id: string, organisation_id: string, delete_file: boolean = false): Promise<boolean> {
|
|
240
|
+
try {
|
|
241
|
+
// Get file reference first to determine bucket
|
|
242
|
+
const fileRef = await this.getFileReference(table_name, record_id, organisation_id);
|
|
243
|
+
|
|
244
|
+
const { error } = await this.supabase
|
|
245
|
+
.rpc('data_file_reference_delete', {
|
|
246
|
+
p_table_name: table_name,
|
|
247
|
+
p_record_id: record_id,
|
|
248
|
+
p_organisation_id: organisation_id,
|
|
249
|
+
p_delete_file: delete_file
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (error) {
|
|
253
|
+
throw new Error(`Failed to delete file reference: ${error.message}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If delete_file is true and we have the file reference, delete from storage
|
|
257
|
+
if (delete_file && fileRef) {
|
|
258
|
+
await deleteFile(this.supabase, fileRef.file_path, fileRef.is_public || false);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('Error deleting file reference:', error);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async listFileReferences(table_name: string, record_id: string, organisation_id: string): Promise<FileReference[]> {
|
|
269
|
+
try {
|
|
270
|
+
const { data, error } = await this.supabase
|
|
271
|
+
.rpc('data_file_reference_list', {
|
|
272
|
+
p_table_name: table_name,
|
|
273
|
+
p_record_id: record_id,
|
|
274
|
+
p_organisation_id: organisation_id
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (error) {
|
|
278
|
+
throw new Error(`Failed to list file references: ${error.message}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// RPC returns partial data, need to fetch full file references
|
|
282
|
+
if (!data || data.length === 0) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fetch full file reference data for each ID
|
|
287
|
+
const ids = data.map((item: any) => item.id);
|
|
288
|
+
const { data: fullData, error: fetchError } = await this.supabase
|
|
289
|
+
.from('file_references')
|
|
290
|
+
.select('*')
|
|
291
|
+
.in('id', ids);
|
|
292
|
+
|
|
293
|
+
if (fetchError) {
|
|
294
|
+
throw new Error(`Failed to fetch file references: ${fetchError.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (fullData || []) as FileReference[];
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error('Error listing file references:', error);
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async getFileCount(table_name: string, record_id: string, organisation_id: string): Promise<number> {
|
|
305
|
+
try {
|
|
306
|
+
const { data, error } = await this.supabase
|
|
307
|
+
.rpc('data_file_reference_count_get', {
|
|
308
|
+
p_table_name: table_name,
|
|
309
|
+
p_record_id: record_id,
|
|
310
|
+
p_organisation_id: organisation_id
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (error) {
|
|
314
|
+
throw new Error(`Failed to get file count: ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return data || 0;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Error getting file count:', error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async getFileReferenceById(id: string, organisation_id: string): Promise<FileReference | null> {
|
|
325
|
+
try {
|
|
326
|
+
const { data, error } = await this.supabase
|
|
327
|
+
.rpc('data_file_reference_get', {
|
|
328
|
+
p_file_reference_id: id,
|
|
329
|
+
p_organisation_id: organisation_id
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (error) {
|
|
333
|
+
throw new Error(`Failed to get file reference by ID: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!data || data.length === 0) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return data[0] as FileReference;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error('Error getting file reference by ID:', error);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async getFilesByCategory(
|
|
348
|
+
table_name: string,
|
|
349
|
+
record_id: string,
|
|
350
|
+
category: FileCategory,
|
|
351
|
+
organisation_id: string
|
|
352
|
+
): Promise<FileReference[]> {
|
|
353
|
+
try {
|
|
354
|
+
// CRITICAL: Use RPC function to get files by category - this correctly filters on file_metadata->>'category'
|
|
355
|
+
// NOTE: We MUST use RPC function. Direct queries with .eq('category', ...) will FAIL with HTTP 406
|
|
356
|
+
// because there is NO 'category' column. The category is stored in the JSONB file_metadata field.
|
|
357
|
+
console.log('[FileReferenceService.getFilesByCategory] Calling RPC function:', {
|
|
358
|
+
table_name,
|
|
359
|
+
record_id,
|
|
360
|
+
category,
|
|
361
|
+
organisation_id
|
|
362
|
+
});
|
|
363
|
+
const { data, error } = await this.supabase
|
|
364
|
+
.rpc('data_file_reference_by_category_list', {
|
|
365
|
+
p_table_name: table_name,
|
|
366
|
+
p_record_id: record_id,
|
|
367
|
+
p_category: category,
|
|
368
|
+
p_organisation_id: organisation_id
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (error) {
|
|
372
|
+
// Provide clear error message about category filtering
|
|
373
|
+
console.error('[FileReferenceService.getFilesByCategory] RPC ERROR:', {
|
|
374
|
+
error,
|
|
375
|
+
errorCode: error.code,
|
|
376
|
+
errorMessage: error.message,
|
|
377
|
+
errorDetails: error.details,
|
|
378
|
+
table_name,
|
|
379
|
+
record_id,
|
|
380
|
+
category,
|
|
381
|
+
organisation_id,
|
|
382
|
+
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.'
|
|
383
|
+
});
|
|
384
|
+
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.`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// RPC returns partial data with: id, file_path, file_metadata, is_public, created_at
|
|
388
|
+
// We have table_name, record_id, organisation_id from function parameters
|
|
389
|
+
// We can construct FileReference objects directly without another query (avoiding RLS issues)
|
|
390
|
+
console.log('[FileReferenceService.getFilesByCategory] RPC response received:', {
|
|
391
|
+
dataLength: data?.length || 0,
|
|
392
|
+
data: data ? data.slice(0, 2) : null, // Log first 2 items for debugging
|
|
393
|
+
fullDataAvailable: data?.every((item: any) => item.id && item.file_path && item.file_metadata)
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (!data || data.length === 0) {
|
|
397
|
+
console.log('[FileReferenceService.getFilesByCategory] No data from RPC, returning empty array');
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Construct FileReference objects from RPC response
|
|
402
|
+
// This avoids RLS issues with direct queries - the RPC already validated permissions
|
|
403
|
+
const fileReferences: FileReference[] = data
|
|
404
|
+
.filter((item: any) => {
|
|
405
|
+
// Verify category matches (defensive check)
|
|
406
|
+
const fileCategory = item.file_metadata?.category;
|
|
407
|
+
const matches = fileCategory === category;
|
|
408
|
+
if (!matches) {
|
|
409
|
+
console.warn('[FileReferenceService.getFilesByCategory] File category mismatch in RPC response:', {
|
|
410
|
+
fileId: item.id,
|
|
411
|
+
expectedCategory: category,
|
|
412
|
+
actualCategory: fileCategory
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return matches && item.id && item.file_path && item.file_metadata;
|
|
416
|
+
})
|
|
417
|
+
.map((item: any) => {
|
|
418
|
+
// Construct complete FileReference from RPC response + function parameters
|
|
419
|
+
const fileRef: FileReference = {
|
|
420
|
+
id: item.id,
|
|
421
|
+
table_name: table_name,
|
|
422
|
+
record_id: record_id,
|
|
423
|
+
file_path: item.file_path,
|
|
424
|
+
file_metadata: item.file_metadata || {},
|
|
425
|
+
organisation_id: organisation_id,
|
|
426
|
+
app_id: item.file_metadata?.app_id || '', // May not be in metadata, use empty string
|
|
427
|
+
is_public: item.is_public ?? false,
|
|
428
|
+
created_at: item.created_at || new Date().toISOString(),
|
|
429
|
+
updated_at: item.created_at || new Date().toISOString() // RPC doesn't return updated_at, use created_at
|
|
430
|
+
};
|
|
431
|
+
return fileRef;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
console.log('[FileReferenceService.getFilesByCategory] Constructed file references from RPC response:', {
|
|
435
|
+
count: fileReferences.length,
|
|
436
|
+
firstFileId: fileReferences[0]?.id,
|
|
437
|
+
firstFilePath: fileReferences[0]?.file_path,
|
|
438
|
+
firstFileIsPublic: fileReferences[0]?.is_public,
|
|
439
|
+
firstFileHasAllRequiredFields: !!(fileReferences[0]?.id && fileReferences[0]?.file_path && fileReferences[0]?.file_metadata && fileReferences[0]?.table_name && fileReferences[0]?.record_id && fileReferences[0]?.organisation_id)
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
return fileReferences;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error('Error getting files by category:', error);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async uploadMultipleFiles(
|
|
450
|
+
options: FileUploadOptions,
|
|
451
|
+
files: File[]
|
|
452
|
+
): Promise<import('../types/file-reference').BulkUploadResult> {
|
|
453
|
+
const success: FileReference[] = [];
|
|
454
|
+
const failed: { file: File; error: string }[] = [];
|
|
455
|
+
const results: Array<{ file: File; result: FileUploadResult | null; error?: string }> = [];
|
|
456
|
+
|
|
457
|
+
// Upload files sequentially to avoid overwhelming the server
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
try {
|
|
460
|
+
const fileReference = await this.createFileReference(options, file);
|
|
461
|
+
|
|
462
|
+
success.push(fileReference);
|
|
463
|
+
results.push({
|
|
464
|
+
file,
|
|
465
|
+
result: {
|
|
466
|
+
file_reference: fileReference,
|
|
467
|
+
file_url: fileReference.is_public
|
|
468
|
+
? getPublicUrl(this.supabase, fileReference.file_path, true)
|
|
469
|
+
: '',
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
474
|
+
failed.push({ file, error: message });
|
|
475
|
+
results.push({ file, result: null, error: message });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
success,
|
|
481
|
+
failed,
|
|
482
|
+
total: results.length,
|
|
483
|
+
successful: success.length,
|
|
484
|
+
results,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Factory function to create file reference service
|
|
490
|
+
export function createFileReferenceService(supabase: SupabaseClient): FileReferenceService {
|
|
491
|
+
return new FileReferenceServiceImpl(supabase);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Helper function to upload file and create reference in one operation
|
|
495
|
+
export async function uploadFileWithReference(
|
|
496
|
+
supabase: SupabaseClient,
|
|
497
|
+
options: FileUploadOptions,
|
|
498
|
+
file: File
|
|
499
|
+
): Promise<FileUploadResult> {
|
|
500
|
+
|
|
501
|
+
const service = createFileReferenceService(supabase);
|
|
502
|
+
const fileReference = await service.createFileReference(options, file);
|
|
503
|
+
|
|
504
|
+
const fileUrl = options.is_public
|
|
505
|
+
? getPublicUrl(supabase, fileReference.file_path, true)
|
|
506
|
+
: await getSignedUrl(supabase, fileReference.file_path, {
|
|
507
|
+
appName: 'file-reference',
|
|
508
|
+
orgId: options.organisation_id,
|
|
509
|
+
expiresIn: 3600
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const urlString = typeof fileUrl === 'string' ? fileUrl : fileUrl?.url || '';
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
file_reference: fileReference,
|
|
516
|
+
file_url: urlString,
|
|
517
|
+
signed_url: options.is_public ? undefined : urlString || undefined
|
|
518
|
+
};
|
|
519
|
+
}
|