mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
69fd7449be
commit
bd4b643cba
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
@ -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';
|
||||
|
@ -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>),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user