@everysk/apps-sdk 0.2.0 → 0.3.0-beta-20261019175049
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/README.md +342 -12
- package/dist/EveryskApp.d.ts +34 -2
- package/dist/EveryskApp.d.ts.map +1 -1
- package/dist/EveryskApp.js +35 -2
- package/dist/EveryskApp.js.map +1 -1
- package/dist/agGrid/handleRowsScrollEnd.d.ts +53 -0
- package/dist/agGrid/handleRowsScrollEnd.d.ts.map +1 -0
- package/dist/agGrid/handleRowsScrollEnd.js +47 -0
- package/dist/agGrid/handleRowsScrollEnd.js.map +1 -0
- package/dist/agGrid/index.d.ts +2 -0
- package/dist/agGrid/index.d.ts.map +1 -1
- package/dist/agGrid/index.js +2 -1
- package/dist/api/datastore.d.ts +10 -8
- package/dist/api/datastore.d.ts.map +1 -1
- package/dist/api/datastore.js +24 -55
- package/dist/api/datastore.js.map +1 -1
- package/dist/api/entityQuery.d.ts +46 -41
- package/dist/api/entityQuery.d.ts.map +1 -1
- package/dist/api/entityQuery.js +55 -14
- package/dist/api/entityQuery.js.map +1 -1
- package/dist/api/file.d.ts +26 -9
- package/dist/api/file.d.ts.map +1 -1
- package/dist/api/file.js +45 -55
- package/dist/api/file.js.map +1 -1
- package/dist/api/portfolio.d.ts +11 -8
- package/dist/api/portfolio.d.ts.map +1 -1
- package/dist/api/portfolio.js +24 -55
- package/dist/api/portfolio.js.map +1 -1
- package/dist/api/profile.d.ts +3 -2
- package/dist/api/profile.d.ts.map +1 -1
- package/dist/api/profile.js +4 -9
- package/dist/api/profile.js.map +1 -1
- package/dist/api/request.d.ts +106 -0
- package/dist/api/request.d.ts.map +1 -0
- package/dist/api/request.js +132 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/websocketsConfig.d.ts +7 -2
- package/dist/api/websocketsConfig.d.ts.map +1 -1
- package/dist/api/websocketsConfig.js +2 -10
- package/dist/api/websocketsConfig.js.map +1 -1
- package/dist/api/workflow.d.ts +84 -89
- package/dist/api/workflow.d.ts.map +1 -1
- package/dist/api/workflow.js +121 -208
- package/dist/api/workflow.js.map +1 -1
- package/dist/api/workspace.d.ts +18 -8
- package/dist/api/workspace.d.ts.map +1 -1
- package/dist/api/workspace.js +21 -26
- package/dist/api/workspace.js.map +1 -1
- package/dist/components/ui/Footer/index.d.ts.map +1 -1
- package/dist/components/ui/Footer/index.js.map +1 -1
- package/dist/components/ui/GlobalLoading/index.d.ts.map +1 -1
- package/dist/components/ui/GlobalLoading/index.js.map +1 -1
- package/dist/components/ui/PageContainer/index.d.ts +1 -1
- package/dist/components/ui/PageContainer/index.d.ts.map +1 -1
- package/dist/components/ui/PageContainer/index.js.map +1 -1
- package/dist/components/ui/Toolbar/index.d.ts +40 -3
- package/dist/components/ui/Toolbar/index.d.ts.map +1 -1
- package/dist/components/ui/Toolbar/index.js +42 -15
- package/dist/components/ui/Toolbar/index.js.map +1 -1
- package/dist/contexts/appAlertContext/provider.js.map +1 -1
- package/dist/contexts/appAlertContext/types.d.ts.map +1 -1
- package/dist/contexts/appConfigContext/index.d.ts.map +1 -1
- package/dist/contexts/appConfigContext/index.js.map +1 -1
- package/dist/contexts/appConfigContext/provider.d.ts.map +1 -1
- package/dist/contexts/appConfigContext/provider.js.map +1 -1
- package/dist/contexts/webSocketContext/handler.d.ts +3 -2
- package/dist/contexts/webSocketContext/handler.d.ts.map +1 -1
- package/dist/contexts/webSocketContext/handler.js.map +1 -1
- package/dist/contexts/webSocketContext/hooks/useWebSocket/index.d.ts +1 -1
- package/dist/contexts/webSocketContext/hooks/useWebSocket/index.js +1 -1
- package/dist/contexts/webSocketContext/hooks/useWebSocket/index.js.map +1 -1
- package/dist/contexts/webSocketContext/provider.d.ts +6 -3
- package/dist/contexts/webSocketContext/provider.d.ts.map +1 -1
- package/dist/contexts/webSocketContext/provider.js +42 -7
- package/dist/contexts/webSocketContext/provider.js.map +1 -1
- package/dist/contexts/webSocketContext/types.d.ts +7 -5
- package/dist/contexts/webSocketContext/types.d.ts.map +1 -1
- package/dist/file/index.d.ts +2 -0
- package/dist/file/index.d.ts.map +1 -1
- package/dist/hooks/datastore/useDatastoreMutations.d.ts +4 -4
- package/dist/hooks/datastore/useDatastoreMutations.d.ts.map +1 -1
- package/dist/hooks/datastore/useDatastoreMutations.js +17 -55
- package/dist/hooks/datastore/useDatastoreMutations.js.map +1 -1
- package/dist/hooks/datastore/useFetchDatastore.d.ts +30 -41
- package/dist/hooks/datastore/useFetchDatastore.d.ts.map +1 -1
- package/dist/hooks/datastore/useFetchDatastore.js +25 -59
- package/dist/hooks/datastore/useFetchDatastore.js.map +1 -1
- package/dist/hooks/datastore/useFetchDatastores.d.ts +47 -52
- package/dist/hooks/datastore/useFetchDatastores.d.ts.map +1 -1
- package/dist/hooks/datastore/useFetchDatastores.js +55 -70
- package/dist/hooks/datastore/useFetchDatastores.js.map +1 -1
- package/dist/hooks/file/fileData.d.ts +17 -0
- package/dist/hooks/file/fileData.d.ts.map +1 -0
- package/dist/hooks/file/fileData.js +10 -0
- package/dist/hooks/file/fileData.js.map +1 -0
- package/dist/hooks/file/useFetchFile.d.ts +38 -58
- package/dist/hooks/file/useFetchFile.d.ts.map +1 -1
- package/dist/hooks/file/useFetchFile.js +26 -66
- package/dist/hooks/file/useFetchFile.js.map +1 -1
- package/dist/hooks/file/useFetchFiles.d.ts +18 -52
- package/dist/hooks/file/useFetchFiles.d.ts.map +1 -1
- package/dist/hooks/file/useFetchFiles.js +53 -69
- package/dist/hooks/file/useFetchFiles.js.map +1 -1
- package/dist/hooks/file/useFileMutations.d.ts +9 -8
- package/dist/hooks/file/useFileMutations.d.ts.map +1 -1
- package/dist/hooks/file/useFileMutations.js +23 -60
- package/dist/hooks/file/useFileMutations.js.map +1 -1
- package/dist/hooks/portfolio/useFetchPortfolio.d.ts +16 -50
- package/dist/hooks/portfolio/useFetchPortfolio.d.ts.map +1 -1
- package/dist/hooks/portfolio/useFetchPortfolio.js +15 -63
- package/dist/hooks/portfolio/useFetchPortfolio.js.map +1 -1
- package/dist/hooks/portfolio/useFetchPortfolios.d.ts +13 -52
- package/dist/hooks/portfolio/useFetchPortfolios.d.ts.map +1 -1
- package/dist/hooks/portfolio/useFetchPortfolios.js +29 -71
- package/dist/hooks/portfolio/useFetchPortfolios.js.map +1 -1
- package/dist/hooks/portfolio/usePortfolioMutations.d.ts +8 -8
- package/dist/hooks/portfolio/usePortfolioMutations.d.ts.map +1 -1
- package/dist/hooks/portfolio/usePortfolioMutations.js +20 -58
- package/dist/hooks/portfolio/usePortfolioMutations.js.map +1 -1
- package/dist/hooks/profile/useFetchProfile.d.ts +18 -44
- package/dist/hooks/profile/useFetchProfile.d.ts.map +1 -1
- package/dist/hooks/profile/useFetchProfile.js +16 -48
- package/dist/hooks/profile/useFetchProfile.js.map +1 -1
- package/dist/hooks/useAxios.js.map +1 -1
- package/dist/hooks/useEntityInfiniteQuery.d.ts +88 -0
- package/dist/hooks/useEntityInfiniteQuery.d.ts.map +1 -0
- package/dist/hooks/useEntityInfiniteQuery.js +111 -0
- package/dist/hooks/useEntityInfiniteQuery.js.map +1 -0
- package/dist/hooks/useEntityMutation.d.ts +47 -0
- package/dist/hooks/useEntityMutation.d.ts.map +1 -0
- package/dist/hooks/useEntityMutation.js +58 -0
- package/dist/hooks/useEntityMutation.js.map +1 -0
- package/dist/hooks/useEntityQuery.d.ts +68 -0
- package/dist/hooks/useEntityQuery.d.ts.map +1 -0
- package/dist/hooks/useEntityQuery.js +75 -0
- package/dist/hooks/useEntityQuery.js.map +1 -0
- package/dist/hooks/workflow/useFetchWorkerExecution.d.ts +17 -34
- package/dist/hooks/workflow/useFetchWorkerExecution.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkerExecution.js +16 -46
- package/dist/hooks/workflow/useFetchWorkerExecution.js.map +1 -1
- package/dist/hooks/workflow/useFetchWorkerExecutions.d.ts +179 -41
- package/dist/hooks/workflow/useFetchWorkerExecutions.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkerExecutions.js +29 -62
- package/dist/hooks/workflow/useFetchWorkerExecutions.js.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflow.d.ts +16 -35
- package/dist/hooks/workflow/useFetchWorkflow.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflow.js +15 -48
- package/dist/hooks/workflow/useFetchWorkflow.js.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflowExecution.d.ts +17 -34
- package/dist/hooks/workflow/useFetchWorkflowExecution.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflowExecution.js +16 -46
- package/dist/hooks/workflow/useFetchWorkflowExecution.js.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflowExecutions.d.ts +22 -36
- package/dist/hooks/workflow/useFetchWorkflowExecutions.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflowExecutions.js +43 -56
- package/dist/hooks/workflow/useFetchWorkflowExecutions.js.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflows.d.ts +20 -37
- package/dist/hooks/workflow/useFetchWorkflows.d.ts.map +1 -1
- package/dist/hooks/workflow/useFetchWorkflows.js +32 -47
- package/dist/hooks/workflow/useFetchWorkflows.js.map +1 -1
- package/dist/hooks/workflow/useRunWorkflow.d.ts +21 -16
- package/dist/hooks/workflow/useRunWorkflow.d.ts.map +1 -1
- package/dist/hooks/workflow/useRunWorkflow.js +66 -29
- package/dist/hooks/workflow/useRunWorkflow.js.map +1 -1
- package/dist/hooks/workflow/useTriggerWorkflow.d.ts +20 -0
- package/dist/hooks/workflow/useTriggerWorkflow.d.ts.map +1 -0
- package/dist/hooks/workflow/useTriggerWorkflow.js +32 -0
- package/dist/hooks/workflow/useTriggerWorkflow.js.map +1 -0
- package/dist/hooks/workspace/useFetchWorkspace.d.ts +19 -34
- package/dist/hooks/workspace/useFetchWorkspace.d.ts.map +1 -1
- package/dist/hooks/workspace/useFetchWorkspace.js +16 -46
- package/dist/hooks/workspace/useFetchWorkspace.js.map +1 -1
- package/dist/hooks/workspace/useFetchWorkspaces.d.ts +17 -37
- package/dist/hooks/workspace/useFetchWorkspaces.d.ts.map +1 -1
- package/dist/hooks/workspace/useFetchWorkspaces.js +25 -47
- package/dist/hooks/workspace/useFetchWorkspaces.js.map +1 -1
- package/dist/index.d.ts +13 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/lib/abort.d.ts +58 -0
- package/dist/lib/abort.d.ts.map +1 -0
- package/dist/lib/abort.js +112 -0
- package/dist/lib/abort.js.map +1 -0
- package/dist/lib/datastoreTable.d.ts +59 -54
- package/dist/lib/datastoreTable.d.ts.map +1 -1
- package/dist/lib/datastoreTable.js +58 -62
- package/dist/lib/datastoreTable.js.map +1 -1
- package/dist/lib/file.d.ts +34 -0
- package/dist/lib/file.d.ts.map +1 -0
- package/dist/lib/file.js +63 -0
- package/dist/lib/file.js.map +1 -0
- package/dist/lib/queryClient.d.ts.map +1 -1
- package/dist/lib/queryClient.js.map +1 -1
- package/dist/lib/workflowRun.d.ts +91 -0
- package/dist/lib/workflowRun.d.ts.map +1 -0
- package/dist/lib/workflowRun.js +224 -0
- package/dist/lib/workflowRun.js.map +1 -0
- package/dist/socket/index.d.ts +1 -3
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +3 -5
- package/dist/test/mocks/index.d.ts +1 -1
- package/dist/test/mocks/index.d.ts.map +1 -1
- package/dist/test/msw/fakeApi.d.ts +43 -0
- package/dist/test/msw/fakeApi.d.ts.map +1 -0
- package/dist/test/msw/fakeApi.js +74 -0
- package/dist/test/msw/fakeApi.js.map +1 -0
- package/dist/test/msw/handlers.d.ts +20 -0
- package/dist/test/msw/handlers.d.ts.map +1 -0
- package/dist/test/msw/handlers.js +279 -0
- package/dist/test/msw/handlers.js.map +1 -0
- package/dist/test/msw/index.d.ts +5 -0
- package/dist/test/msw/index.d.ts.map +1 -0
- package/dist/test/msw/index.js +2 -0
- package/dist/test/msw/query.d.ts +34 -0
- package/dist/test/msw/query.d.ts.map +1 -0
- package/dist/test/msw/query.js +96 -0
- package/dist/test/msw/query.js.map +1 -0
- package/dist/test/msw/recorder.d.ts +30 -0
- package/dist/test/msw/recorder.d.ts.map +1 -0
- package/dist/test/msw/recorder.js +65 -0
- package/dist/test/msw/recorder.js.map +1 -0
- package/dist/test/msw/store.d.ts +57 -0
- package/dist/test/msw/store.d.ts.map +1 -0
- package/dist/test/msw/store.js +145 -0
- package/dist/test/msw/store.js.map +1 -0
- package/dist/test/renderWithProviders.d.ts +2 -2
- package/dist/test/renderWithProviders.d.ts.map +1 -1
- package/dist/test/renderWithProviders.js.map +1 -1
- package/dist/test/setup.js.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/overrides/index.d.ts +4 -2
- package/dist/theme/overrides/index.d.ts.map +1 -1
- package/dist/theme/overrides/index.js +5 -3
- package/dist/theme/overrides/index.js.map +1 -1
- package/dist/theme/overrides/muiButton.d.ts +2 -2
- package/dist/theme/overrides/muiButton.js.map +1 -1
- package/dist/theme/overrides/muiCssBaseline.d.ts.map +1 -1
- package/dist/theme/overrides/muiCssBaseline.js.map +1 -1
- package/dist/theme/overrides/muiDatePicker.js.map +1 -1
- package/dist/theme/overrides/muiFormControl.js.map +1 -1
- package/dist/theme/overrides/muiIconButton.d.ts +2 -2
- package/dist/theme/overrides/muiIconButton.js.map +1 -1
- package/dist/theme/overrides/muiSelect.d.ts.map +1 -1
- package/dist/theme/overrides/muiSelect.js.map +1 -1
- package/dist/theme/overrides/muiSvgIcon.d.ts.map +1 -1
- package/dist/theme/overrides/muiSvgIcon.js.map +1 -1
- package/dist/theme/overrides/muiTextField.js.map +1 -1
- package/dist/theme/overrides/muiToggleButton.d.ts +2 -2
- package/dist/theme/overrides/muiToggleButton.js.map +1 -1
- package/dist/theme/overrides/muiToggleButtonGroup.d.ts +2 -2
- package/dist/theme/overrides/muiToggleButtonGroup.js.map +1 -1
- package/dist/theme/themes/DefaultDark.js +2 -2
- package/dist/theme/themes/DefaultDark.js.map +1 -1
- package/dist/theme/themes/DefaultLight.js +2 -2
- package/dist/theme/themes/DefaultLight.js.map +1 -1
- package/dist/theme/themes/EveryskDark.js +2 -2
- package/dist/theme/themes/EveryskDark.js.map +1 -1
- package/dist/theme/themes/EveryskLight.js +2 -2
- package/dist/theme/themes/EveryskLight.js.map +1 -1
- package/dist/theme/themes/MidnightEmber.js +2 -2
- package/dist/theme/themes/MidnightEmber.js.map +1 -1
- package/dist/types/anyRecord.d.ts.map +1 -1
- package/dist/types/datastore.d.ts +35 -5
- package/dist/types/datastore.d.ts.map +1 -1
- package/dist/types/datastoreDataFormat.d.ts +13 -0
- package/dist/types/datastoreDataFormat.d.ts.map +1 -0
- package/dist/types/entityQuery.d.ts +86 -16
- package/dist/types/entityQuery.d.ts.map +1 -1
- package/dist/types/file.d.ts +7 -3
- package/dist/types/file.d.ts.map +1 -1
- package/dist/types/portfolio.d.ts +1 -3
- package/dist/types/portfolio.d.ts.map +1 -1
- package/dist/types/profile.d.ts +0 -10
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/types/workflow.d.ts +35 -6
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workspace.d.ts +1 -2
- package/dist/types/workspace.d.ts.map +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/package.json +10 -1
- package/src/EveryskApp.tsx +53 -2
- package/src/agGrid/handleRowsScrollEnd.ts +71 -0
- package/src/agGrid/index.ts +5 -1
- package/src/api/datastore.ts +79 -107
- package/src/api/entityQuery.ts +86 -17
- package/src/api/file.ts +110 -103
- package/src/api/portfolio.ts +82 -125
- package/src/api/profile.ts +25 -18
- package/src/api/request.ts +168 -0
- package/src/api/websocketsConfig.ts +11 -19
- package/src/api/workflow.ts +232 -334
- package/src/api/workspace.ts +43 -43
- package/src/components/ui/Footer/index.tsx +8 -7
- package/src/components/ui/GlobalLoading/index.tsx +13 -3
- package/src/components/ui/PageContainer/index.tsx +9 -2
- package/src/components/ui/Toolbar/index.tsx +105 -30
- package/src/contexts/appAlertContext/provider.tsx +1 -1
- package/src/contexts/appAlertContext/types.ts +3 -3
- package/src/contexts/appConfigContext/index.tsx +1 -1
- package/src/contexts/appConfigContext/provider.tsx +1 -5
- package/src/contexts/webSocketContext/handler.ts +8 -2
- package/src/contexts/webSocketContext/hooks/useWebSocket/index.ts +1 -1
- package/src/contexts/webSocketContext/provider.tsx +64 -14
- package/src/contexts/webSocketContext/types.ts +7 -6
- package/src/file/index.ts +2 -0
- package/src/hooks/datastore/useDatastoreMutations.ts +19 -60
- package/src/hooks/datastore/useFetchDatastore.ts +45 -73
- package/src/hooks/datastore/useFetchDatastores.ts +106 -131
- package/src/hooks/file/fileData.ts +19 -0
- package/src/hooks/file/useFetchFile.ts +41 -76
- package/src/hooks/file/useFetchFiles.ts +92 -87
- package/src/hooks/file/useFileMutations.ts +23 -73
- package/src/hooks/portfolio/useFetchPortfolio.ts +26 -78
- package/src/hooks/portfolio/useFetchPortfolios.ts +50 -93
- package/src/hooks/portfolio/usePortfolioMutations.ts +22 -72
- package/src/hooks/profile/useFetchProfile.ts +24 -65
- package/src/hooks/useAxios.ts +5 -5
- package/src/hooks/useEntityInfiniteQuery.ts +193 -0
- package/src/hooks/useEntityMutation.ts +70 -0
- package/src/hooks/useEntityQuery.ts +117 -0
- package/src/hooks/workflow/useFetchWorkerExecution.ts +27 -56
- package/src/hooks/workflow/useFetchWorkerExecutions.ts +54 -79
- package/src/hooks/workflow/useFetchWorkflow.ts +24 -58
- package/src/hooks/workflow/useFetchWorkflowExecution.ts +27 -57
- package/src/hooks/workflow/useFetchWorkflowExecutions.ts +64 -73
- package/src/hooks/workflow/useFetchWorkflows.ts +48 -64
- package/src/hooks/workflow/useRunWorkflow.ts +82 -36
- package/src/hooks/workflow/useTriggerWorkflow.ts +36 -0
- package/src/hooks/workspace/useFetchWorkspace.ts +25 -58
- package/src/hooks/workspace/useFetchWorkspaces.ts +38 -69
- package/src/index.ts +34 -19
- package/src/lib/abort.ts +131 -0
- package/src/lib/datastoreTable.ts +127 -74
- package/src/lib/file.ts +67 -0
- package/src/lib/queryClient.ts +11 -13
- package/src/lib/workflowRun.ts +329 -0
- package/src/socket/index.ts +1 -3
- package/src/test/mocks/index.ts +1 -5
- package/src/test/msw/fakeApi.ts +83 -0
- package/src/test/msw/handlers.ts +350 -0
- package/src/test/msw/index.ts +9 -0
- package/src/test/msw/query.ts +134 -0
- package/src/test/msw/recorder.ts +88 -0
- package/src/test/msw/store.ts +182 -0
- package/src/test/renderWithProviders.tsx +8 -3
- package/src/test/setup.ts +16 -16
- package/src/theme/index.ts +1 -1
- package/src/theme/overrides/index.ts +5 -3
- package/src/theme/overrides/muiButton.ts +3 -3
- package/src/theme/overrides/muiCssBaseline.ts +11 -12
- package/src/theme/overrides/muiDatePicker.ts +2 -2
- package/src/theme/overrides/muiFormControl.ts +2 -2
- package/src/theme/overrides/muiIconButton.ts +3 -3
- package/src/theme/overrides/muiSelect.ts +2 -2
- package/src/theme/overrides/muiSvgIcon.ts +2 -2
- package/src/theme/overrides/muiTextField.ts +1 -1
- package/src/theme/overrides/muiToggleButton.ts +4 -4
- package/src/theme/overrides/muiToggleButtonGroup.ts +4 -4
- package/src/theme/themes/DefaultDark.ts +2 -2
- package/src/theme/themes/DefaultLight.ts +2 -2
- package/src/theme/themes/EveryskDark.ts +2 -2
- package/src/theme/themes/EveryskLight.ts +2 -2
- package/src/theme/themes/MidnightEmber.ts +15 -15
- package/src/types/anyRecord.ts +2 -2
- package/src/types/datastore.ts +56 -22
- package/src/types/datastoreDataFormat.ts +12 -0
- package/src/types/entityQuery.ts +122 -38
- package/src/types/file.ts +8 -6
- package/src/types/portfolio.ts +70 -74
- package/src/types/profile.ts +0 -11
- package/src/types/workflow.ts +38 -9
- package/src/types/workspace.ts +2 -4
- package/src/ui/index.ts +1 -1
- package/src/workflow/index.ts +5 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { http, HttpResponse, type HttpHandler, type JsonBodyType } from "msw";
|
|
2
|
+
|
|
3
|
+
import type { AnyRecord } from "@/types/anyRecord";
|
|
4
|
+
import type { WorkerExecution } from "@/types/workflow";
|
|
5
|
+
|
|
6
|
+
import { applyListQuery, applyTextSearch, QueryError } from "./query";
|
|
7
|
+
import { parsedQuery, pathOf } from "./recorder";
|
|
8
|
+
import { complete, newId, nowSeconds, type FakeApiStore } from "./store";
|
|
9
|
+
|
|
10
|
+
export type Failure = { method: string; path: string; status: number; message: string };
|
|
11
|
+
|
|
12
|
+
/** What the handlers read and write: the store, and the failures queued. */
|
|
13
|
+
export type FakeState = {
|
|
14
|
+
store: () => FakeApiStore;
|
|
15
|
+
takeFailure: (method: string, path: string) => Failure | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Answer = { status?: number; json: JsonBodyType };
|
|
19
|
+
|
|
20
|
+
type Context = { url: URL; path: string; segments: string[]; body: unknown };
|
|
21
|
+
|
|
22
|
+
/** The page size a text-query list answers with when none is given. Source: `ConfigDefaults.py`. */
|
|
23
|
+
const TEXT_PAGE_SIZE = 10;
|
|
24
|
+
|
|
25
|
+
export const errorBody = (status: number, message: string) => ({ code: status, message });
|
|
26
|
+
|
|
27
|
+
const notFound = (entity: string, id: string): Answer => ({
|
|
28
|
+
status: 404,
|
|
29
|
+
json: errorBody(404, `${entity} ${id} not found`),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wraps a responder: answers with a failure the test queued for the request,
|
|
34
|
+
* with the API's error body when the responder throws a `QueryError`, or with what it returns.
|
|
35
|
+
*/
|
|
36
|
+
const answering =
|
|
37
|
+
(state: FakeState, respond: (context: Context) => Answer) =>
|
|
38
|
+
async ({ request }: { request: Request }) => {
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
const path = pathOf(url);
|
|
41
|
+
const method = request.method.toUpperCase();
|
|
42
|
+
const body = ["POST", "PUT"].includes(method)
|
|
43
|
+
? await request.json().catch(() => undefined)
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const failure = state.takeFailure(method, path);
|
|
47
|
+
let answer: Answer;
|
|
48
|
+
if (failure) {
|
|
49
|
+
answer = { status: failure.status, json: errorBody(failure.status, failure.message) };
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
answer = respond({ url, path, segments: path.split("/").filter(Boolean), body });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!(error instanceof QueryError)) throw error;
|
|
55
|
+
answer = { status: error.status, json: errorBody(error.status, error.message) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return HttpResponse.json(answer.json, { status: answer.status ?? 200 });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** A page of a text-query list: the URL's `page_size` (10 by default) and `page_token`. */
|
|
62
|
+
const textPage = (records: AnyRecord[], url: URL) => {
|
|
63
|
+
const offset = Number(url.searchParams.get("page_token") ?? 0);
|
|
64
|
+
const end = offset + Number(url.searchParams.get("page_size") ?? TEXT_PAGE_SIZE);
|
|
65
|
+
return {
|
|
66
|
+
page: records.slice(offset, end),
|
|
67
|
+
nextPageToken: end < records.length ? String(end) : null,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type JsonQueryEntity = {
|
|
72
|
+
/** The URL segment and the list's key: `datastores`. */
|
|
73
|
+
plural: "datastores" | "files" | "portfolios";
|
|
74
|
+
/** A single record's key: `datastore`. */
|
|
75
|
+
singular: string;
|
|
76
|
+
/** For error messages: `Datastore`. */
|
|
77
|
+
label: string;
|
|
78
|
+
create: (record: AnyRecord) => AnyRecord;
|
|
79
|
+
/** What a read by id answers, given the URL: a file leaves its data out on `with_data=false`. */
|
|
80
|
+
read?: (record: AnyRecord, url: URL) => AnyRecord;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The five routes of an entity listed by JSON query: list, read, create, update and delete,
|
|
85
|
+
* each answered from the store with the API's envelopes — the plural key with
|
|
86
|
+
* `next_page_token` for a list, the singular key for one record, and a delete by id answering
|
|
87
|
+
* one `{ id, name, deleted }`.
|
|
88
|
+
*/
|
|
89
|
+
const jsonQueryEntity = (state: FakeState, entity: JsonQueryEntity): HttpHandler[] => {
|
|
90
|
+
const { plural, singular, label } = entity;
|
|
91
|
+
const records = () => state.store()[plural] as unknown as AnyRecord[];
|
|
92
|
+
const replace = (next: AnyRecord[]) => {
|
|
93
|
+
(state.store() as unknown as Record<string, AnyRecord[]>)[plural] = next;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
http.get(
|
|
98
|
+
`*/api/${plural}`,
|
|
99
|
+
answering(state, ({ url }) => {
|
|
100
|
+
const query = parsedQuery(url.searchParams.get("query"));
|
|
101
|
+
if (typeof query === "object") {
|
|
102
|
+
const { page, nextPageToken } = applyListQuery(records(), query, {
|
|
103
|
+
pageSize: url.searchParams.get("page_size"),
|
|
104
|
+
pageToken: url.searchParams.get("page_token"),
|
|
105
|
+
});
|
|
106
|
+
return { json: { [plural]: page, next_page_token: nextPageToken } };
|
|
107
|
+
}
|
|
108
|
+
const { page, nextPageToken } = textPage(
|
|
109
|
+
applyTextSearch(records(), query ?? ""),
|
|
110
|
+
url,
|
|
111
|
+
);
|
|
112
|
+
return { json: { [plural]: page, next_page_token: nextPageToken } };
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
http.get(
|
|
116
|
+
`*/api/${plural}/:id`,
|
|
117
|
+
answering(state, ({ url, segments }) => {
|
|
118
|
+
const id = segments[1] ?? "";
|
|
119
|
+
if (url.searchParams.get("workspace") === "") {
|
|
120
|
+
return { status: 403, json: errorBody(403, "An empty workspace is refused.") };
|
|
121
|
+
}
|
|
122
|
+
const found = records().find((r) => r.id === id);
|
|
123
|
+
if (!found) return notFound(label, id);
|
|
124
|
+
return { json: { [singular]: entity.read ? entity.read(found, url) : found } };
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
http.post(
|
|
128
|
+
`*/api/${plural}`,
|
|
129
|
+
answering(state, ({ body }) => {
|
|
130
|
+
const now = nowSeconds();
|
|
131
|
+
const created = entity.create({
|
|
132
|
+
...(body as AnyRecord),
|
|
133
|
+
id: undefined,
|
|
134
|
+
created: now,
|
|
135
|
+
updated: now,
|
|
136
|
+
});
|
|
137
|
+
records().push(created);
|
|
138
|
+
return { json: { [singular]: created } };
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
http.put(
|
|
142
|
+
`*/api/${plural}/:id`,
|
|
143
|
+
answering(state, ({ segments, body }) => {
|
|
144
|
+
const id = segments[1] ?? "";
|
|
145
|
+
const index = records().findIndex((r) => r.id === id);
|
|
146
|
+
if (index === -1) return notFound(label, id);
|
|
147
|
+
const updated = {
|
|
148
|
+
...records()[index],
|
|
149
|
+
...(body as AnyRecord),
|
|
150
|
+
id,
|
|
151
|
+
updated: nowSeconds(),
|
|
152
|
+
};
|
|
153
|
+
records()[index] = updated;
|
|
154
|
+
return { json: { [singular]: updated } };
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
http.delete(
|
|
158
|
+
`*/api/${plural}/:id`,
|
|
159
|
+
answering(state, ({ segments }) => {
|
|
160
|
+
const id = segments[1] ?? "";
|
|
161
|
+
const found = records().find((r) => r.id === id);
|
|
162
|
+
if (!found) return notFound(label, id);
|
|
163
|
+
replace(records().filter((r) => r.id !== id));
|
|
164
|
+
return { json: { [singular]: { id, name: found.name, deleted: true } } };
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
];
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Worker executions as `GET /workflows/:id/worker_executions` groups them: one group per worker,
|
|
172
|
+
* in the order the workers first appear, a worker with several runs as a parallel group.
|
|
173
|
+
*/
|
|
174
|
+
const groupWorkerExecutions = (executions: WorkerExecution[]) => {
|
|
175
|
+
const byWorker = new Map<string, WorkerExecution[]>();
|
|
176
|
+
for (const execution of executions) {
|
|
177
|
+
byWorker.set(execution.worker_id, [
|
|
178
|
+
...(byWorker.get(execution.worker_id) ?? []),
|
|
179
|
+
execution,
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
const groups = [...byWorker.values()].map((runs) => {
|
|
183
|
+
const base = { worker_id: runs[0]!.worker_id, worker_name: runs[0]!.worker_name };
|
|
184
|
+
return runs.length > 1
|
|
185
|
+
? { ...base, is_parallel_execution: true, executions: runs }
|
|
186
|
+
: { ...base, is_parallel_execution: false, execution: runs[0] };
|
|
187
|
+
});
|
|
188
|
+
const parallel = [...byWorker.values()].filter((runs) => runs.length > 1);
|
|
189
|
+
return {
|
|
190
|
+
worker_executions: groups,
|
|
191
|
+
parallel_index_max:
|
|
192
|
+
parallel.length > 0 ? Math.max(...parallel.map((r) => r.length - 1)) : -1,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const workflowHandlers = (state: FakeState): HttpHandler[] => [
|
|
197
|
+
// The workflow list takes only a text search, under the `workspace` it is given.
|
|
198
|
+
http.get(
|
|
199
|
+
"*/api/workflows",
|
|
200
|
+
answering(state, ({ url }) => {
|
|
201
|
+
const workspace = url.searchParams.get("workspace") ?? "main";
|
|
202
|
+
const inWorkspace = state.store().workflows.filter((w) => w.workspace === workspace);
|
|
203
|
+
const { page, nextPageToken } = textPage(
|
|
204
|
+
applyTextSearch(inWorkspace, url.searchParams.get("query") ?? ""),
|
|
205
|
+
url,
|
|
206
|
+
);
|
|
207
|
+
return { json: { workflows: page, next_page_token: nextPageToken } };
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
http.get(
|
|
211
|
+
"*/api/workflows/:id",
|
|
212
|
+
answering(state, ({ segments }) => {
|
|
213
|
+
const id = segments[1] ?? "";
|
|
214
|
+
const found = state.store().workflows.find((w) => w.id === id);
|
|
215
|
+
return found ? { json: { workflow: found } } : notFound("Workflow", id);
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
// 202 with the execution it started, still RUNNING, and a `result` that is always null.
|
|
219
|
+
http.post(
|
|
220
|
+
"*/api/workflows/:id/run",
|
|
221
|
+
answering(state, ({ segments, body }) => {
|
|
222
|
+
const id = segments[1] ?? "";
|
|
223
|
+
const workflow = state.store().workflows.find((w) => w.id === id);
|
|
224
|
+
if (!workflow) return notFound("Workflow", id);
|
|
225
|
+
const now = nowSeconds();
|
|
226
|
+
const execution = complete.workflowExecution({
|
|
227
|
+
id: newId("wfex"),
|
|
228
|
+
workflow_id: id,
|
|
229
|
+
workflow_name: workflow.name,
|
|
230
|
+
workspace: (body as AnyRecord | undefined)?.workspace ?? workflow.workspace,
|
|
231
|
+
status: "RUNNING",
|
|
232
|
+
run_status: "RUNNING",
|
|
233
|
+
started: now,
|
|
234
|
+
created: now,
|
|
235
|
+
updated: now,
|
|
236
|
+
});
|
|
237
|
+
state.store().workflowExecutions.push(execution);
|
|
238
|
+
return { status: 202, json: { workflow_execution: execution, result: null } };
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
// Ignores a JSON query: reads `workflow_execution_id`, `worker_id`, `page_size` and
|
|
242
|
+
// `page_token` from the URL. With `workflow_execution_id` it answers that one execution.
|
|
243
|
+
http.get(
|
|
244
|
+
"*/api/workflows/:id/workflow_executions",
|
|
245
|
+
answering(state, ({ url, segments }) => {
|
|
246
|
+
const workflowId = segments[1] ?? "";
|
|
247
|
+
const executions = state
|
|
248
|
+
.store()
|
|
249
|
+
.workflowExecutions.filter((e) => e.workflow_id === workflowId);
|
|
250
|
+
const executionId = url.searchParams.get("workflow_execution_id");
|
|
251
|
+
if (executionId) {
|
|
252
|
+
const found = executions.find((e) => e.id === executionId);
|
|
253
|
+
return found
|
|
254
|
+
? { json: { workflow_execution: found } }
|
|
255
|
+
: notFound("Workflow execution", executionId);
|
|
256
|
+
}
|
|
257
|
+
const workerId = url.searchParams.get("worker_id");
|
|
258
|
+
const ran = workerId
|
|
259
|
+
? executions.filter((e) =>
|
|
260
|
+
state
|
|
261
|
+
.store()
|
|
262
|
+
.workerExecutions.some(
|
|
263
|
+
(w) => w.workflow_execution_id === e.id && w.worker_id === workerId,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
: executions;
|
|
267
|
+
const { page, nextPageToken } = textPage(ran, url);
|
|
268
|
+
return { json: { workflow_executions: page, next_page_token: nextPageToken } };
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
// Not paginated. With `worker_execution_id` it answers that one run; with
|
|
272
|
+
// `workflow_execution_id`, every worker of that execution, grouped.
|
|
273
|
+
http.get(
|
|
274
|
+
"*/api/workflows/:id/worker_executions",
|
|
275
|
+
answering(state, ({ url, segments }) => {
|
|
276
|
+
const workflowId = segments[1] ?? "";
|
|
277
|
+
const runs = state.store().workerExecutions.filter((w) => w.workflow_id === workflowId);
|
|
278
|
+
const workerExecutionId = url.searchParams.get("worker_execution_id");
|
|
279
|
+
if (workerExecutionId) {
|
|
280
|
+
const found = runs.find((w) => w.id === workerExecutionId);
|
|
281
|
+
return found
|
|
282
|
+
? { json: { worker_execution: found } }
|
|
283
|
+
: notFound("Worker execution", workerExecutionId);
|
|
284
|
+
}
|
|
285
|
+
const executionId = url.searchParams.get("workflow_execution_id");
|
|
286
|
+
return {
|
|
287
|
+
json: groupWorkerExecutions(
|
|
288
|
+
runs.filter((w) => w.workflow_execution_id === executionId),
|
|
289
|
+
),
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const workspaceHandlers = (state: FakeState): HttpHandler[] => [
|
|
296
|
+
http.get(
|
|
297
|
+
"*/api/workspaces",
|
|
298
|
+
answering(state, ({ url }) => {
|
|
299
|
+
const { page, nextPageToken } = textPage(
|
|
300
|
+
applyTextSearch(state.store().workspaces, url.searchParams.get("query") ?? ""),
|
|
301
|
+
url,
|
|
302
|
+
);
|
|
303
|
+
return { json: { workspaces: page, next_page_token: nextPageToken } };
|
|
304
|
+
}),
|
|
305
|
+
),
|
|
306
|
+
// A workspace is identified by its name.
|
|
307
|
+
http.get(
|
|
308
|
+
"*/api/workspaces/:name",
|
|
309
|
+
answering(state, ({ segments }) => {
|
|
310
|
+
const name = decodeURIComponent(segments[1] ?? "");
|
|
311
|
+
const found = state.store().workspaces.find((w) => w.name === name);
|
|
312
|
+
return found ? { json: { workspace: found } } : notFound("Workspace", name);
|
|
313
|
+
}),
|
|
314
|
+
),
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
/** Every route the SDK's request functions call. */
|
|
318
|
+
export const fakeApiHandlers = (state: FakeState): HttpHandler[] => [
|
|
319
|
+
...jsonQueryEntity(state, {
|
|
320
|
+
plural: "datastores",
|
|
321
|
+
singular: "datastore",
|
|
322
|
+
label: "Datastore",
|
|
323
|
+
create: (r) => complete.datastore(r),
|
|
324
|
+
}),
|
|
325
|
+
...jsonQueryEntity(state, {
|
|
326
|
+
plural: "files",
|
|
327
|
+
singular: "file",
|
|
328
|
+
label: "File",
|
|
329
|
+
create: (r) => complete.file({ ...r, hash: undefined, url: undefined }),
|
|
330
|
+
read: (file, url) =>
|
|
331
|
+
url.searchParams.get("with_data") === "false" ? { ...file, data: null } : file,
|
|
332
|
+
}),
|
|
333
|
+
...jsonQueryEntity(state, {
|
|
334
|
+
plural: "portfolios",
|
|
335
|
+
singular: "portfolio",
|
|
336
|
+
label: "Portfolio",
|
|
337
|
+
create: (r) => complete.portfolio(r),
|
|
338
|
+
}),
|
|
339
|
+
...workflowHandlers(state),
|
|
340
|
+
...workspaceHandlers(state),
|
|
341
|
+
// An app-only route; the SDK reads only `profile` from it.
|
|
342
|
+
http.get(
|
|
343
|
+
"*/api/profile",
|
|
344
|
+
answering(state, () => ({ json: { status: "OK", profile: state.store().profile } })),
|
|
345
|
+
),
|
|
346
|
+
http.get(
|
|
347
|
+
"*/api/websockets_config",
|
|
348
|
+
answering(state, () => ({ json: state.store().websocketsConfig })),
|
|
349
|
+
),
|
|
350
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// `@everysk/apps-sdk/test/msw` — a fake of the API behind `/api`, for an app's tests.
|
|
2
|
+
//
|
|
3
|
+
// Its own subpath because `msw` is an optional peer: an app that only renders components with
|
|
4
|
+
// `@everysk/apps-sdk/test` does not need it installed.
|
|
5
|
+
|
|
6
|
+
export * from "./fakeApi";
|
|
7
|
+
export type { RecordedRequest } from "./recorder";
|
|
8
|
+
export type { FakeApiSeed, FakeApiStore } from "./store";
|
|
9
|
+
export type { ListQuery } from "./query";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { AnyRecord } from "@/types/anyRecord";
|
|
2
|
+
import type { FilterOperator } from "@/types/entityQuery";
|
|
3
|
+
|
|
4
|
+
/** The JSON query a list read sends in `?query=`, as `buildQueryObject` writes it. */
|
|
5
|
+
export type ListQuery = {
|
|
6
|
+
filters?: unknown[][];
|
|
7
|
+
order?: string[];
|
|
8
|
+
projection?: string | string[];
|
|
9
|
+
page_size?: number;
|
|
10
|
+
page_token?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** The page size a JSON-query list answers with when none is given. Source: `ConfigDefaults.py`. */
|
|
14
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
15
|
+
|
|
16
|
+
const compare = (a: unknown, b: unknown): number => {
|
|
17
|
+
if (a === b) return 0;
|
|
18
|
+
if (a === undefined || a === null) return -1;
|
|
19
|
+
if (b === undefined || b === null) return 1;
|
|
20
|
+
return (a as string | number) < (b as string | number) ? -1 : 1;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Each operator the SDK can send, as the API applies it. Keyed by `FilterOperator`, so an
|
|
25
|
+
* operator added to the SDK does not compile until the fake knows it, rather than failing inside
|
|
26
|
+
* an app's test.
|
|
27
|
+
*/
|
|
28
|
+
const OPERATORS: Record<FilterOperator, (actual: unknown, value: unknown) => boolean> = {
|
|
29
|
+
"=": (actual, value) => actual === value,
|
|
30
|
+
"!=": (actual, value) => actual !== value,
|
|
31
|
+
">": (actual, value) => compare(actual, value) > 0,
|
|
32
|
+
">=": (actual, value) => compare(actual, value) >= 0,
|
|
33
|
+
"<": (actual, value) => compare(actual, value) < 0,
|
|
34
|
+
"<=": (actual, value) => compare(actual, value) <= 0,
|
|
35
|
+
IN: (actual, value) => Array.isArray(value) && value.includes(actual),
|
|
36
|
+
NOT_IN: (actual, value) => Array.isArray(value) && !value.includes(actual),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const isOperator = (op: unknown): op is FilterOperator =>
|
|
40
|
+
typeof op === "string" && Object.hasOwn(OPERATORS, op);
|
|
41
|
+
|
|
42
|
+
/** A filter tuple as the API reads it: two items are `=`, three name the operator. */
|
|
43
|
+
const matches = (record: AnyRecord, tuple: unknown[]): boolean => {
|
|
44
|
+
const [field, op, value] = tuple.length === 2 ? [tuple[0], "=", tuple[1]] : tuple;
|
|
45
|
+
if (!isOperator(op)) throw new QueryError(`Unknown filter operator: ${String(op)}`);
|
|
46
|
+
|
|
47
|
+
return OPERATORS[op](record[field as string], value);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** A query the API refuses: the handler answers it with the API's error body and status. */
|
|
51
|
+
export class QueryError extends Error {
|
|
52
|
+
status: number;
|
|
53
|
+
|
|
54
|
+
constructor(message: string, status = 400) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "QueryError";
|
|
57
|
+
this.status = status;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ordered = (records: AnyRecord[], order: string[] = []): AnyRecord[] =>
|
|
62
|
+
[...records].sort((a, b) => {
|
|
63
|
+
for (const key of order) {
|
|
64
|
+
const desc = key.startsWith("-");
|
|
65
|
+
const field = desc ? key.slice(1) : key;
|
|
66
|
+
const result = compare(a[field], b[field]);
|
|
67
|
+
if (result !== 0) return desc ? -result : result;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One field or an array, never a comma-joined string, and never inclusions mixed with
|
|
74
|
+
* exclusions: the API answers both with a 500 (`_normalize_projection`).
|
|
75
|
+
*/
|
|
76
|
+
const project = (records: AnyRecord[], projection?: string | string[]): AnyRecord[] => {
|
|
77
|
+
if (projection === undefined || projection === "") return records;
|
|
78
|
+
const fields = Array.isArray(projection) ? projection : [projection];
|
|
79
|
+
if (fields.some((f) => f.includes(","))) {
|
|
80
|
+
throw new QueryError("A projection is one field or an array, not a list in a string.", 500);
|
|
81
|
+
}
|
|
82
|
+
const excluded = fields.filter((f) => f.startsWith("-")).map((f) => f.slice(1));
|
|
83
|
+
if (excluded.length > 0 && excluded.length < fields.length) {
|
|
84
|
+
throw new QueryError("A projection cannot mix inclusions with exclusions.", 500);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return records.map((record) =>
|
|
88
|
+
excluded.length > 0
|
|
89
|
+
? Object.fromEntries(Object.entries(record).filter(([k]) => !excluded.includes(k)))
|
|
90
|
+
: Object.fromEntries(
|
|
91
|
+
Object.entries(record).filter(([k]) => k === "id" || fields.includes(k)),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* applyListQuery — a JSON-query list read, as the API answers it: the query's filters (the
|
|
98
|
+
* `workspace` URL parameter is ignored on this path), its order and projection, and a page.
|
|
99
|
+
* The URL's `page_size` and `page_token` win over the query's.
|
|
100
|
+
*
|
|
101
|
+
* The page token is an offset here, opaque to the caller as the API's is.
|
|
102
|
+
*/
|
|
103
|
+
export const applyListQuery = (
|
|
104
|
+
records: AnyRecord[],
|
|
105
|
+
query: ListQuery,
|
|
106
|
+
url: { pageSize?: string | null; pageToken?: string | null } = {},
|
|
107
|
+
): { page: AnyRecord[]; nextPageToken: string | null } => {
|
|
108
|
+
const filtered = records.filter((record) =>
|
|
109
|
+
(query.filters ?? []).every((tuple) => matches(record, tuple)),
|
|
110
|
+
);
|
|
111
|
+
const sorted = ordered(filtered, query.order);
|
|
112
|
+
|
|
113
|
+
const pageSize = Number(url.pageSize ?? query.page_size ?? DEFAULT_PAGE_SIZE);
|
|
114
|
+
const offset = Number(url.pageToken ?? query.page_token ?? 0);
|
|
115
|
+
const end = offset + pageSize;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
page: project(sorted.slice(offset, end), query.projection),
|
|
119
|
+
nextPageToken: end < sorted.length ? String(end) : null,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A text search, the other form a list's `query` takes: an id, `#tag`, `&link_uid`, or a name
|
|
125
|
+
* prefix. The workflow and workspace lists take only this form.
|
|
126
|
+
*/
|
|
127
|
+
export const applyTextSearch = (records: AnyRecord[], text: string): AnyRecord[] => {
|
|
128
|
+
if (text === "") return records;
|
|
129
|
+
if (text.startsWith("#")) {
|
|
130
|
+
return records.filter((r) => Array.isArray(r.tags) && r.tags.includes(text.slice(1)));
|
|
131
|
+
}
|
|
132
|
+
if (text.startsWith("&")) return records.filter((r) => r.link_uid === text.slice(1));
|
|
133
|
+
return records.filter((r) => r.id === text || String(r.name ?? "").startsWith(text));
|
|
134
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { SetupServer } from "msw/node";
|
|
2
|
+
|
|
3
|
+
import type { ListQuery } from "./query";
|
|
4
|
+
|
|
5
|
+
/** A request the app sent, as a test asserts on it. */
|
|
6
|
+
export type RecordedRequest = {
|
|
7
|
+
method: string;
|
|
8
|
+
/** After `/api`: `/datastores`, `/datastores/dats_1`. */
|
|
9
|
+
path: string;
|
|
10
|
+
/** The URL's parameters other than `query`. */
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
/** The `query` parameter, parsed when it is a JSON query; a text search stays a string. */
|
|
13
|
+
query?: ListQuery | string;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
/** `0` for a network error a handler answered with `HttpResponse.error()`. */
|
|
16
|
+
status: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const API_PATH = /^.*?\/api(?=\/)/;
|
|
20
|
+
|
|
21
|
+
export const pathOf = (url: URL) => url.pathname.replace(API_PATH, "");
|
|
22
|
+
|
|
23
|
+
export const parsedQuery = (raw: string | null): ListQuery | string | undefined => {
|
|
24
|
+
if (raw === null) return undefined;
|
|
25
|
+
try {
|
|
26
|
+
const parsed: unknown = JSON.parse(raw);
|
|
27
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
28
|
+
? (parsed as ListQuery)
|
|
29
|
+
: raw;
|
|
30
|
+
} catch {
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parsedBody = (text: string): unknown => {
|
|
36
|
+
if (text === "") return undefined;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Records every request to `/api` the server answers, from MSW's own events rather than from the
|
|
46
|
+
* fake's handlers: a handler a test adds with `server.use` answers in their place, and a request
|
|
47
|
+
* it answered went missing from the log, as if the page had never sent it.
|
|
48
|
+
*
|
|
49
|
+
* The body is read from a clone taken when the request starts, before a handler consumes it. The
|
|
50
|
+
* entry is logged when the response goes out, in the order the responses do.
|
|
51
|
+
*
|
|
52
|
+
* @param {SetupServer} server - The server whose requests to record.
|
|
53
|
+
* @param {() => RecordedRequest[]} log - The current log; a getter, as it is replaced per test.
|
|
54
|
+
*/
|
|
55
|
+
export const recordRequests = (server: SetupServer, log: () => RecordedRequest[]) => {
|
|
56
|
+
const started = new Map<string, RecordedRequest>();
|
|
57
|
+
|
|
58
|
+
server.events.on("request:start", ({ request, requestId }) => {
|
|
59
|
+
const url = new URL(request.url);
|
|
60
|
+
if (!API_PATH.test(url.pathname)) return;
|
|
61
|
+
|
|
62
|
+
const entry: RecordedRequest = {
|
|
63
|
+
method: request.method.toUpperCase(),
|
|
64
|
+
path: pathOf(url),
|
|
65
|
+
params: Object.fromEntries([...url.searchParams].filter(([key]) => key !== "query")),
|
|
66
|
+
query: parsedQuery(url.searchParams.get("query")),
|
|
67
|
+
body: undefined,
|
|
68
|
+
status: 0,
|
|
69
|
+
};
|
|
70
|
+
started.set(requestId, entry);
|
|
71
|
+
if (["POST", "PUT", "PATCH"].includes(entry.method)) {
|
|
72
|
+
void request
|
|
73
|
+
.clone()
|
|
74
|
+
.text()
|
|
75
|
+
.then((text) => {
|
|
76
|
+
entry.body = parsedBody(text);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.events.on("response:mocked", ({ response, requestId }) => {
|
|
82
|
+
const entry = started.get(requestId);
|
|
83
|
+
if (!entry) return;
|
|
84
|
+
started.delete(requestId);
|
|
85
|
+
entry.status = response.status;
|
|
86
|
+
log().push(entry);
|
|
87
|
+
});
|
|
88
|
+
};
|