@jmruthers/pace-core 0.5.185 → 0.5.187
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/chunk-SAUPYVLF.js.map +1 -0
- package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
- package/dist/chunk-ULX5FYEM.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
- package/dist/hooks.d.ts +265 -6
- package/dist/hooks.js +148 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +25 -10
- package/dist/index.js +65 -30
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +125 -8
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
- package/dist/utils.d.ts +214 -4
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- 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/Logger.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/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +6 -6
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.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/ComplianceResult.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/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.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/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- 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 +33 -9
- package/docs/api/interfaces/FileUploadProps.md +36 -14
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.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/LoggerConfig.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 +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- 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/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +27 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.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/RuntimeComplianceResult.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/SetupIssue.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/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.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 +328 -69
- package/docs/api-reference/components.md +26 -12
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-reference-system.md +24 -2
- package/docs/implementation-guides/file-upload-storage.md +38 -1
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.js +512 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +7 -1
- package/src/components/Header/Header.tsx +2 -5
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +9 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +21 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +3 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
- package/src/utils/file-reference/index.ts +56 -17
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +211 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-AISXLWGZ.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-HC67NW5K.js.map +0 -1
- package/dist/chunk-IXSNYUCT.js.map +0 -1
- package/dist/chunk-MX3EIJGQ.js.map +0 -1
- package/dist/chunk-OKI34GZD.js.map +0 -1
- package/dist/chunk-STTZQK2I.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Deduplication for RBAC Permission Checks
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/RequestDeduplication
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* This module provides request deduplication to prevent multiple identical
|
|
8
|
+
* permission checks from being made simultaneously. When multiple components
|
|
9
|
+
* request the same permission at the same time, they share the same promise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { PermissionCheck } from './types';
|
|
13
|
+
import { RBACCache } from './cache';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map of in-flight permission check requests
|
|
17
|
+
* Key: cache key string, Value: Promise<boolean>
|
|
18
|
+
*/
|
|
19
|
+
const inFlightRequests = new Map<string, Promise<boolean>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a deduplication key from permission check input
|
|
23
|
+
*
|
|
24
|
+
* @param input - Permission check input
|
|
25
|
+
* @returns Deduplication key string
|
|
26
|
+
*/
|
|
27
|
+
function generateDeduplicationKey(input: PermissionCheck): string {
|
|
28
|
+
return RBACCache.generatePermissionKey({
|
|
29
|
+
userId: input.userId,
|
|
30
|
+
organisationId: input.scope.organisationId!,
|
|
31
|
+
eventId: input.scope.eventId,
|
|
32
|
+
appId: input.scope.appId,
|
|
33
|
+
permission: input.permission,
|
|
34
|
+
pageId: input.pageId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get or create a deduplicated permission check request
|
|
40
|
+
*
|
|
41
|
+
* If a request for the same permission is already in-flight, returns the existing promise.
|
|
42
|
+
* Otherwise, creates a new request and tracks it.
|
|
43
|
+
*
|
|
44
|
+
* @param input - Permission check input
|
|
45
|
+
* @param checkFn - Function to perform the actual permission check
|
|
46
|
+
* @returns Promise resolving to permission result
|
|
47
|
+
*/
|
|
48
|
+
export async function getOrCreateRequest(
|
|
49
|
+
input: PermissionCheck,
|
|
50
|
+
checkFn: (input: PermissionCheck) => Promise<boolean>
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const key = generateDeduplicationKey(input);
|
|
53
|
+
|
|
54
|
+
// Check if request is already in-flight
|
|
55
|
+
const existingRequest = inFlightRequests.get(key);
|
|
56
|
+
if (existingRequest) {
|
|
57
|
+
return existingRequest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create new request
|
|
61
|
+
const requestPromise = checkFn(input).finally(() => {
|
|
62
|
+
// Clean up when request completes (success or failure)
|
|
63
|
+
inFlightRequests.delete(key);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Track the request
|
|
67
|
+
inFlightRequests.set(key, requestPromise);
|
|
68
|
+
|
|
69
|
+
return requestPromise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all in-flight requests (useful for testing or cleanup)
|
|
74
|
+
*/
|
|
75
|
+
export function clearInFlightRequests(): void {
|
|
76
|
+
inFlightRequests.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get count of in-flight requests (useful for monitoring)
|
|
81
|
+
*
|
|
82
|
+
* @returns Number of in-flight requests
|
|
83
|
+
*/
|
|
84
|
+
export function getInFlightRequestCount(): number {
|
|
85
|
+
return inFlightRequests.size;
|
|
86
|
+
}
|
|
87
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep equality check utility for RBAC
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/Utils/DeepEqual
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* Provides deep equality checking for scope objects and other RBAC data structures.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Scope } from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Deep equality check for two values
|
|
14
|
+
*
|
|
15
|
+
* @param a - First value
|
|
16
|
+
* @param b - Second value
|
|
17
|
+
* @returns True if values are deeply equal
|
|
18
|
+
*/
|
|
19
|
+
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
20
|
+
if (a === b) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (a == null || b == null) {
|
|
25
|
+
return a === b;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof a !== typeof b) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof a !== 'object') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
41
|
+
if (a.length !== b.length) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < a.length; i++) {
|
|
45
|
+
if (!deepEqual(a[i], b[i])) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const keysA = Object.keys(a as Record<string, unknown>);
|
|
53
|
+
const keysB = Object.keys(b as Record<string, unknown>);
|
|
54
|
+
|
|
55
|
+
if (keysA.length !== keysB.length) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const key of keysA) {
|
|
60
|
+
if (!keysB.includes(key)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deep equality check for Scope objects
|
|
73
|
+
*
|
|
74
|
+
* @param a - First scope
|
|
75
|
+
* @param b - Second scope
|
|
76
|
+
* @returns True if scopes are deeply equal
|
|
77
|
+
*/
|
|
78
|
+
export function scopeEqual(a: Scope | null | undefined, b: Scope | null | undefined): boolean {
|
|
79
|
+
if (a === b) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (a == null || b == null) {
|
|
84
|
+
return a === b;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
a.organisationId === b.organisationId &&
|
|
89
|
+
a.eventId === b.eventId &&
|
|
90
|
+
a.appId === b.appId
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
package/src/styles/core.css
CHANGED
|
@@ -240,14 +240,14 @@
|
|
|
240
240
|
/* Custom utility styles go here */
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
/* Hide spinner arrows on number inputs
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
/* Hide spinner arrows on all number inputs (modern UX convention) */
|
|
244
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
245
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
246
246
|
-webkit-appearance: none;
|
|
247
247
|
margin: 0;
|
|
248
248
|
}
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
|
|
250
|
+
input[type="number"] {
|
|
251
251
|
-moz-appearance: textfield;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
@@ -3320,13 +3320,13 @@ export type Database = {
|
|
|
3320
3320
|
},
|
|
3321
3321
|
]
|
|
3322
3322
|
}
|
|
3323
|
-
|
|
3323
|
+
pace_identification: {
|
|
3324
3324
|
Row: {
|
|
3325
3325
|
created_at: string | null
|
|
3326
3326
|
document_number: string | null
|
|
3327
|
-
document_type: string
|
|
3328
3327
|
expiry_date: string | null
|
|
3329
3328
|
id: string
|
|
3329
|
+
identification_type_id: number | null
|
|
3330
3330
|
issue_city: string | null
|
|
3331
3331
|
issue_country: string | null
|
|
3332
3332
|
issue_date: string | null
|
|
@@ -3339,9 +3339,9 @@ export type Database = {
|
|
|
3339
3339
|
Insert: {
|
|
3340
3340
|
created_at?: string | null
|
|
3341
3341
|
document_number?: string | null
|
|
3342
|
-
document_type: string
|
|
3343
3342
|
expiry_date?: string | null
|
|
3344
3343
|
id?: string
|
|
3344
|
+
identification_type_id?: number | null
|
|
3345
3345
|
issue_city?: string | null
|
|
3346
3346
|
issue_country?: string | null
|
|
3347
3347
|
issue_date?: string | null
|
|
@@ -3354,9 +3354,9 @@ export type Database = {
|
|
|
3354
3354
|
Update: {
|
|
3355
3355
|
created_at?: string | null
|
|
3356
3356
|
document_number?: string | null
|
|
3357
|
-
document_type?: string
|
|
3358
3357
|
expiry_date?: string | null
|
|
3359
3358
|
id?: string
|
|
3359
|
+
identification_type_id?: number | null
|
|
3360
3360
|
issue_city?: string | null
|
|
3361
3361
|
issue_country?: string | null
|
|
3362
3362
|
issue_date?: string | null
|
|
@@ -3368,14 +3368,21 @@ export type Database = {
|
|
|
3368
3368
|
}
|
|
3369
3369
|
Relationships: [
|
|
3370
3370
|
{
|
|
3371
|
-
foreignKeyName: "
|
|
3371
|
+
foreignKeyName: "fk_pace_identification_organisation_id"
|
|
3372
3372
|
columns: ["organisation_id"]
|
|
3373
3373
|
isOneToOne: false
|
|
3374
3374
|
referencedRelation: "organisations"
|
|
3375
3375
|
referencedColumns: ["id"]
|
|
3376
3376
|
},
|
|
3377
3377
|
{
|
|
3378
|
-
foreignKeyName: "
|
|
3378
|
+
foreignKeyName: "fk_pace_identification_type_id"
|
|
3379
|
+
columns: ["identification_type_id"]
|
|
3380
|
+
isOneToOne: false
|
|
3381
|
+
referencedRelation: "pace_identification_type"
|
|
3382
|
+
referencedColumns: ["id"]
|
|
3383
|
+
},
|
|
3384
|
+
{
|
|
3385
|
+
foreignKeyName: "pace_identification_member_id_fkey"
|
|
3379
3386
|
columns: ["member_id"]
|
|
3380
3387
|
isOneToOne: false
|
|
3381
3388
|
referencedRelation: "pace_member"
|
|
@@ -3383,6 +3390,53 @@ export type Database = {
|
|
|
3383
3390
|
},
|
|
3384
3391
|
]
|
|
3385
3392
|
}
|
|
3393
|
+
pace_identification_type: {
|
|
3394
|
+
Row: {
|
|
3395
|
+
created_at: string | null
|
|
3396
|
+
created_by: string | null
|
|
3397
|
+
description: string | null
|
|
3398
|
+
id: number
|
|
3399
|
+
is_active: boolean | null
|
|
3400
|
+
name: string
|
|
3401
|
+
organisation_id: string
|
|
3402
|
+
sort_order: number | null
|
|
3403
|
+
updated_at: string | null
|
|
3404
|
+
updated_by: string | null
|
|
3405
|
+
}
|
|
3406
|
+
Insert: {
|
|
3407
|
+
created_at?: string | null
|
|
3408
|
+
created_by?: string | null
|
|
3409
|
+
description?: string | null
|
|
3410
|
+
id?: never
|
|
3411
|
+
is_active?: boolean | null
|
|
3412
|
+
name: string
|
|
3413
|
+
organisation_id: string
|
|
3414
|
+
sort_order?: number | null
|
|
3415
|
+
updated_at?: string | null
|
|
3416
|
+
updated_by?: string | null
|
|
3417
|
+
}
|
|
3418
|
+
Update: {
|
|
3419
|
+
created_at?: string | null
|
|
3420
|
+
created_by?: string | null
|
|
3421
|
+
description?: string | null
|
|
3422
|
+
id?: never
|
|
3423
|
+
is_active?: boolean | null
|
|
3424
|
+
name?: string
|
|
3425
|
+
organisation_id?: string
|
|
3426
|
+
sort_order?: number | null
|
|
3427
|
+
updated_at?: string | null
|
|
3428
|
+
updated_by?: string | null
|
|
3429
|
+
}
|
|
3430
|
+
Relationships: [
|
|
3431
|
+
{
|
|
3432
|
+
foreignKeyName: "pace_identification_type_organisation_id_fkey"
|
|
3433
|
+
columns: ["organisation_id"]
|
|
3434
|
+
isOneToOne: false
|
|
3435
|
+
referencedRelation: "organisations"
|
|
3436
|
+
referencedColumns: ["id"]
|
|
3437
|
+
},
|
|
3438
|
+
]
|
|
3439
|
+
}
|
|
3386
3440
|
pace_member: {
|
|
3387
3441
|
Row: {
|
|
3388
3442
|
address_id: string | null
|
|
@@ -3858,7 +3912,7 @@ export type Database = {
|
|
|
3858
3912
|
},
|
|
3859
3913
|
]
|
|
3860
3914
|
}
|
|
3861
|
-
|
|
3915
|
+
pace_qualification: {
|
|
3862
3916
|
Row: {
|
|
3863
3917
|
created_at: string | null
|
|
3864
3918
|
credential_id: string | null
|
|
@@ -3900,14 +3954,14 @@ export type Database = {
|
|
|
3900
3954
|
}
|
|
3901
3955
|
Relationships: [
|
|
3902
3956
|
{
|
|
3903
|
-
foreignKeyName: "
|
|
3957
|
+
foreignKeyName: "fk_pace_qualification_organisation_id"
|
|
3904
3958
|
columns: ["organisation_id"]
|
|
3905
3959
|
isOneToOne: false
|
|
3906
3960
|
referencedRelation: "organisations"
|
|
3907
3961
|
referencedColumns: ["id"]
|
|
3908
3962
|
},
|
|
3909
3963
|
{
|
|
3910
|
-
foreignKeyName: "
|
|
3964
|
+
foreignKeyName: "pace_qualification_member_id_fkey"
|
|
3911
3965
|
columns: ["member_id"]
|
|
3912
3966
|
isOneToOne: false
|
|
3913
3967
|
referencedRelation: "pace_member"
|
|
@@ -55,6 +55,7 @@ export enum FileCategory {
|
|
|
55
55
|
* Options for uploading a file with a file reference
|
|
56
56
|
* @property pageContext - The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
|
|
57
57
|
* Used for context-aware permission checks. Required to check appropriate page-level permissions.
|
|
58
|
+
* @property event_id - Optional event ID for event-scoped permission checks. Required for event-based apps.
|
|
58
59
|
*/
|
|
59
60
|
export interface FileUploadOptions {
|
|
60
61
|
table_name: string;
|
|
@@ -62,7 +63,9 @@ export interface FileUploadOptions {
|
|
|
62
63
|
organisation_id: string;
|
|
63
64
|
app_id: AppId;
|
|
64
65
|
category: FileCategory;
|
|
66
|
+
folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
|
|
65
67
|
pageContext: string;
|
|
68
|
+
event_id?: string;
|
|
66
69
|
is_public?: boolean;
|
|
67
70
|
custom_metadata?: Record<string, unknown>;
|
|
68
71
|
}
|
|
@@ -84,7 +87,6 @@ export interface FileReferenceService {
|
|
|
84
87
|
|
|
85
88
|
export interface StorageUploadOptions {
|
|
86
89
|
orgId: string;
|
|
87
|
-
category: FileCategory;
|
|
88
90
|
isPublic?: boolean;
|
|
89
91
|
customPath?: string;
|
|
90
92
|
}
|
|
@@ -50,6 +50,7 @@ const mockFileUploadOptions = {
|
|
|
50
50
|
organisation_id: 'test-org-123',
|
|
51
51
|
app_id: 'test-app-123',
|
|
52
52
|
category: FileCategory.GENERAL_DOCUMENTS,
|
|
53
|
+
folder: 'documents',
|
|
53
54
|
pageContext: 'configuration',
|
|
54
55
|
is_public: false
|
|
55
56
|
};
|
|
@@ -105,7 +106,7 @@ describe('[service] FileReferenceServiceImpl', () => {
|
|
|
105
106
|
testFile,
|
|
106
107
|
expect.objectContaining({
|
|
107
108
|
orgId: mockFileUploadOptions.organisation_id,
|
|
108
|
-
customPath: mockFileUploadOptions.
|
|
109
|
+
customPath: mockFileUploadOptions.folder,
|
|
109
110
|
isPublic: mockFileUploadOptions.is_public
|
|
110
111
|
})
|
|
111
112
|
);
|
|
@@ -448,9 +449,22 @@ describe('[service] FileReferenceServiceImpl', () => {
|
|
|
448
449
|
});
|
|
449
450
|
|
|
450
451
|
it('lists all file references for record', async () => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
// Mock RPC to return full data structure (as per new implementation)
|
|
453
|
+
// RPC returns: id, file_path, file_metadata, is_public, created_at
|
|
454
|
+
// The code constructs FileReference objects from this RPC response
|
|
455
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
456
|
+
data: [{
|
|
457
|
+
id: 'file-ref-123',
|
|
458
|
+
file_path: mockFileReference.file_path,
|
|
459
|
+
file_metadata: {
|
|
460
|
+
...mockFileReference.file_metadata,
|
|
461
|
+
app_id: mockFileReference.app_id // Include app_id in metadata for proper construction
|
|
462
|
+
},
|
|
463
|
+
is_public: mockFileReference.is_public,
|
|
464
|
+
created_at: mockFileReference.created_at
|
|
465
|
+
}],
|
|
466
|
+
error: null
|
|
467
|
+
});
|
|
454
468
|
|
|
455
469
|
const result = await service.listFileReferences(
|
|
456
470
|
'test_table',
|
|
@@ -458,7 +472,21 @@ describe('[service] FileReferenceServiceImpl', () => {
|
|
|
458
472
|
'test-org-123'
|
|
459
473
|
);
|
|
460
474
|
|
|
461
|
-
|
|
475
|
+
// Verify result has correct structure (constructed from RPC response)
|
|
476
|
+
expect(result).toHaveLength(1);
|
|
477
|
+
expect(result[0].id).toBe('file-ref-123');
|
|
478
|
+
expect(result[0].table_name).toBe('test_table');
|
|
479
|
+
expect(result[0].record_id).toBe('test-record-123');
|
|
480
|
+
expect(result[0].organisation_id).toBe('test-org-123');
|
|
481
|
+
expect(result[0].file_path).toBe(mockFileReference.file_path);
|
|
482
|
+
// file_metadata: code extracts fileName and fileType from file_path, then spreads item.file_metadata
|
|
483
|
+
// Since item.file_metadata has fileName: 'test-document.pdf' and fileType: 'application/pdf',
|
|
484
|
+
// the spread overwrites the extracted values
|
|
485
|
+
// So we expect the metadata's values, not the extracted ones
|
|
486
|
+
expect(result[0].file_metadata.fileName).toBe('test-document.pdf');
|
|
487
|
+
expect(result[0].file_metadata.fileType).toBe('application/pdf');
|
|
488
|
+
expect(result[0].is_public).toBe(mockFileReference.is_public);
|
|
489
|
+
expect(result[0].app_id).toBe(mockFileReference.app_id);
|
|
462
490
|
});
|
|
463
491
|
});
|
|
464
492
|
|
|
@@ -757,22 +785,75 @@ describe('[utility] createFileReferenceService', () => {
|
|
|
757
785
|
describe('[utility] uploadFileWithReference', () => {
|
|
758
786
|
it('uploads file and creates reference successfully', async () => {
|
|
759
787
|
const testFile = createTestFile();
|
|
788
|
+
|
|
789
|
+
// Mock successful RPC call that returns file reference ID
|
|
790
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
791
|
+
data: 'file-ref-123',
|
|
792
|
+
error: null
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Mock successful file reference fetch - using the same pattern as other tests
|
|
796
|
+
(mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
|
|
797
|
+
data: mockFileReference,
|
|
798
|
+
error: null
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Mock storage signed URL generation
|
|
802
|
+
mockSupabase.storage = {
|
|
803
|
+
from: vi.fn().mockReturnValue({
|
|
804
|
+
createSignedUrl: vi.fn().mockResolvedValue({
|
|
805
|
+
data: { signedUrl: 'https://example.com/signed-url' },
|
|
806
|
+
error: null
|
|
807
|
+
})
|
|
808
|
+
})
|
|
809
|
+
} as any;
|
|
810
|
+
|
|
760
811
|
const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
|
|
761
812
|
expect(result).toHaveProperty('file_reference');
|
|
762
813
|
expect('file_url' in result).toBe(true);
|
|
763
814
|
});
|
|
764
815
|
|
|
765
816
|
it('handles upload failures gracefully', async () => {
|
|
766
|
-
//
|
|
817
|
+
// Mock upload failure
|
|
818
|
+
mockUploadFile.mockResolvedValue({
|
|
819
|
+
success: false,
|
|
820
|
+
error: 'Upload failed'
|
|
821
|
+
});
|
|
822
|
+
|
|
767
823
|
const testFile = createTestFile();
|
|
768
|
-
|
|
769
|
-
|
|
824
|
+
await expect(uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile))
|
|
825
|
+
.rejects.toThrow('Upload failed');
|
|
770
826
|
});
|
|
771
827
|
|
|
772
828
|
it('handles URL generation failures gracefully', async () => {
|
|
773
829
|
const testFile = createTestFile();
|
|
830
|
+
|
|
831
|
+
// Mock successful RPC call
|
|
832
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
833
|
+
data: 'file-ref-123',
|
|
834
|
+
error: null
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Mock successful file reference fetch
|
|
838
|
+
(mockSupabase.from() as any).select().eq().eq().eq().single.mockResolvedValue({
|
|
839
|
+
data: mockFileReference,
|
|
840
|
+
error: null
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
// Mock storage to fail on signed URL generation
|
|
844
|
+
mockSupabase.storage = {
|
|
845
|
+
from: vi.fn().mockReturnValue({
|
|
846
|
+
createSignedUrl: vi.fn().mockResolvedValue({
|
|
847
|
+
data: null,
|
|
848
|
+
error: { message: 'URL generation failed' }
|
|
849
|
+
})
|
|
850
|
+
})
|
|
851
|
+
} as any;
|
|
852
|
+
|
|
774
853
|
const result = await uploadFileWithReference(mockSupabase, mockFileUploadOptions, testFile);
|
|
775
854
|
expect(result).toHaveProperty('file_reference');
|
|
855
|
+
// URL should be empty string when generation fails
|
|
856
|
+
expect(result.file_url).toBe('');
|
|
776
857
|
});
|
|
777
858
|
|
|
778
859
|
it('validates required parameters', async () => {
|
|
@@ -26,7 +26,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
26
26
|
*
|
|
27
27
|
* Storage Flow:
|
|
28
28
|
* 1. Upload file to storage bucket first (files or public-files based on is_public flag)
|
|
29
|
-
* - Path format: {orgId}/{
|
|
29
|
+
* - Path format: {orgId}/{folder}/{timestamp-uuid-filename}
|
|
30
30
|
* - Bucket selection: 'files' (private) or 'public-files' (public)
|
|
31
31
|
* 2. Extract file metadata (dimensions, hash, etc.)
|
|
32
32
|
* 3. Set organisation context for RLS policies
|
|
@@ -48,15 +48,18 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
48
48
|
if (!options.record_id) {
|
|
49
49
|
throw new Error('record_id is required for file upload');
|
|
50
50
|
}
|
|
51
|
+
if (!options.folder) {
|
|
52
|
+
throw new Error('folder is required for file upload. The folder prop determines the storage path.');
|
|
53
|
+
}
|
|
51
54
|
|
|
52
55
|
// Step 1: Upload file to storage bucket first
|
|
53
|
-
// This generates a unique path: {orgId}/{
|
|
56
|
+
// This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename}
|
|
54
57
|
// Bucket is automatically selected based on is_public flag
|
|
55
58
|
const uploadResult = await uploadFile(this.supabase, file, {
|
|
56
59
|
appName: 'file-reference',
|
|
57
60
|
orgId: options.organisation_id,
|
|
58
61
|
isPublic: options.is_public || false,
|
|
59
|
-
customPath: options.
|
|
62
|
+
customPath: options.folder // Use folder prop as the custom path segment
|
|
60
63
|
});
|
|
61
64
|
if (!uploadResult.success) {
|
|
62
65
|
throw new Error(`Failed to upload file: ${uploadResult.error}`);
|
|
@@ -89,6 +92,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
89
92
|
p_organisation_id: options.organisation_id,
|
|
90
93
|
p_app_id: options.app_id,
|
|
91
94
|
p_page_context: options.pageContext,
|
|
95
|
+
p_event_id: options.event_id || null, // Pass event_id for event-based apps
|
|
92
96
|
p_file_metadata: {
|
|
93
97
|
fileName: file.name,
|
|
94
98
|
fileType: file.type,
|
|
@@ -106,14 +110,23 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
106
110
|
throw new Error(`Failed to create file reference: ${error.message}`);
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
// Check if RPC returned null (permission denied or other failure)
|
|
114
|
+
if (!data || data === null) {
|
|
115
|
+
// Clean up the uploaded file since DB insert failed
|
|
116
|
+
await deleteFile(this.supabase, filePath, options.is_public || false);
|
|
117
|
+
throw new Error(`File upload denied: insufficient permissions. You need 'create:page.${options.pageContext}' or 'update:page.${options.pageContext}' permission for the '${options.pageContext}' page. Make sure the page exists in rbac_app_pages table.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
// Get the created file reference
|
|
110
121
|
const { data: fileRef, error: fetchError } = await this.supabase
|
|
111
122
|
.from('file_references')
|
|
112
|
-
.select('
|
|
123
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
113
124
|
.eq('id', data)
|
|
114
125
|
.single();
|
|
115
126
|
|
|
116
127
|
if (fetchError || !fileRef) {
|
|
128
|
+
// Clean up uploaded file if we can't fetch the reference
|
|
129
|
+
await deleteFile(this.supabase, filePath, options.is_public || false);
|
|
117
130
|
throw new Error(`Failed to fetch created file reference: ${fetchError?.message}`);
|
|
118
131
|
}
|
|
119
132
|
|
|
@@ -136,7 +149,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
136
149
|
try {
|
|
137
150
|
const { data, error } = await this.supabase
|
|
138
151
|
.from('file_references')
|
|
139
|
-
.select('
|
|
152
|
+
.select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
|
|
140
153
|
.eq('table_name', table_name)
|
|
141
154
|
.eq('record_id', record_id)
|
|
142
155
|
.eq('organisation_id', organisation_id)
|
|
@@ -284,24 +297,50 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
284
297
|
throw new Error(`Failed to list file references: ${error.message}`);
|
|
285
298
|
}
|
|
286
299
|
|
|
287
|
-
// RPC returns
|
|
300
|
+
// RPC returns: id, file_path, file_metadata, is_public, created_at
|
|
301
|
+
// We can construct FileReference objects directly from RPC response + function parameters
|
|
302
|
+
// This avoids a second query and reduces network requests
|
|
288
303
|
if (!data || data.length === 0) {
|
|
289
304
|
return [];
|
|
290
305
|
}
|
|
291
306
|
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (fetchError) {
|
|
301
|
-
throw new Error(`Failed to fetch file references: ${fetchError.message}`);
|
|
307
|
+
// Construct FileReference objects from RPC response
|
|
308
|
+
// This avoids RLS issues with direct queries - the RPC already validated permissions
|
|
309
|
+
interface RpcFileItem {
|
|
310
|
+
id: string;
|
|
311
|
+
file_path: string;
|
|
312
|
+
file_metadata: { app_id?: string; [key: string]: unknown };
|
|
313
|
+
is_public?: boolean;
|
|
314
|
+
created_at?: string;
|
|
302
315
|
}
|
|
316
|
+
const fileReferences: FileReference[] = data
|
|
317
|
+
.filter((item: RpcFileItem) => item.id && item.file_path && item.file_metadata)
|
|
318
|
+
.map((item: RpcFileItem) => {
|
|
319
|
+
// Extract file name and type from file_path
|
|
320
|
+
const fileName = item.file_path.split('/').pop() || 'unknown';
|
|
321
|
+
const fileType = fileName.split('.').pop() || 'unknown';
|
|
322
|
+
|
|
323
|
+
// Construct complete FileReference from RPC response + function parameters
|
|
324
|
+
const fileRef: FileReference = {
|
|
325
|
+
id: item.id,
|
|
326
|
+
table_name: table_name,
|
|
327
|
+
record_id: record_id,
|
|
328
|
+
file_path: item.file_path,
|
|
329
|
+
file_metadata: {
|
|
330
|
+
fileName,
|
|
331
|
+
fileType,
|
|
332
|
+
...(item.file_metadata || {}),
|
|
333
|
+
} as FileMetadata,
|
|
334
|
+
organisation_id: organisation_id,
|
|
335
|
+
app_id: item.file_metadata?.app_id ? assertAppId(item.file_metadata.app_id) : assertAppId(''), // May not be in metadata, use empty string
|
|
336
|
+
is_public: item.is_public ?? false,
|
|
337
|
+
created_at: item.created_at || new Date().toISOString(),
|
|
338
|
+
updated_at: item.created_at || new Date().toISOString() // RPC doesn't return updated_at, use created_at
|
|
339
|
+
};
|
|
340
|
+
return fileRef;
|
|
341
|
+
});
|
|
303
342
|
|
|
304
|
-
return
|
|
343
|
+
return fileReferences;
|
|
305
344
|
} catch (error) {
|
|
306
345
|
log.error('Error listing file references:', error);
|
|
307
346
|
throw error;
|