@jmruthers/pace-core 0.5.136 → 0.5.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
  8. package/dist/chunk-5JMOHWDI.js.map +1 -0
  9. package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
  10. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  11. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  12. package/dist/chunk-7QCC6MCP.js.map +1 -0
  13. package/dist/chunk-BJPBT3CU.js +21 -0
  14. package/dist/chunk-BJPBT3CU.js.map +1 -0
  15. package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
  16. package/dist/chunk-BOOI7GK2.js.map +1 -0
  17. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  18. package/dist/chunk-JISYG63F.js +70 -0
  19. package/dist/chunk-JISYG63F.js.map +1 -0
  20. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  21. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  22. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  23. package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
  24. package/dist/chunk-TLT2ZR3L.js.map +1 -0
  25. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  26. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  27. package/dist/components.d.ts +1 -1
  28. package/dist/components.js +12 -11
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.js +8 -9
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.js +3 -4
  36. package/dist/rbac/index.js +8 -9
  37. package/dist/schema-DTDZQe2u.d.ts +28 -0
  38. package/dist/types.d.ts +152 -3
  39. package/dist/types.js +51 -16
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils.d.ts +89 -4
  42. package/dist/utils.js +214 -96
  43. package/dist/utils.js.map +1 -1
  44. package/dist/validation.d.ts +1 -343
  45. package/dist/validation.js +3 -100
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +1 -1
  60. package/docs/api/enums/FileCategory.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/BadgeProps.md +27 -0
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/EventLogoProps.md +1 -1
  77. package/docs/api/interfaces/ExportColumn.md +1 -1
  78. package/docs/api/interfaces/ExportOptions.md +1 -1
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  128. package/docs/api/interfaces/StorageConfig.md +1 -1
  129. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  130. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  131. package/docs/api/interfaces/StorageListOptions.md +1 -1
  132. package/docs/api/interfaces/StorageListResult.md +1 -1
  133. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  134. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  135. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  136. package/docs/api/interfaces/StyleImport.md +1 -1
  137. package/docs/api/interfaces/SwitchProps.md +1 -1
  138. package/docs/api/interfaces/ToastActionElement.md +1 -1
  139. package/docs/api/interfaces/ToastProps.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  141. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  143. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  145. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  147. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  148. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  150. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  151. package/docs/api/interfaces/UserEventAccess.md +1 -1
  152. package/docs/api/interfaces/UserMenuProps.md +1 -1
  153. package/docs/api/interfaces/UserProfile.md +1 -1
  154. package/docs/api/modules.md +84 -15
  155. package/docs/architecture/README.md +0 -1
  156. package/docs/styles/README.md +0 -2
  157. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  158. package/examples/RBAC/EventBasedApp.tsx +239 -0
  159. package/examples/RBAC/PermissionExample.tsx +151 -0
  160. package/examples/RBAC/index.ts +13 -0
  161. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  162. package/examples/public-pages/PublicEventPage.tsx +274 -0
  163. package/examples/public-pages/PublicPageApp.tsx +308 -0
  164. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  165. package/examples/public-pages/index.ts +14 -0
  166. package/package.json +1 -10
  167. package/src/__tests__/TEST_STANDARD.md +92 -0
  168. package/src/components/Badge/Badge.test.tsx +314 -0
  169. package/src/components/Badge/Badge.tsx +304 -0
  170. package/src/components/Badge/index.ts +3 -0
  171. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  172. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  173. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  174. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  175. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  176. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  177. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  178. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  179. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  180. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  181. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  182. package/src/components/DataTable/core/ActionManager.ts +235 -0
  183. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  184. package/src/components/DataTable/core/DataManager.ts +188 -0
  185. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  186. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  187. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  188. package/src/components/DataTable/core/StateManager.ts +311 -0
  189. package/src/components/DataTable/core/interfaces.ts +338 -0
  190. package/src/components/DataTable/styles.ts +27 -6
  191. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  192. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  193. package/src/components/DataTable/utils/debugTools.ts +609 -0
  194. package/src/components/DataTable/utils/index.ts +1 -0
  195. package/src/components/Dialog/README.md +804 -0
  196. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  197. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  198. package/src/components/Footer/Footer.test.tsx +1 -1
  199. package/src/components/Form/Form.test.tsx +1 -1
  200. package/src/components/Form/FormErrorSummary.tsx +113 -0
  201. package/src/components/Form/FormFieldset.tsx +127 -0
  202. package/src/components/Form/FormLiveRegion.tsx +198 -0
  203. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  204. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  205. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  206. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  207. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  208. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  209. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  210. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  211. package/src/components/Select/Select.test.tsx +1 -1
  212. package/src/components/Select/Select.tsx +20 -8
  213. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  214. package/src/components/index.ts +3 -0
  215. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  216. package/src/index.ts +4 -0
  217. package/src/rbac/hooks/useCan.test.ts +24 -0
  218. package/src/rbac/hooks/usePermissions.ts +49 -12
  219. package/src/styles/core.css +3 -0
  220. package/src/utils/appConfig.ts +47 -0
  221. package/src/utils/appIdResolver.test.ts +499 -0
  222. package/src/utils/appIdResolver.ts +130 -0
  223. package/src/utils/appNameResolver.simple.test.ts +212 -0
  224. package/src/utils/appNameResolver.test.ts +121 -0
  225. package/src/utils/appNameResolver.ts +191 -0
  226. package/src/utils/audit.ts +127 -0
  227. package/src/utils/auth-utils.ts +96 -0
  228. package/src/utils/bundleAnalysis.ts +129 -0
  229. package/src/utils/cn.ts +7 -0
  230. package/src/utils/debugLogger.ts +67 -0
  231. package/src/utils/deviceFingerprint.ts +215 -0
  232. package/src/utils/dynamicUtils.ts +105 -0
  233. package/src/utils/file-reference.test.ts +788 -0
  234. package/src/utils/file-reference.ts +519 -0
  235. package/src/utils/formatDate.test.ts +237 -0
  236. package/src/utils/formatting.ts +133 -0
  237. package/src/utils/index.ts +7 -0
  238. package/src/utils/lazyLoad.tsx +44 -0
  239. package/src/utils/logger.ts +179 -0
  240. package/src/utils/organisationContext.test.ts +322 -0
  241. package/src/utils/organisationContext.ts +153 -0
  242. package/src/utils/performanceBenchmark.ts +64 -0
  243. package/src/utils/performanceBudgets.ts +110 -0
  244. package/src/utils/permissionTypes.ts +37 -0
  245. package/src/utils/permissionUtils.test.ts +393 -0
  246. package/src/utils/permissionUtils.ts +34 -0
  247. package/src/utils/sanitization.ts +264 -0
  248. package/src/utils/schemaUtils.ts +37 -0
  249. package/src/utils/secureDataAccess.test.ts +711 -0
  250. package/src/utils/secureDataAccess.ts +377 -0
  251. package/src/utils/secureErrors.ts +79 -0
  252. package/src/utils/secureStorage.ts +244 -0
  253. package/src/utils/security.ts +156 -0
  254. package/src/utils/securityMonitor.ts +45 -0
  255. package/src/utils/sessionTracking.ts +126 -0
  256. package/src/utils/validation.ts +111 -0
  257. package/src/utils/validationUtils.ts +120 -0
  258. package/src/validation/index.ts +2 -2
  259. package/dist/chunk-444EZN6N.js.map +0 -1
  260. package/dist/chunk-APIBCTL2.js +0 -670
  261. package/dist/chunk-APIBCTL2.js.map +0 -1
  262. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  263. package/dist/chunk-K2WWTH7O.js +0 -94
  264. package/dist/chunk-K2WWTH7O.js.map +0 -1
  265. package/dist/chunk-L6PGMCMD.js.map +0 -1
  266. package/dist/chunk-LMC26NLJ.js +0 -84
  267. package/dist/chunk-LMC26NLJ.js.map +0 -1
  268. package/dist/chunk-NOHEVYVX.js.map +0 -1
  269. package/dist/chunk-TVYPTYOY.js.map +0 -1
  270. package/dist/validation-8npbysjg.d.ts +0 -177
  271. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
  272. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  273. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  274. /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
  275. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  276. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  277. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  278. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  279. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  280. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  282. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  283. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  284. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  285. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  289. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  290. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  291. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  292. /package/examples/{components → components 2}/index.ts +0 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * @file App ID Resolver Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/AppIdResolver
5
+ * @since 1.0.0
6
+ *
7
+ * Comprehensive tests for app ID resolution utility functions covering all critical functionality.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import { getAppId, getAppIds, CachedAppIdResolver } from './appIdResolver';
12
+ import type { SupabaseClient } from '@supabase/supabase-js';
13
+ import type { Database } from '../types/database';
14
+
15
+ // Mock Supabase client
16
+ const createMockSupabaseClient = () => {
17
+ const queryBuilder = {
18
+ select: vi.fn().mockReturnThis(),
19
+ ilike: vi.fn().mockReturnThis(),
20
+ eq: vi.fn().mockReturnThis(),
21
+ or: vi.fn().mockReturnThis(),
22
+ single: vi.fn().mockResolvedValue({ data: null, error: null }),
23
+ mockResolvedValue: vi.fn(),
24
+ mockRejectedValue: vi.fn()
25
+ };
26
+
27
+ // Make the chained methods return the query builder with mock methods
28
+ queryBuilder.select.mockReturnValue(queryBuilder);
29
+ queryBuilder.ilike.mockReturnValue(queryBuilder);
30
+ queryBuilder.eq.mockReturnValue(queryBuilder);
31
+ queryBuilder.or.mockReturnValue(queryBuilder);
32
+
33
+ return {
34
+ from: vi.fn(() => queryBuilder),
35
+ queryBuilder // Export the query builder for direct access
36
+ };
37
+ };
38
+
39
+ describe('App ID Resolver', () => {
40
+ let mockSupabase: any;
41
+ let mockQueryBuilder: any;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ mockSupabase = createMockSupabaseClient();
46
+ mockQueryBuilder = mockSupabase.queryBuilder;
47
+ });
48
+
49
+ describe('getAppId', () => {
50
+ it('resolves app ID for valid app name', async () => {
51
+ mockQueryBuilder.single.mockResolvedValue({
52
+ data: { id: 'app-123' },
53
+ error: null
54
+ });
55
+
56
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
57
+
58
+ expect(result).toBe('app-123');
59
+ expect(mockSupabase.from).toHaveBeenCalledWith('rbac_apps');
60
+ expect(mockQueryBuilder.select).toHaveBeenCalledWith('id');
61
+ expect(mockQueryBuilder.ilike).toHaveBeenCalledWith('name', 'MY_APP');
62
+ expect(mockQueryBuilder.eq).toHaveBeenCalledWith('is_active', true);
63
+ });
64
+
65
+ it('returns null when app not found', async () => {
66
+ mockQueryBuilder.single.mockResolvedValue({
67
+ data: null,
68
+ error: { message: 'No rows found' }
69
+ });
70
+
71
+ const result = await getAppId(mockSupabase as any, 'NONEXISTENT_APP');
72
+
73
+ expect(result).toBeNull();
74
+ });
75
+
76
+ it('handles database errors gracefully', async () => {
77
+ mockQueryBuilder.single.mockResolvedValue({
78
+ data: null,
79
+ error: { message: 'Database error' }
80
+ });
81
+
82
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
83
+
84
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
85
+
86
+ expect(result).toBeNull();
87
+ expect(consoleSpy).toHaveBeenCalledWith(
88
+ 'Failed to resolve app ID for app name:',
89
+ 'MY_APP',
90
+ { message: 'Database error' }
91
+ );
92
+
93
+ consoleSpy.mockRestore();
94
+ });
95
+
96
+ it('handles exceptions gracefully', async () => {
97
+ mockQueryBuilder.single.mockRejectedValue(new Error('Network error'));
98
+
99
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
100
+
101
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
102
+
103
+ expect(result).toBeNull();
104
+ expect(consoleSpy).toHaveBeenCalledWith(
105
+ 'Error resolving app ID for app name:',
106
+ 'MY_APP',
107
+ expect.any(Error)
108
+ );
109
+
110
+ consoleSpy.mockRestore();
111
+ });
112
+
113
+ it('handles null/undefined app name', async () => {
114
+ const result1 = await getAppId(mockSupabase as any, null as any);
115
+ const result2 = await getAppId(mockSupabase as any, undefined as any);
116
+
117
+ expect(result1).toBeNull();
118
+ expect(result2).toBeNull();
119
+ });
120
+
121
+ it('handles empty app name', async () => {
122
+ const result = await getAppId(mockSupabase as any, '');
123
+
124
+ expect(result).toBeNull();
125
+ });
126
+ });
127
+
128
+ describe('getAppIds', () => {
129
+ it('resolves multiple app IDs', async () => {
130
+ // Mock the chained call sequence for getAppIds
131
+ const mockChain = {
132
+ select: vi.fn().mockReturnThis(),
133
+ or: vi.fn().mockReturnThis(),
134
+ eq: vi.fn().mockResolvedValue({
135
+ data: [
136
+ { id: 'app-123', name: 'MY_APP' },
137
+ { id: 'app-456', name: 'ANOTHER_APP' }
138
+ ],
139
+ error: null
140
+ })
141
+ };
142
+
143
+ mockQueryBuilder.select.mockReturnValue(mockChain);
144
+ mockQueryBuilder.or.mockReturnValue(mockChain);
145
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
146
+
147
+ const result = await getAppIds(mockSupabase as any, ['MY_APP', 'ANOTHER_APP', 'MISSING_APP']);
148
+
149
+ expect(result).toEqual({
150
+ 'MY_APP': 'app-123',
151
+ 'ANOTHER_APP': 'app-456',
152
+ 'MISSING_APP': null
153
+ });
154
+ });
155
+
156
+ it('handles case-insensitive matching', async () => {
157
+ // Mock the chained call sequence for getAppIds
158
+ const mockChain = {
159
+ select: vi.fn().mockReturnThis(),
160
+ or: vi.fn().mockReturnThis(),
161
+ eq: vi.fn().mockResolvedValue({
162
+ data: [
163
+ { id: 'app-123', name: 'my_app' },
164
+ { id: 'app-456', name: 'another_app' }
165
+ ],
166
+ error: null
167
+ })
168
+ };
169
+
170
+ mockQueryBuilder.select.mockReturnValue(mockChain);
171
+ mockQueryBuilder.or.mockReturnValue(mockChain);
172
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
173
+
174
+ const result = await getAppIds(mockSupabase as any, ['MY_APP', 'ANOTHER_APP']);
175
+
176
+ expect(result).toEqual({
177
+ 'MY_APP': 'app-123',
178
+ 'ANOTHER_APP': 'app-456'
179
+ });
180
+ });
181
+
182
+ it('returns empty object for empty app names array', async () => {
183
+ const result = await getAppIds(mockSupabase as any, []);
184
+
185
+ expect(result).toEqual({});
186
+ });
187
+
188
+ it('handles database errors gracefully', async () => {
189
+ const mockChain = {
190
+ select: vi.fn().mockReturnThis(),
191
+ or: vi.fn().mockReturnThis(),
192
+ eq: vi.fn().mockResolvedValue({
193
+ data: null,
194
+ error: { message: 'Database error' }
195
+ })
196
+ };
197
+
198
+ mockQueryBuilder.select.mockReturnValue(mockChain);
199
+ mockQueryBuilder.or.mockReturnValue(mockChain);
200
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
201
+
202
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
203
+
204
+ const result = await getAppIds(mockSupabase as any, ['MY_APP', 'ANOTHER_APP']);
205
+
206
+ expect(result).toEqual({});
207
+ expect(consoleSpy).toHaveBeenCalledWith(
208
+ 'Failed to resolve app IDs for app names:',
209
+ ['MY_APP', 'ANOTHER_APP'],
210
+ { message: 'Database error' }
211
+ );
212
+
213
+ consoleSpy.mockRestore();
214
+ });
215
+
216
+ it('handles exceptions gracefully', async () => {
217
+ const mockChain = {
218
+ select: vi.fn().mockReturnThis(),
219
+ or: vi.fn().mockReturnThis(),
220
+ eq: vi.fn().mockRejectedValue(new Error('Network error'))
221
+ };
222
+
223
+ mockQueryBuilder.select.mockReturnValue(mockChain);
224
+ mockQueryBuilder.or.mockReturnValue(mockChain);
225
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
226
+
227
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
228
+
229
+ const result = await getAppIds(mockSupabase as any, ['MY_APP', 'ANOTHER_APP']);
230
+
231
+ expect(result).toEqual({});
232
+ expect(consoleSpy).toHaveBeenCalledWith(
233
+ 'Error resolving app IDs for app names:',
234
+ ['MY_APP', 'ANOTHER_APP'],
235
+ expect.any(Error)
236
+ );
237
+
238
+ consoleSpy.mockRestore();
239
+ });
240
+
241
+ it('handles null/undefined app names array', async () => {
242
+ const result1 = await getAppIds(mockSupabase as any, null as any);
243
+ const result2 = await getAppIds(mockSupabase as any, undefined as any);
244
+
245
+ expect(result1).toEqual({});
246
+ expect(result2).toEqual({});
247
+ });
248
+ });
249
+
250
+ describe('CachedAppIdResolver', () => {
251
+ let resolver: CachedAppIdResolver;
252
+
253
+ beforeEach(() => {
254
+ resolver = new CachedAppIdResolver();
255
+ });
256
+
257
+ it('resolves app ID and caches result', async () => {
258
+ mockQueryBuilder.single.mockResolvedValue({
259
+ data: { id: 'app-123' },
260
+ error: null
261
+ });
262
+
263
+ const result1 = await resolver.getAppId(mockSupabase as any, 'MY_APP');
264
+ const result2 = await resolver.getAppId(mockSupabase as any, 'MY_APP');
265
+
266
+ expect(result1).toBe('app-123');
267
+ expect(result2).toBe('app-123');
268
+ expect(mockQueryBuilder.single).toHaveBeenCalledTimes(1); // Should only call once due to caching
269
+ });
270
+
271
+ it('expires cached results after TTL', async () => {
272
+ resolver = new CachedAppIdResolver();
273
+
274
+ mockQueryBuilder.single.mockResolvedValue({
275
+ data: { id: 'app-123' },
276
+ error: null
277
+ });
278
+
279
+ const result1 = await resolver.getAppId(mockSupabase as any, 'MY_APP');
280
+ expect(result1).toBe('app-123');
281
+
282
+ // Clear cache to simulate TTL expiration
283
+ resolver.clearCacheForApp('MY_APP');
284
+
285
+ const result2 = await resolver.getAppId(mockSupabase as any, 'MY_APP');
286
+ expect(result2).toBe('app-123');
287
+ expect(mockQueryBuilder.single).toHaveBeenCalledTimes(2); // Should call again after cache clear
288
+ });
289
+
290
+ it('handles cache misses gracefully', async () => {
291
+ mockQueryBuilder.single.mockResolvedValue({
292
+ data: null,
293
+ error: { message: 'No rows found' }
294
+ });
295
+
296
+ const result = await resolver.getAppId(mockSupabase as any, 'NONEXISTENT_APP');
297
+
298
+ expect(result).toBeNull();
299
+ });
300
+
301
+ it('handles database errors gracefully', async () => {
302
+ mockQueryBuilder.single.mockResolvedValue({
303
+ data: null,
304
+ error: { message: 'Database error' }
305
+ });
306
+
307
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
308
+
309
+ const result = await resolver.getAppId(mockSupabase as any, 'MY_APP');
310
+
311
+ expect(result).toBeNull();
312
+ expect(consoleSpy).toHaveBeenCalledWith(
313
+ 'Failed to resolve app ID for app name:',
314
+ 'MY_APP',
315
+ { message: 'Database error' }
316
+ );
317
+
318
+ consoleSpy.mockRestore();
319
+ });
320
+
321
+ it('clears cache correctly', async () => {
322
+ mockQueryBuilder.single.mockResolvedValue({
323
+ data: { id: 'app-123' },
324
+ error: null
325
+ });
326
+
327
+ await resolver.getAppId(mockSupabase as any, 'MY_APP');
328
+ resolver.clearCache();
329
+
330
+ const result = await resolver.getAppId(mockSupabase as any, 'MY_APP');
331
+
332
+ expect(result).toBe('app-123');
333
+ expect(mockQueryBuilder.single).toHaveBeenCalledTimes(2); // Should call again after cache clear
334
+ });
335
+
336
+ it('respects max cache size', async () => {
337
+ resolver = new CachedAppIdResolver(60000, 2); // 2 max cache entries
338
+
339
+ mockQueryBuilder.single.mockResolvedValue({
340
+ data: { id: 'app-123' },
341
+ error: null
342
+ });
343
+
344
+ // Fill cache beyond max size
345
+ await resolver.getAppId(mockSupabase as any, 'APP1');
346
+ await resolver.getAppId(mockSupabase as any, 'APP2');
347
+ await resolver.getAppId(mockSupabase as any, 'APP3');
348
+
349
+ // Should have called database for all three apps
350
+ expect(mockQueryBuilder.single).toHaveBeenCalledTimes(3);
351
+ });
352
+ });
353
+
354
+ describe('Integration Tests', () => {
355
+ it('integrates with real Supabase client structure', async () => {
356
+ const mockClient = {
357
+ from: vi.fn(() => ({
358
+ select: vi.fn().mockReturnThis(),
359
+ ilike: vi.fn().mockReturnThis(),
360
+ eq: vi.fn().mockReturnThis(),
361
+ single: vi.fn().mockResolvedValue({
362
+ data: { id: 'app-123' },
363
+ error: null
364
+ })
365
+ }))
366
+ };
367
+
368
+ const result = await getAppId(mockClient as any, 'MY_APP');
369
+
370
+ expect(result).toBe('app-123');
371
+ expect(mockClient.from).toHaveBeenCalledWith('rbac_apps');
372
+ });
373
+
374
+ it('handles complex app name patterns', async () => {
375
+ const appNames = [
376
+ 'MY_APP',
377
+ 'my-app',
378
+ 'my_app',
379
+ 'MyApp',
380
+ 'myapp'
381
+ ];
382
+
383
+ const mockChain = {
384
+ select: vi.fn().mockReturnThis(),
385
+ or: vi.fn().mockReturnThis(),
386
+ eq: vi.fn().mockResolvedValue({
387
+ data: [
388
+ { id: 'app-123', name: 'my_app' },
389
+ { id: 'app-456', name: 'my-app' }
390
+ ],
391
+ error: null
392
+ })
393
+ };
394
+
395
+ mockQueryBuilder.select.mockReturnValue(mockChain);
396
+ mockQueryBuilder.or.mockReturnValue(mockChain);
397
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
398
+
399
+ const result = await getAppIds(mockSupabase as any, appNames);
400
+
401
+ expect(result).toEqual({
402
+ 'MY_APP': 'app-123',
403
+ 'my-app': 'app-456',
404
+ 'my_app': null,
405
+ 'MyApp': null,
406
+ 'myapp': null
407
+ });
408
+ });
409
+ });
410
+
411
+ describe('Error Handling', () => {
412
+ it('handles network timeouts', async () => {
413
+ mockQueryBuilder.single.mockRejectedValue(new Error('Request timeout'));
414
+
415
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
416
+
417
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
418
+
419
+ expect(result).toBeNull();
420
+ expect(consoleSpy).toHaveBeenCalledWith(
421
+ 'Error resolving app ID for app name:',
422
+ 'MY_APP',
423
+ expect.any(Error)
424
+ );
425
+
426
+ consoleSpy.mockRestore();
427
+ });
428
+
429
+ it('handles malformed responses', async () => {
430
+ mockQueryBuilder.single.mockResolvedValue({
431
+ data: { invalid: 'structure' },
432
+ error: null
433
+ });
434
+
435
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
436
+
437
+ expect(result).toBeNull();
438
+ });
439
+
440
+ it('handles null data responses', async () => {
441
+ mockQueryBuilder.single.mockResolvedValue({
442
+ data: null,
443
+ error: null
444
+ });
445
+
446
+ const result = await getAppId(mockSupabase as any, 'MY_APP');
447
+
448
+ expect(result).toBeNull();
449
+ });
450
+ });
451
+
452
+ describe('Performance', () => {
453
+ it('handles large numbers of app names efficiently', async () => {
454
+ const appNames = Array.from({ length: 100 }, (_, i) => `APP_${i}`);
455
+
456
+ const mockChain = {
457
+ select: vi.fn().mockReturnThis(),
458
+ or: vi.fn().mockReturnThis(),
459
+ eq: vi.fn().mockResolvedValue({
460
+ data: appNames.map((name, i) => ({ id: `app-${i}`, name: name.toLowerCase() })),
461
+ error: null
462
+ })
463
+ };
464
+
465
+ mockQueryBuilder.select.mockReturnValue(mockChain);
466
+ mockQueryBuilder.or.mockReturnValue(mockChain);
467
+ mockQueryBuilder.eq.mockReturnValue(mockChain);
468
+
469
+ const startTime = Date.now();
470
+ const result = await getAppIds(mockSupabase as any, appNames);
471
+ const endTime = Date.now();
472
+
473
+ expect(Object.keys(result)).toHaveLength(100);
474
+ expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second
475
+ });
476
+
477
+ it('caches results efficiently', async () => {
478
+ const resolver = new CachedAppIdResolver();
479
+
480
+ mockQueryBuilder.single.mockResolvedValue({
481
+ data: { id: 'app-123' },
482
+ error: null
483
+ });
484
+
485
+ const startTime = Date.now();
486
+
487
+ // First call - should hit database
488
+ await resolver.getAppId(mockSupabase as any, 'MY_APP');
489
+
490
+ // Second call - should hit cache
491
+ await resolver.getAppId(mockSupabase as any, 'MY_APP');
492
+
493
+ const endTime = Date.now();
494
+
495
+ expect(mockQueryBuilder.single).toHaveBeenCalledTimes(1);
496
+ expect(endTime - startTime).toBeLessThan(100); // Should be very fast due to caching
497
+ });
498
+ });
499
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * App ID Resolution Utility
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/AppIdResolver
5
+ * @since 1.0.0
6
+ *
7
+ * This module provides utilities to resolve app names to app IDs for database operations.
8
+ */
9
+
10
+ import type { SupabaseClient } from '@supabase/supabase-js';
11
+ import type { Database } from '../types/database';
12
+
13
+ /**
14
+ * Resolves an app name to its corresponding app ID
15
+ *
16
+ * @param supabase - Supabase client instance
17
+ * @param appName - The app name to resolve
18
+ * @returns Promise resolving to the app ID or null if not found
19
+ */
20
+ export async function getAppId(
21
+ supabase: SupabaseClient<Database>,
22
+ appName: string
23
+ ): Promise<string | null> {
24
+ try {
25
+ const { data, error } = await supabase
26
+ .from('rbac_apps')
27
+ .select('id')
28
+ .ilike('name', appName)
29
+ .eq('is_active', true)
30
+ .single();
31
+
32
+ if (error) {
33
+ console.error('Failed to resolve app ID for app name:', appName, error);
34
+ return null;
35
+ }
36
+
37
+ return (data as { id: string } | null)?.id || null;
38
+ } catch (error) {
39
+ console.error('Error resolving app ID for app name:', appName, error);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Resolves multiple app names to their corresponding app IDs
46
+ *
47
+ * @param supabase - Supabase client instance
48
+ * @param appNames - Array of app names to resolve
49
+ * @returns Promise resolving to a map of app names to app IDs
50
+ */
51
+ export async function getAppIds(
52
+ supabase: SupabaseClient<Database>,
53
+ appNames: string[]
54
+ ): Promise<Record<string, string | null>> {
55
+ try {
56
+ // For case-insensitive matching with multiple values, we need to use OR conditions
57
+ // since PostgreSQL doesn't support case-insensitive IN with ILIKE
58
+ const orConditions = appNames.map(name => `name.ilike.${name}`).join(',');
59
+
60
+ const { data, error } = await supabase
61
+ .from('rbac_apps')
62
+ .select('id, name')
63
+ .or(orConditions)
64
+ .eq('is_active', true);
65
+
66
+ if (error) {
67
+ console.error('Failed to resolve app IDs for app names:', appNames, error);
68
+ return {};
69
+ }
70
+
71
+ const result: Record<string, string | null> = {};
72
+
73
+ // Initialize all app names with null
74
+ appNames.forEach(name => {
75
+ result[name] = null;
76
+ });
77
+
78
+ // Set resolved app IDs - match case-insensitively
79
+ (data as { id: string; name: string }[] | null)?.forEach(app => {
80
+ // Find the original app name that matches (case-insensitive)
81
+ const originalName = appNames.find(name =>
82
+ name.toLowerCase() === app.name.toLowerCase()
83
+ );
84
+ if (originalName) {
85
+ result[originalName] = app.id;
86
+ }
87
+ });
88
+
89
+ return result;
90
+ } catch (error) {
91
+ console.error('Error resolving app IDs for app names:', appNames, error);
92
+ return {};
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Cached app ID resolver with TTL
98
+ */
99
+ export class CachedAppIdResolver {
100
+ private cache = new Map<string, { id: string | null; expires: number }>();
101
+ private ttl = 5 * 60 * 1000; // 5 minutes
102
+
103
+ async getAppId(
104
+ supabase: SupabaseClient<Database>,
105
+ appName: string
106
+ ): Promise<string | null> {
107
+ const now = Date.now();
108
+ const cached = this.cache.get(appName);
109
+
110
+ if (cached && cached.expires > now) {
111
+ return cached.id;
112
+ }
113
+
114
+ const id = await getAppId(supabase, appName);
115
+ this.cache.set(appName, { id, expires: now + this.ttl });
116
+
117
+ return id;
118
+ }
119
+
120
+ clearCache(): void {
121
+ this.cache.clear();
122
+ }
123
+
124
+ clearCacheForApp(appName: string): void {
125
+ this.cache.delete(appName);
126
+ }
127
+ }
128
+
129
+ // Export singleton instance
130
+ export const cachedAppIdResolver = new CachedAppIdResolver();