diff --git a/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetadataMigrationServerMode.test.tsx b/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetadataMigrationServerMode.test.tsx new file mode 100644 index 00000000000..02b40d9283b --- /dev/null +++ b/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetadataMigrationServerMode.test.tsx @@ -0,0 +1,150 @@ +/* eslint-disable global-require */ +import { renderHook } from '@testing-library/react-hooks'; +import { screen, waitFor as testLibWaitFor } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import React from 'react'; +import { useMetadataVersion } from '../../hooks/useMetadataVersion'; +import { useMetadataMigration } from '../../hooks/useMetadataMigration'; +import { + wrapper, + renderWithClient, +} from '../../../../hooks/__tests__/common/decorator'; + +const metadata = { + resource_version: 1, + sources: [], +}; + +const server = setupServer( + rest.post('http://localhost/v1/metadata', (req, res, ctx) => { + if ((req.body as Record).type === 'export_metadata') { + return res(ctx.status(200), ctx.json(metadata)); + } + + if ( + (req.body as Record).type === 'pg_create_remote_relationship' + ) { + metadata.resource_version += 1; + return res( + ctx.status(200), + ctx.json({ message: 'mock success response from server' }) + ); + } + }) +); + +describe('using the mutation in server mode', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => null); + }); + + afterEach(() => { + jest.spyOn(console, 'error').mockRestore(); + }); + + beforeAll(() => server.listen()); + afterAll(() => server.close()); + + it('should increment metadata version by 1 after successful mutatation', async () => { + const onSuccessMock = jest.fn(); + const onMutationSuccessMock = jest.fn(); + + function Page() { + const mutationCallBack = () => { + onMutationSuccessMock(); + }; + + const mutation = useMetadataMigration({ onSuccess: mutationCallBack }); + const query = useMetadataVersion(); + + if (query.isSuccess) onSuccessMock(); + + return ( + <> + +

{query.isSuccess ? JSON.stringify(query.data) : 'NA'}

+ + ); + } + + renderWithClient(); + + await testLibWaitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(1); + }); + + await testLibWaitFor(() => { + expect(screen.getByRole('heading')).toMatchInlineSnapshot(` +

+ 1 +

+ `); + }); + + screen.getByRole('button', { name: /mutate/i }).click(); + + await testLibWaitFor(() => { + expect(onMutationSuccessMock).toHaveBeenCalledTimes(1); + }); + + await testLibWaitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(5); + }); + + await testLibWaitFor(() => { + expect(screen.getByRole('heading')).toMatchInlineSnapshot(` +

+ 2 +

+ `); + }); + }); + + it('should return an error when `` is passed to source', async () => { + const { result, waitFor } = renderHook(() => useMetadataMigration(), { + wrapper, + }); + + result.current.mutate({ + source: '', + query: { + type: 'pg_create_remote_relationship', + args: {}, + }, + migrationName: '', + }); + + await waitFor(() => result.current.isError); + expect(result.current?.error?.message).toBe('source cannot be empty'); + }); + + it('should call the /v1/metadata when console is running in server mode', async () => { + const { result, waitFor } = renderHook(() => useMetadataMigration(), { + wrapper, + }); + + result.current.mutate({ + source: 'default', + query: { type: 'pg_create_remote_relationship', args: {} }, + migrationName: '', + }); + + await waitFor(() => result.current.isSuccess); + expect(result.current?.data).toMatchInlineSnapshot(` + Object { + "message": "mock success response from server", + } + `); + }); +}); diff --git a/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetataMigrationCLIMode.test.tsx b/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetataMigrationCLIMode.test.tsx new file mode 100644 index 00000000000..3f09d0c8e61 --- /dev/null +++ b/console/src/features/MetadataAPI/__tests__/useMetadataMigration/useMetataMigrationCLIMode.test.tsx @@ -0,0 +1,156 @@ +/* eslint-disable global-require */ +import { renderHook } from '@testing-library/react-hooks'; +import { screen, waitFor as testLibWaitFor } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import React from 'react'; +import { useMetadataMigration } from '../../hooks/useMetadataMigration'; +import { useMetadataVersion } from '../../hooks/useMetadataVersion'; +import { + renderWithClient, + wrapper, +} from '../../../../hooks/__tests__/common/decorator'; + +const metadata = { + resource_version: 1, + sources: [], +}; + +function setCLIEnvVars() { + /* eslint no-underscore-dangle: 0 */ + + (window as any).__env = { + ...(window as any).__env, + consoleMode: 'cli', + apiHost: 'http://localhost', + apiPort: '9693', + }; +} + +const server = setupServer( + rest.post('http://localhost:9693/apis/metadata', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ message: 'mock success response from cli server' }) + ); + }), + rest.get('http://localhost:9693/apis/migrate/settings', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ message: 'mock success response from cli server' }) + ); + }), + rest.post('http://localhost/v1/metadata', (req, res, ctx) => { + if ((req.body as Record).type === 'export_metadata') { + return res(ctx.status(200), ctx.json(metadata)); + } + + if ( + (req.body as Record).type === 'pg_create_remote_relationship' + ) { + metadata.resource_version += 1; + return res( + ctx.status(200), + ctx.json({ message: 'mock success response from cli server' }) + ); + } + }) +); + +describe('in CLI mode', () => { + beforeEach(() => { + setCLIEnvVars(); + jest.spyOn(console, 'error').mockImplementation(() => null); + }); + + afterEach(() => { + jest.spyOn(console, 'error').mockRestore(); + }); + + beforeAll(() => server.listen()); + afterAll(() => server.close()); + + it('should increment metadata version by 1 after successful mutatation', async () => { + const onSuccessMock = jest.fn(); + const onMutationSuccessMock = jest.fn(); + + function Page() { + const mutationCallBack = () => { + onMutationSuccessMock(); + }; + + const mutation = useMetadataMigration({ onSuccess: mutationCallBack }); + const query = useMetadataVersion(); + + if (query.isSuccess) onSuccessMock(); + + return ( + <> + +

{query.isSuccess ? JSON.stringify(query.data) : 'NA'}

+ + ); + } + + renderWithClient(); + + await testLibWaitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(1); + }); + + await testLibWaitFor(() => { + expect(screen.getByRole('heading')).toMatchInlineSnapshot(` +

+ 1 +

+ `); + }); + + screen.getByRole('button', { name: /mutate/i }).click(); + + await testLibWaitFor(() => { + expect(onMutationSuccessMock).toHaveBeenCalledTimes(1); + }); + + await testLibWaitFor(() => { + expect(onSuccessMock).toHaveBeenCalledTimes(5); + }); + + await testLibWaitFor(() => { + expect(screen.getByRole('heading')).toMatchInlineSnapshot(` +

+ 2 +

+ `); + }); + }); + + it('should call the /apis/migrate when console is running in cli mode', async () => { + const { result, waitFor } = renderHook(() => useMetadataMigration(), { + wrapper, + }); + + result.current.mutate({ + source: 'default', + query: { type: 'pg_create_remote_relationship', args: {} }, + migrationName: '', + }); + await waitFor(() => result.current.isSuccess); + + expect(result.current?.data).toMatchInlineSnapshot(` + Object { + "message": "mock success response from cli server", + } + `); + }); +}); diff --git a/console/src/features/MetadataAPI/hooks/useMetadataMigration.ts b/console/src/features/MetadataAPI/hooks/useMetadataMigration.ts new file mode 100644 index 00000000000..ac96bbf309b --- /dev/null +++ b/console/src/features/MetadataAPI/hooks/useMetadataMigration.ts @@ -0,0 +1,86 @@ +import returnMigrateUrl from '@/components/Services/Data/Common/getMigrateUrl'; +import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '@/constants'; +import { useMigrationMode } from '@/hooks'; +import { Api } from '@/hooks/apiUtils'; +import { RunSQLResponse } from '@/hooks/types'; +import { useConsoleConfig } from '@/hooks/useEnvVars'; +import { useAppSelector } from '@/store'; +import { useMutation, UseMutationOptions, useQueryClient } from 'react-query'; +import sanitize from 'sanitize-filename'; + +const maxAllowedLength = 255; +const unixEpochLength = 14; +export const maxAllowedMigrationLength = maxAllowedLength - unixEpochLength; + +export type allowedMetadataTypes = 'pg_create_remote_relationship'; + +export type TMigration = { + source: string; + query: { type: allowedMetadataTypes; args: Record }; + migrationName: string; +}; + +export function useMetadataMigration( + mutationOptions?: Omit< + UseMutationOptions, Error, TMigration>, + 'mutationFn' + > +) { + const { mode } = useConsoleConfig(); + const headers = useAppSelector(state => state.tables.dataHeaders); + + const { data: migrationMode } = useMigrationMode(); + const queryClient = useQueryClient(); + return useMutation( + async props => { + try { + const { source, query, migrationName } = props; + + if (!source) throw Error('source cannot be empty'); + + const migrateUrl = returnMigrateUrl(migrationMode ?? false, [query]); + + let body = {}; + + if (mode === SERVER_CONSOLE_MODE) { + body = query; + } else { + body = { + name: sanitize( + migrationName.substring(0, maxAllowedMigrationLength) + ), + up: [query], + down: [], + datasource: source, + skip_execution: false, + }; + } + + const result = await Api.post({ + url: migrateUrl, + headers, + body, + }); + + return result; + } catch (err) { + throw err; + } + }, + { + ...mutationOptions, + onSuccess: (data, variables, ctx) => { + if (mode === CLI_CONSOLE_MODE) { + queryClient.refetchQueries('migrationMode', { active: true }); + } + + queryClient.refetchQueries(['metadata'], { active: true }); + + const { onSuccess } = mutationOptions ?? {}; + if (onSuccess) { + onSuccess(data, variables, ctx); + } + }, + } + ); +} diff --git a/console/src/features/MetadataAPI/index.ts b/console/src/features/MetadataAPI/index.ts index 67951574840..96326ce2bb1 100644 --- a/console/src/features/MetadataAPI/index.ts +++ b/console/src/features/MetadataAPI/index.ts @@ -9,5 +9,6 @@ export { useMetadataVersion } from './hooks/useMetadataVersion'; export { useMetadataTableComputedFields } from './hooks/useMetadataTableComputedFields'; export { useMetadataTablePermissions } from './hooks/useMetadataTablePermissions'; export { useMetadata } from './hooks/useMetadata'; +export { useMetadataMigration } from './hooks/useMetadataMigration'; export * from './types'; diff --git a/console/src/hooks/__tests__/common/decorator.tsx b/console/src/hooks/__tests__/common/decorator.tsx index 1e18ee45aac..53cb0009cfa 100644 --- a/console/src/hooks/__tests__/common/decorator.tsx +++ b/console/src/hooks/__tests__/common/decorator.tsx @@ -2,6 +2,7 @@ import reducer from '@/reducer'; import { RootState } from '@/store'; import { DeepPartial } from '@/types'; import { configureStore } from '@reduxjs/toolkit'; +import { render } from '@testing-library/react'; import merge from 'lodash.merge'; import React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; @@ -30,3 +31,14 @@ const HookTestProvider: React.FC<{ export const wrapper: React.FC = ({ children }) => ( {children} ); + +export function renderWithClient(ui: React.ReactElement) { + const { rerender, ...result } = render( + {ui} + ); + return { + ...result, + rerender: (rerenderUi: React.ReactElement) => + rerender({rerenderUi}), + }; +} diff --git a/console/src/hooks/useConfig.ts b/console/src/hooks/useConfig.ts index e51fdf167e6..05dd8fcc997 100644 --- a/console/src/hooks/useConfig.ts +++ b/console/src/hooks/useConfig.ts @@ -1,7 +1,9 @@ +import { SERVER_CONSOLE_MODE } from '@/constants'; import Endpoints from '@/Endpoints'; import { useQuery, UseQueryOptions } from 'react-query'; import { useAppSelector } from '../store'; import { Api } from './apiUtils'; +import { useConsoleConfig } from './useEnvVars'; export interface Config { migrationMode: boolean; @@ -13,9 +15,13 @@ export function useMigrationMode( ) { const headers = useAppSelector(s => s.tables.dataHeaders); const migrationUrl = Endpoints.hasuractlMigrateSettings; + const { mode } = useConsoleConfig(); return useQuery({ queryKey: 'migrationMode', queryFn() { + if (mode === SERVER_CONSOLE_MODE) + return Promise.resolve({ migration_mode: false }); + return Api.get<{ migration_mode: boolean }>({ url: migrationUrl, headers,