console: Add minimal version of metadata migration hook

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3629
Co-authored-by: Varun Choudhary <68095256+Varun-Choudhary@users.noreply.github.com>
GitOrigin-RevId: 715aab88b7d6a1b1526899f17b167394c60e9574
This commit is contained in:
Vijay Prasanna 2022-02-17 21:48:15 +05:30 committed by hasura-bot
parent 69fd7449be
commit bd4b643cba
6 changed files with 411 additions and 0 deletions

View File

@ -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<string, any>).type === 'export_metadata') {
return res(ctx.status(200), ctx.json(metadata));
}
if (
(req.body as Record<string, any>).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 (
<>
<button
onClick={() => {
mutation.mutate({
source: 'default',
query: { type: 'pg_create_remote_relationship', args: {} },
migrationName: '',
});
}}
>
mutate
</button>
<h1>{query.isSuccess ? JSON.stringify(query.data) : 'NA'}</h1>
</>
);
}
renderWithClient(<Page />);
await testLibWaitFor(() => {
expect(onSuccessMock).toHaveBeenCalledTimes(1);
});
await testLibWaitFor(() => {
expect(screen.getByRole('heading')).toMatchInlineSnapshot(`
<h1>
1
</h1>
`);
});
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(`
<h1>
2
</h1>
`);
});
});
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",
}
`);
});
});

View File

@ -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<string, any>).type === 'export_metadata') {
return res(ctx.status(200), ctx.json(metadata));
}
if (
(req.body as Record<string, any>).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 (
<>
<button
onClick={() => {
mutation.mutate({
source: 'default',
query: { type: 'pg_create_remote_relationship', args: {} },
migrationName: '',
});
}}
>
mutate
</button>
<h1>{query.isSuccess ? JSON.stringify(query.data) : 'NA'}</h1>
</>
);
}
renderWithClient(<Page />);
await testLibWaitFor(() => {
expect(onSuccessMock).toHaveBeenCalledTimes(1);
});
await testLibWaitFor(() => {
expect(screen.getByRole('heading')).toMatchInlineSnapshot(`
<h1>
1
</h1>
`);
});
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(`
<h1>
2
</h1>
`);
});
});
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",
}
`);
});
});

View File

@ -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<string, any> };
migrationName: string;
};
export function useMetadataMigration(
mutationOptions?: Omit<
UseMutationOptions<Record<string, any>, 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<RunSQLResponse>({
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);
}
},
}
);
}

View File

@ -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';

View File

@ -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 }) => (
<HookTestProvider>{children}</HookTestProvider>
);
export function renderWithClient(ui: React.ReactElement) {
const { rerender, ...result } = render(
<HookTestProvider>{ui}</HookTestProvider>
);
return {
...result,
rerender: (rerenderUi: React.ReactElement) =>
rerender(<HookTestProvider>{rerenderUi}</HookTestProvider>),
};
}

View File

@ -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,