mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 14:28:08 +03:00
feat (console): add connect db UI that works via the data abstraction layer
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4763 GitOrigin-RevId: 6fae278b86d7cc56d4f7980ddeb5bea7afd55f1a
This commit is contained in:
parent
c2d0d272ee
commit
3d2ad8fdbb
232
console/src/features/ConnectDB/Configuration.tsx
Normal file
232
console/src/features/ConnectDB/Configuration.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import React, { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
Ref,
|
||||
Property,
|
||||
DataSource,
|
||||
SupportedDrivers,
|
||||
Feature,
|
||||
OneOf,
|
||||
} from '@/features/DataSource';
|
||||
import get from 'lodash.get';
|
||||
import axios from 'axios';
|
||||
import { Switch } from '@/new-components/Switch';
|
||||
import { InputField, Select } from '@/new-components/Form';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
getPropertyByRef,
|
||||
isOneOf,
|
||||
isProperty,
|
||||
isRef,
|
||||
} from '../DataSource/utils';
|
||||
|
||||
const useConfigSchema = (driver: SupportedDrivers) => {
|
||||
const fetch = axios.create();
|
||||
return useQuery({
|
||||
queryKey: [driver, 'configSchema'],
|
||||
queryFn: async () => {
|
||||
return DataSource(fetch).connectDB.getConfigSchema(driver);
|
||||
},
|
||||
enabled: !!driver,
|
||||
});
|
||||
};
|
||||
|
||||
const RadioGroup = ({
|
||||
property,
|
||||
otherSchemas,
|
||||
name,
|
||||
}: {
|
||||
name: string;
|
||||
property: OneOf;
|
||||
otherSchemas: Record<string, Property>;
|
||||
}) => {
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
// const currentOption: number = watch(`extra_props.${name}.radio_group_option`);
|
||||
|
||||
const options = property.oneOf.map((_property, i) => {
|
||||
if (isRef(_property))
|
||||
return (
|
||||
getPropertyByRef(_property, otherSchemas).description ?? `Option-${i}`
|
||||
);
|
||||
|
||||
return _property.description ?? `Option-${i}`;
|
||||
});
|
||||
|
||||
const [currentOption, setCurrentOption] = useState<undefined | number>();
|
||||
|
||||
const currentProperty =
|
||||
currentOption !== undefined ? property.oneOf[currentOption] : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-8 rounded border bg-white border-gray-300 p-4">
|
||||
<div className="text-gray-600 font-semibold py-3">
|
||||
{property.description}
|
||||
</div>
|
||||
{options.map((option, i) => {
|
||||
return (
|
||||
<span key={i} className="mr-md">
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => {
|
||||
setCurrentOption(i);
|
||||
setValue(name, undefined);
|
||||
}}
|
||||
name={`${name}-select`}
|
||||
className="radio radio-primary"
|
||||
value={i}
|
||||
/>
|
||||
<label className="ml-sm">{option}</label>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
<div>
|
||||
{currentProperty ? (
|
||||
<div className="grid card bg-base-300 rounded-box my-4">
|
||||
<Field
|
||||
property={currentProperty}
|
||||
otherSchemas={otherSchemas}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="my-3 italic">Select an option</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Field = ({
|
||||
name,
|
||||
property,
|
||||
otherSchemas,
|
||||
}: {
|
||||
name: string;
|
||||
property: Ref | Property | { oneOf: (Property | Ref)[] };
|
||||
otherSchemas: Record<string, Property>;
|
||||
}) => {
|
||||
if (isProperty(property)) {
|
||||
if (property.type === 'string') {
|
||||
if (property.enum) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Select
|
||||
options={property.enum.map(option => ({
|
||||
value: option,
|
||||
label: option,
|
||||
}))}
|
||||
name={name}
|
||||
label={property.description ?? name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputField
|
||||
type="text"
|
||||
name={name}
|
||||
label={property.description ?? name}
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (property.type === 'number')
|
||||
return (
|
||||
<InputField
|
||||
type="number"
|
||||
name={name}
|
||||
label={property.description ?? name}
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
|
||||
if (property.type === 'boolean')
|
||||
return (
|
||||
<div className="max-w-xl flex justify-between my-4">
|
||||
<label className="font-semibold text-gray-600">
|
||||
{property.description ?? name}
|
||||
</label>
|
||||
<Controller
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch checked={value} onCheckedChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (property.type === 'object')
|
||||
return (
|
||||
<div>
|
||||
{Object.entries(property.properties).map(([key, _property], i) => {
|
||||
return (
|
||||
<div key={`${name}.${key}.${i}`}>
|
||||
<Field
|
||||
property={_property}
|
||||
otherSchemas={otherSchemas}
|
||||
name={`${name}.${key}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isRef(property)) {
|
||||
const ref = property.$ref;
|
||||
const _property = get(otherSchemas, ref.split('/').slice(2).join('.'));
|
||||
|
||||
return (
|
||||
<Field property={_property} otherSchemas={otherSchemas} name={name} />
|
||||
);
|
||||
}
|
||||
|
||||
if (isOneOf(property)) {
|
||||
return (
|
||||
<RadioGroup property={property} otherSchemas={otherSchemas} name={name} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Configuration = ({ name }: { name: string }) => {
|
||||
const { watch } = useFormContext();
|
||||
const driver: SupportedDrivers = watch('driver');
|
||||
const { data: schema, isLoading, isError } = useConfigSchema(driver);
|
||||
|
||||
if (schema === Feature.NotImplemented)
|
||||
return <>Feature is not available for this {driver}</>;
|
||||
|
||||
if (!driver) return <>Driver not selected</>;
|
||||
|
||||
if (isLoading) return <>Loading configuration info...</>;
|
||||
|
||||
if (isError) return <>welp something broke ! </>;
|
||||
|
||||
if (!schema) return <>Unable to find any schema for the {driver}</>;
|
||||
|
||||
if (schema.configSchema.type !== 'object') return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.entries(schema.configSchema.properties).map(([key, value], i) => {
|
||||
return (
|
||||
<Field
|
||||
property={value}
|
||||
otherSchemas={schema.otherSchemas}
|
||||
key={i}
|
||||
name={`${name}.${key}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
18
console/src/features/ConnectDB/Connect.stories.tsx
Normal file
18
console/src/features/ConnectDB/Connect.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
// Button.stories.ts|tsx
|
||||
|
||||
import React from 'react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { Connect } from './Connect';
|
||||
|
||||
export default {
|
||||
/* 👇 The title prop is optional.
|
||||
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
||||
* to learn how to generate automatic titles
|
||||
*/
|
||||
title: 'Data/Connect',
|
||||
component: Connect,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} as ComponentMeta<typeof Connect>;
|
||||
|
||||
export const Primary: ComponentStory<typeof Connect> = () => <Connect />;
|
60
console/src/features/ConnectDB/Connect.tsx
Normal file
60
console/src/features/ConnectDB/Connect.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import axios from 'axios';
|
||||
import { DataSource } from '@/features/DataSource';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { Form } from '@/new-components/Form';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Driver } from './Driver';
|
||||
import { Name } from './Name';
|
||||
import { Configuration } from './Configuration';
|
||||
import { useMetadataMigration } from '../MetadataAPI';
|
||||
|
||||
const usePossibleFormSchemas = () => {
|
||||
const fetch = axios.create();
|
||||
return useQuery({
|
||||
queryKey: ['validation-schemas'],
|
||||
queryFn: async () => {
|
||||
return DataSource(fetch).connectDB.getFormSchema();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const Connect = () => {
|
||||
const metadataMutation = useMetadataMigration();
|
||||
|
||||
const { data: schemas } = usePossibleFormSchemas();
|
||||
if (!schemas) return <>unable to retrieve any validation schema</>;
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={schemas}
|
||||
onSubmit={values => {
|
||||
console.log('form data', values);
|
||||
metadataMutation.mutate({
|
||||
query: {
|
||||
type: 'pg_add_source',
|
||||
args: values,
|
||||
},
|
||||
});
|
||||
}}
|
||||
options={{
|
||||
defaultValues: {
|
||||
name: '',
|
||||
driver: 'postgres',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{options => {
|
||||
console.log(options.formState.errors);
|
||||
return (
|
||||
<div className="w-1/2">
|
||||
<Driver name="driver" />
|
||||
<Name name="name" />
|
||||
<Configuration name="configuration" />
|
||||
<Button type="submit">Connect</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
35
console/src/features/ConnectDB/Driver.tsx
Normal file
35
console/src/features/ConnectDB/Driver.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { DataSource } from '@/features/DataSource';
|
||||
import React from 'react';
|
||||
import axios from 'axios';
|
||||
import { Select } from '@/new-components/Form';
|
||||
|
||||
const useAvailableDrivers = () => {
|
||||
const fetch = axios.create();
|
||||
return useQuery({
|
||||
queryKey: ['getDrivers'],
|
||||
queryFn: async () => {
|
||||
return DataSource(fetch).driver.getSupportedDrivers();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const Driver = ({ name }: { name: string }) => {
|
||||
const { data: drivers, isLoading, isError } = useAvailableDrivers();
|
||||
|
||||
if (isLoading) return <>Fetching driver info...</>;
|
||||
|
||||
if (isError) return <>Something went wrong while fetching drivers</>;
|
||||
|
||||
if (!drivers) return <>No available drivers were found</>;
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<Select
|
||||
options={drivers.map(driver => ({ value: driver, label: driver }))}
|
||||
name={name}
|
||||
label="Driver"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
14
console/src/features/ConnectDB/Name.tsx
Normal file
14
console/src/features/ConnectDB/Name.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { InputField } from '@/new-components/Form';
|
||||
import React from 'react';
|
||||
|
||||
export const Name = ({ name }: { name: string }) => {
|
||||
return (
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="Enter a display name"
|
||||
name={name}
|
||||
label="Display Name"
|
||||
className="py-4"
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,99 +0,0 @@
|
||||
const name = {
|
||||
label: 'Name',
|
||||
key: 'key',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
const service_account = {
|
||||
key: 'service_account',
|
||||
label: 'Connection Details',
|
||||
type: 'radio-group-with-inputs',
|
||||
options: [
|
||||
{
|
||||
key: 'env_var',
|
||||
label: 'Enviroment Variable',
|
||||
fields: [
|
||||
{
|
||||
label: 'Env var',
|
||||
key: 'env_var',
|
||||
type: 'text',
|
||||
placeholder: 'SERVICE_ACCOUNT_KEY_FROM_ENV',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'connection_parameters',
|
||||
label: 'Connection Parameters',
|
||||
fields: [
|
||||
{
|
||||
label: 'Service Account Key',
|
||||
key: 'service_account_key',
|
||||
type: 'json',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
key: 'configuration',
|
||||
label: 'Connect Database Via',
|
||||
fields: [
|
||||
service_account,
|
||||
{
|
||||
label: 'Project Id',
|
||||
key: 'project_id',
|
||||
type: 'text',
|
||||
placeholder: 'project_id',
|
||||
},
|
||||
{
|
||||
label: 'Datasets',
|
||||
key: 'datasets',
|
||||
type: 'text',
|
||||
placeholder: 'dataset_1, dataset_2',
|
||||
},
|
||||
{
|
||||
label: 'Global Select Limit',
|
||||
key: 'global_select_limit',
|
||||
type: 'number',
|
||||
placeholder: 1000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const customization = {
|
||||
key: 'customization',
|
||||
label: 'GraphQL Customization',
|
||||
fields: [
|
||||
{
|
||||
label: 'Namespace',
|
||||
key: 'root_fields.namespace',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'root_fields.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'root_fields.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'type_names.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'type_names.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const getUISchema = async () => {
|
||||
return [name, configuration, customization];
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const service_account = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('env_var'),
|
||||
details: z.object({
|
||||
value: z.string().min(1, 'Environment variable name cannot be empty!'),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('json'),
|
||||
details: z.object({
|
||||
value: z.string().min(1, 'Service Account Key is required!'),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const schema = z.object({
|
||||
driver: z.literal('bigquery'),
|
||||
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
configuration: z.object({
|
||||
service_account,
|
||||
project_id: z.string().min(1, 'Project ID cannot be empty!'),
|
||||
datasets: z.string().min(1, 'Datasets cannot be empty'),
|
||||
}),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
customization: z
|
||||
.object({
|
||||
root_fields: z.object({
|
||||
namespace: z.string().optional(),
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
type_names: z.object({
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const getValidationSchema = async () => {
|
||||
return schema;
|
||||
};
|
@ -1,10 +1,9 @@
|
||||
import { Database } from '..';
|
||||
import { getUISchema } from './connectDB/getUISchema';
|
||||
import { getValidationSchema } from './connectDB/getValidationSchema';
|
||||
import { Database, Feature } from '..';
|
||||
|
||||
export const bigquery: Database = {
|
||||
connectDB: {
|
||||
getUISchema,
|
||||
getValidationSchema,
|
||||
getConfigSchema: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,191 +0,0 @@
|
||||
const name = {
|
||||
label: 'Name',
|
||||
key: 'key',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
const pool_settings = {
|
||||
key: 'pool_settings',
|
||||
label: 'Connection Settings',
|
||||
fields: [
|
||||
{
|
||||
key: 'max_connections',
|
||||
label: 'Max Connections',
|
||||
type: 'number',
|
||||
tooltip: 'Maximum number of connections to be kept in the pool',
|
||||
placeholder: 50,
|
||||
},
|
||||
{
|
||||
key: 'idle_timeout',
|
||||
label: 'Idle Timeout',
|
||||
type: 'number',
|
||||
tooltip: 'The idle timeout (in seconds) per connection',
|
||||
placeholder: 180,
|
||||
},
|
||||
{
|
||||
key: 'retries',
|
||||
label: 'Retries',
|
||||
type: 'number',
|
||||
tooltip: 'Number of retries to perform',
|
||||
placeholder: 1,
|
||||
},
|
||||
{
|
||||
key: 'pool_timeout',
|
||||
label: 'Pool Timeout',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Maximum time (in seconds) to wait while acquiring a Postgres connection from the pool',
|
||||
placeholder: 360,
|
||||
},
|
||||
{
|
||||
key: 'connection_lifetime',
|
||||
label: 'Connection Lifetime',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Time (in seconds) from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed.',
|
||||
placeholder: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const database_url = {
|
||||
key: 'database_url',
|
||||
label: '',
|
||||
type: 'radio-group-with-inputs',
|
||||
options: [
|
||||
{
|
||||
key: 'env_var',
|
||||
label: 'Enviroment Variable',
|
||||
fields: [
|
||||
{
|
||||
label: 'Env var',
|
||||
key: 'env_var',
|
||||
type: 'text',
|
||||
placeholder: 'HASURA_DB_URL_FROM_ENV',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'database_url',
|
||||
label: 'Database URL',
|
||||
fields: [
|
||||
{
|
||||
label: 'URL',
|
||||
key: 'database_url',
|
||||
type: 'text',
|
||||
placeholder: 'postgresql://username:password@hostname:5432/database',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
{
|
||||
key: 'connection_parameters',
|
||||
label: 'Connection Parameters',
|
||||
fields: [
|
||||
{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'postgres_user',
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
placeholder: 'postgrespassword',
|
||||
},
|
||||
{
|
||||
label: 'Database Name',
|
||||
key: 'database',
|
||||
type: 'text',
|
||||
placeholder: 'postgres',
|
||||
},
|
||||
{
|
||||
label: 'Host',
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
placeholder: 'localhost',
|
||||
},
|
||||
{
|
||||
label: 'Port',
|
||||
key: 'port',
|
||||
type: 'number',
|
||||
placeholder: '5432',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const connection_info = {
|
||||
key: 'connection_info',
|
||||
label: 'Connection Details',
|
||||
fields: [
|
||||
database_url,
|
||||
pool_settings,
|
||||
{
|
||||
key: 'isolation_level',
|
||||
label: 'Isolation Level',
|
||||
type: 'select',
|
||||
tooltip:
|
||||
'The transaction isolation level in which the queries made to the source will be run',
|
||||
options: ['read-commited', 'repeatable-read', 'serializable'],
|
||||
},
|
||||
{
|
||||
key: 'prepared_statements',
|
||||
label: 'Use Prepared Statements',
|
||||
type: 'boolean',
|
||||
tooltip: 'Prepared statements are disabled by default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const read_replicas = {
|
||||
key: 'read_replicas',
|
||||
label: 'Read Replicas',
|
||||
type: 'array',
|
||||
fields: [connection_info],
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
key: 'configuration',
|
||||
label: 'Connect Database Via',
|
||||
fields: [connection_info, read_replicas],
|
||||
};
|
||||
|
||||
const customization = {
|
||||
key: 'customization',
|
||||
label: 'GraphQL Customization',
|
||||
fields: [
|
||||
{
|
||||
label: 'Namespace',
|
||||
key: 'root_fields.namespace',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'root_fields.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'root_fields.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'type_names.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'type_names.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const getUISchema = async () => {
|
||||
return [name, configuration, customization];
|
||||
};
|
@ -1,87 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const connection_types = {
|
||||
env_var: z.object({
|
||||
type: z.literal('env_var'),
|
||||
details: z.object({
|
||||
env_var: z.string().min(1, 'Please provide a environment variable'),
|
||||
}),
|
||||
}),
|
||||
connection_parameters: z.object({
|
||||
type: z.literal('connection_parameters'),
|
||||
details: z.object({
|
||||
host: z.string().min(1, 'Host is a required Field'),
|
||||
port: z
|
||||
.string()
|
||||
.min(1, 'Port is a required Field!')
|
||||
.transform(x => parseInt(x, 10)),
|
||||
username: z.string().min(1, 'Username is a required Field'),
|
||||
password: z.string().min(1, 'Password is a required Field'),
|
||||
db_name: z.string().min(1, 'Database Name is a required Field'),
|
||||
}),
|
||||
}),
|
||||
url: z.object({
|
||||
type: z.literal('value'),
|
||||
details: z.object({
|
||||
database_url: z.string().min(1, 'Database URL is a required Field'),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const database_url = z.discriminatedUnion('type', [
|
||||
connection_types.connection_parameters,
|
||||
connection_types.url,
|
||||
connection_types.env_var,
|
||||
]);
|
||||
|
||||
const connection_info = z.object({
|
||||
database_url,
|
||||
pool_settings: z
|
||||
.object({
|
||||
max_connections: z.string().transform(x => parseInt(x, 10)),
|
||||
idle_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
retries: z.string().transform(x => parseInt(x, 10)),
|
||||
pool_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
connection_lifetime: z.string().transform(x => parseInt(x, 10)),
|
||||
})
|
||||
.optional(),
|
||||
isolation_level: z.string().transform(x => {
|
||||
if (!x) return 'read-commited';
|
||||
return x;
|
||||
}),
|
||||
prepared_statements: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
driver: z.literal('postgres'),
|
||||
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
configuration: z.object({
|
||||
connection_info,
|
||||
read_replicas: z.array(connection_info).optional(),
|
||||
}),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
customization: z
|
||||
.object({
|
||||
root_fields: z.object({
|
||||
namespace: z.string().optional(),
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
type_names: z.object({
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const getValidationSchema = async () => {
|
||||
return schema;
|
||||
};
|
@ -1,10 +1,9 @@
|
||||
import { getValidationSchema } from './connectDB/getValidationSchema';
|
||||
import { getUISchema } from './connectDB/getUISchema';
|
||||
import { Database } from '..';
|
||||
import { Database, Feature } from '..';
|
||||
|
||||
export const citus: Database = {
|
||||
connectDB: {
|
||||
getUISchema,
|
||||
getValidationSchema,
|
||||
getConfigSchema: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -2,10 +2,7 @@ import { Database, Feature } from '../index';
|
||||
|
||||
export const gdc: Database = {
|
||||
connectDB: {
|
||||
getValidationSchema: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
getUISchema: async () => {
|
||||
getConfigSchema: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
},
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { Primitive, ZodDiscriminatedUnionOption } from 'zod';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { z, ZodSchema } from 'zod';
|
||||
import { postgres } from './postgres';
|
||||
import { bigquery } from './bigquery';
|
||||
import { citus } from './citus';
|
||||
import { mssql } from './mssql';
|
||||
import { gdc } from './gdc';
|
||||
|
||||
// This will be modified in the upcoming changes to connectDB GDC feature
|
||||
type UISchema = {
|
||||
key: string;
|
||||
label: string;
|
||||
};
|
||||
import { Property, Ref, OneOf } from './types';
|
||||
import { getZodSchema } from './utils';
|
||||
|
||||
export enum Feature {
|
||||
NotImplemented = 'Not Implemented',
|
||||
@ -35,13 +31,11 @@ export type Database = {
|
||||
/* This section defines how a database is connected to Hasura */
|
||||
connectDB: {
|
||||
/* Expose Methods that are used in the DB connection part */
|
||||
getUISchema: (
|
||||
fetch: AxiosInstance
|
||||
) => Promise<UISchema[] | Feature.NotImplemented>;
|
||||
getValidationSchema: (
|
||||
getConfigSchema: (
|
||||
fetch: AxiosInstance
|
||||
) => Promise<
|
||||
ZodDiscriminatedUnionOption<'driver', Primitive> | Feature.NotImplemented
|
||||
| { configSchema: Property; otherSchemas: Record<string, Property> }
|
||||
| Feature.NotImplemented
|
||||
>;
|
||||
};
|
||||
};
|
||||
@ -61,11 +55,55 @@ export const DataSource = (hasuraFetch: AxiosInstance) => ({
|
||||
},
|
||||
},
|
||||
connectDB: {
|
||||
getUISchema: async (driver: SupportedDrivers) => {
|
||||
return drivers[driver].connectDB.getUISchema(hasuraFetch);
|
||||
getConfigSchema: async (driver: SupportedDrivers) => {
|
||||
return drivers[driver].connectDB.getConfigSchema(hasuraFetch);
|
||||
},
|
||||
getValidationSchema: async (driver: SupportedDrivers) => {
|
||||
return drivers[driver].connectDB.getValidationSchema(hasuraFetch);
|
||||
getFormSchema: async () => {
|
||||
const schemas = await Promise.all(
|
||||
Object.values(drivers).map(database =>
|
||||
database.connectDB.getConfigSchema(hasuraFetch)
|
||||
)
|
||||
);
|
||||
|
||||
let res: ZodSchema | undefined;
|
||||
|
||||
schemas.forEach(schema => {
|
||||
if (schema === Feature.NotImplemented) return;
|
||||
|
||||
if (!res) {
|
||||
res = z.object({
|
||||
driver: z.literal('postgres'),
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
configuration: getZodSchema(
|
||||
schema.configSchema,
|
||||
schema.otherSchemas
|
||||
),
|
||||
});
|
||||
} else {
|
||||
res = res.or(
|
||||
z.object({
|
||||
driver: z.literal('postgres'),
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
configuration: getZodSchema(
|
||||
schema.configSchema,
|
||||
schema.otherSchemas
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { Property, Ref, OneOf };
|
||||
|
@ -1,191 +0,0 @@
|
||||
const name = {
|
||||
label: 'Name',
|
||||
key: 'key',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
const pool_settings = {
|
||||
key: 'pool_settings',
|
||||
label: 'Connection Settings',
|
||||
fields: [
|
||||
{
|
||||
key: 'max_connections',
|
||||
label: 'Max Connections',
|
||||
type: 'number',
|
||||
tooltip: 'Maximum number of connections to be kept in the pool',
|
||||
placeholder: 50,
|
||||
},
|
||||
{
|
||||
key: 'idle_timeout',
|
||||
label: 'Idle Timeout',
|
||||
type: 'number',
|
||||
tooltip: 'The idle timeout (in seconds) per connection',
|
||||
placeholder: 180,
|
||||
},
|
||||
{
|
||||
key: 'retries',
|
||||
label: 'Retries',
|
||||
type: 'number',
|
||||
tooltip: 'Number of retries to perform',
|
||||
placeholder: 1,
|
||||
},
|
||||
{
|
||||
key: 'pool_timeout',
|
||||
label: 'Pool Timeout',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Maximum time (in seconds) to wait while acquiring a Postgres connection from the pool',
|
||||
placeholder: 360,
|
||||
},
|
||||
{
|
||||
key: 'connection_lifetime',
|
||||
label: 'Connection Lifetime',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Time (in seconds) from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed.',
|
||||
placeholder: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const database_url = {
|
||||
key: 'database_url',
|
||||
label: '',
|
||||
type: 'radio-group-with-inputs',
|
||||
options: [
|
||||
{
|
||||
key: 'env_var',
|
||||
label: 'Enviroment Variable',
|
||||
fields: [
|
||||
{
|
||||
label: 'Env var',
|
||||
key: 'env_var',
|
||||
type: 'text',
|
||||
placeholder: 'HASURA_DB_URL_FROM_ENV',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'database_url',
|
||||
label: 'Database URL',
|
||||
fields: [
|
||||
{
|
||||
label: 'URL',
|
||||
key: 'database_url',
|
||||
type: 'text',
|
||||
placeholder: 'postgresql://username:password@hostname:5432/database',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
{
|
||||
key: 'connection_parameters',
|
||||
label: 'Connection Parameters',
|
||||
fields: [
|
||||
{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'postgres_user',
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
placeholder: 'postgrespassword',
|
||||
},
|
||||
{
|
||||
label: 'Database Name',
|
||||
key: 'database',
|
||||
type: 'text',
|
||||
placeholder: 'postgres',
|
||||
},
|
||||
{
|
||||
label: 'Host',
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
placeholder: 'localhost',
|
||||
},
|
||||
{
|
||||
label: 'Port',
|
||||
key: 'port',
|
||||
type: 'number',
|
||||
placeholder: '5432',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const connection_info = {
|
||||
key: 'connection_info',
|
||||
label: 'Connection Details',
|
||||
fields: [
|
||||
database_url,
|
||||
pool_settings,
|
||||
{
|
||||
key: 'isolation_level',
|
||||
label: 'Isolation Level',
|
||||
type: 'select',
|
||||
tooltip:
|
||||
'The transaction isolation level in which the queries made to the source will be run',
|
||||
options: ['read-commited', 'repeatable-read', 'serializable'],
|
||||
},
|
||||
{
|
||||
key: 'prepared_statements',
|
||||
label: 'Use Prepared Statements',
|
||||
type: 'boolean',
|
||||
tooltip: 'Prepared statements are disabled by default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const read_replicas = {
|
||||
key: 'read_replicas',
|
||||
label: 'Read Replicas',
|
||||
type: 'array',
|
||||
fields: [connection_info],
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
key: 'configuration',
|
||||
label: 'Connect Database Via',
|
||||
fields: [connection_info, read_replicas],
|
||||
};
|
||||
|
||||
const customization = {
|
||||
key: 'customization',
|
||||
label: 'GraphQL Customization',
|
||||
fields: [
|
||||
{
|
||||
label: 'Namespace',
|
||||
key: 'root_fields.namespace',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'root_fields.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'root_fields.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'type_names.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'type_names.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const getUISchema = async () => {
|
||||
return [name, configuration, customization];
|
||||
};
|
@ -1,87 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const connection_types = {
|
||||
env_var: z.object({
|
||||
type: z.literal('env_var'),
|
||||
details: z.object({
|
||||
env_var: z.string().min(1, 'Please provide a environment variable'),
|
||||
}),
|
||||
}),
|
||||
connection_parameters: z.object({
|
||||
type: z.literal('connection_parameters'),
|
||||
details: z.object({
|
||||
host: z.string().min(1, 'Host is a required Field'),
|
||||
port: z
|
||||
.string()
|
||||
.min(1, 'Port is a required Field!')
|
||||
.transform(x => parseInt(x, 10)),
|
||||
username: z.string().min(1, 'Username is a required Field'),
|
||||
password: z.string().min(1, 'Password is a required Field'),
|
||||
db_name: z.string().min(1, 'Database Name is a required Field'),
|
||||
}),
|
||||
}),
|
||||
url: z.object({
|
||||
type: z.literal('value'),
|
||||
details: z.object({
|
||||
database_url: z.string().min(1, 'Database URL is a required Field'),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const database_url = z.discriminatedUnion('type', [
|
||||
connection_types.connection_parameters,
|
||||
connection_types.url,
|
||||
connection_types.env_var,
|
||||
]);
|
||||
|
||||
const connection_info = z.object({
|
||||
database_url,
|
||||
pool_settings: z
|
||||
.object({
|
||||
max_connections: z.string().transform(x => parseInt(x, 10)),
|
||||
idle_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
retries: z.string().transform(x => parseInt(x, 10)),
|
||||
pool_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
connection_lifetime: z.string().transform(x => parseInt(x, 10)),
|
||||
})
|
||||
.optional(),
|
||||
isolation_level: z.string().transform(x => {
|
||||
if (!x) return 'read-commited';
|
||||
return x;
|
||||
}),
|
||||
prepared_statements: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
driver: z.literal('postgres'),
|
||||
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
configuration: z.object({
|
||||
connection_info,
|
||||
read_replicas: z.array(connection_info).optional(),
|
||||
}),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
customization: z
|
||||
.object({
|
||||
root_fields: z.object({
|
||||
namespace: z.string().optional(),
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
type_names: z.object({
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const getValidationSchema = async () => {
|
||||
return schema;
|
||||
};
|
@ -1,10 +1,9 @@
|
||||
import { getValidationSchema } from './connectDB/getValidationSchema';
|
||||
import { getUISchema } from './connectDB/getUISchema';
|
||||
import { Database } from '..';
|
||||
import { Database, Feature } from '..';
|
||||
|
||||
export const mssql: Database = {
|
||||
connectDB: {
|
||||
getUISchema,
|
||||
getValidationSchema,
|
||||
getConfigSchema: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,128 @@
|
||||
import { Property } from '../../types';
|
||||
|
||||
export const getConfigSchema = async () => {
|
||||
const {
|
||||
configSchema,
|
||||
otherSchemas,
|
||||
}: { configSchema: Property; otherSchemas: Record<string, Property> } = {
|
||||
configSchema: {
|
||||
type: 'object',
|
||||
nullable: false,
|
||||
description: 'Configuration',
|
||||
properties: {
|
||||
connection_info: { $ref: '#/otherSchemas/ConnectionInfo' },
|
||||
},
|
||||
},
|
||||
otherSchemas: {
|
||||
PoolSettings: {
|
||||
type: 'object',
|
||||
description: 'Pool Settings',
|
||||
nullable: true,
|
||||
properties: {
|
||||
max_connections: {
|
||||
description: 'Max connections',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
idle_timeout: {
|
||||
description: 'Idle Timeout',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
retries: {
|
||||
description: 'Retries',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
pool_timeout: {
|
||||
description: 'Pool Timeout',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
connection_lifetime: {
|
||||
description: 'Connection Lifetime',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
DatabaseURL: {
|
||||
description: 'Database URL',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
EnvironmentVariable: {
|
||||
description: 'Enviroment Variable',
|
||||
type: 'object',
|
||||
nullable: false,
|
||||
properties: {
|
||||
from_env: {
|
||||
description: 'Enviroment Variable',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ConnectionInfo: {
|
||||
description: 'Connection details',
|
||||
type: 'object',
|
||||
nullable: false,
|
||||
properties: {
|
||||
database_url: {
|
||||
description: 'Connect DB via',
|
||||
oneOf: [
|
||||
{ $ref: '#/otherSchemas/DatabaseURL' },
|
||||
{
|
||||
type: 'object',
|
||||
nullable: false,
|
||||
description: 'Connection Parameters',
|
||||
properties: {
|
||||
username: {
|
||||
description: 'Username',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
password: {
|
||||
description: 'Password',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
host: {
|
||||
description: 'Host',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
database: {
|
||||
description: 'Database',
|
||||
type: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
port: {
|
||||
description: 'Port',
|
||||
type: 'number',
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $ref: '#/otherSchemas/EnvironmentVariable' },
|
||||
],
|
||||
},
|
||||
pool_settings: { $ref: '#/otherSchemas/PoolSettings' },
|
||||
use_prepared_statements: {
|
||||
description: 'Use Prepared Statements',
|
||||
type: 'boolean',
|
||||
nullable: true,
|
||||
},
|
||||
isolation_level: {
|
||||
description: 'Isolation Level',
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
enum: ['read-committed', 'repeatable-read', 'serializable'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { configSchema, otherSchemas };
|
||||
};
|
@ -1,191 +0,0 @@
|
||||
const name = {
|
||||
label: 'Name',
|
||||
key: 'key',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
const pool_settings = {
|
||||
key: 'pool_settings',
|
||||
label: 'Connection Settings',
|
||||
fields: [
|
||||
{
|
||||
key: 'max_connections',
|
||||
label: 'Max Connections',
|
||||
type: 'number',
|
||||
tooltip: 'Maximum number of connections to be kept in the pool',
|
||||
placeholder: 50,
|
||||
},
|
||||
{
|
||||
key: 'idle_timeout',
|
||||
label: 'Idle Timeout',
|
||||
type: 'number',
|
||||
tooltip: 'The idle timeout (in seconds) per connection',
|
||||
placeholder: 180,
|
||||
},
|
||||
{
|
||||
key: 'retries',
|
||||
label: 'Retries',
|
||||
type: 'number',
|
||||
tooltip: 'Number of retries to perform',
|
||||
placeholder: 1,
|
||||
},
|
||||
{
|
||||
key: 'pool_timeout',
|
||||
label: 'Pool Timeout',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Maximum time (in seconds) to wait while acquiring a Postgres connection from the pool',
|
||||
placeholder: 360,
|
||||
},
|
||||
{
|
||||
key: 'connection_lifetime',
|
||||
label: 'Connection Lifetime',
|
||||
type: 'number',
|
||||
tooltip:
|
||||
'Time (in seconds) from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed.',
|
||||
placeholder: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const database_url = {
|
||||
key: 'database_url',
|
||||
label: '',
|
||||
type: 'radio-group-with-inputs',
|
||||
options: [
|
||||
{
|
||||
key: 'env_var',
|
||||
label: 'Enviroment Variable',
|
||||
fields: [
|
||||
{
|
||||
label: 'Env var',
|
||||
key: 'env_var',
|
||||
type: 'text',
|
||||
placeholder: 'HASURA_DB_URL_FROM_ENV',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'database_url',
|
||||
label: 'Database URL',
|
||||
fields: [
|
||||
{
|
||||
label: 'URL',
|
||||
key: 'database_url',
|
||||
type: 'text',
|
||||
placeholder: 'postgresql://username:password@hostname:5432/database',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
{
|
||||
key: 'connection_parameters',
|
||||
label: 'Connection Parameters',
|
||||
fields: [
|
||||
{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'postgres_user',
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
placeholder: 'postgrespassword',
|
||||
},
|
||||
{
|
||||
label: 'Database Name',
|
||||
key: 'database',
|
||||
type: 'text',
|
||||
placeholder: 'postgres',
|
||||
},
|
||||
{
|
||||
label: 'Host',
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
placeholder: 'localhost',
|
||||
},
|
||||
{
|
||||
label: 'Port',
|
||||
key: 'port',
|
||||
type: 'number',
|
||||
placeholder: '5432',
|
||||
},
|
||||
],
|
||||
expand_keys: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const connection_info = {
|
||||
key: 'connection_info',
|
||||
label: 'Connection Details',
|
||||
fields: [
|
||||
database_url,
|
||||
pool_settings,
|
||||
{
|
||||
key: 'isolation_level',
|
||||
label: 'Isolation Level',
|
||||
type: 'select',
|
||||
tooltip:
|
||||
'The transaction isolation level in which the queries made to the source will be run',
|
||||
options: ['read-commited', 'repeatable-read', 'serializable'],
|
||||
},
|
||||
{
|
||||
key: 'prepared_statements',
|
||||
label: 'Use Prepared Statements',
|
||||
type: 'boolean',
|
||||
tooltip: 'Prepared statements are disabled by default',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const read_replicas = {
|
||||
key: 'read_replicas',
|
||||
label: 'Read Replicas',
|
||||
type: 'array',
|
||||
fields: [connection_info],
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
key: 'configuration',
|
||||
label: 'Connect Database Via',
|
||||
fields: [connection_info, read_replicas],
|
||||
};
|
||||
|
||||
const customization = {
|
||||
key: 'customization',
|
||||
label: 'GraphQL Customization',
|
||||
fields: [
|
||||
{
|
||||
label: 'Namespace',
|
||||
key: 'root_fields.namespace',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'root_fields.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'root_fields.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Prefix',
|
||||
key: 'type_names.prefix',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Suffix',
|
||||
key: 'type_names.suffix',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const getUISchema = async () => {
|
||||
return [name, configuration, customization];
|
||||
};
|
@ -1,87 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const connection_types = {
|
||||
env_var: z.object({
|
||||
type: z.literal('env_var'),
|
||||
details: z.object({
|
||||
env_var: z.string().min(1, 'Please provide a environment variable'),
|
||||
}),
|
||||
}),
|
||||
connection_parameters: z.object({
|
||||
type: z.literal('connection_parameters'),
|
||||
details: z.object({
|
||||
host: z.string().min(1, 'Host is a required Field'),
|
||||
port: z
|
||||
.string()
|
||||
.min(1, 'Port is a required Field!')
|
||||
.transform(x => parseInt(x, 10)),
|
||||
username: z.string().min(1, 'Username is a required Field'),
|
||||
password: z.string().min(1, 'Password is a required Field'),
|
||||
db_name: z.string().min(1, 'Database Name is a required Field'),
|
||||
}),
|
||||
}),
|
||||
url: z.object({
|
||||
type: z.literal('value'),
|
||||
details: z.object({
|
||||
database_url: z.string().min(1, 'Database URL is a required Field'),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const database_url = z.discriminatedUnion('type', [
|
||||
connection_types.connection_parameters,
|
||||
connection_types.url,
|
||||
connection_types.env_var,
|
||||
]);
|
||||
|
||||
const connection_info = z.object({
|
||||
database_url,
|
||||
pool_settings: z
|
||||
.object({
|
||||
max_connections: z.string().transform(x => parseInt(x, 10)),
|
||||
idle_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
retries: z.string().transform(x => parseInt(x, 10)),
|
||||
pool_timeout: z.string().transform(x => parseInt(x, 10)),
|
||||
connection_lifetime: z.string().transform(x => parseInt(x, 10)),
|
||||
})
|
||||
.optional(),
|
||||
isolation_level: z.string().transform(x => {
|
||||
if (!x) return 'read-commited';
|
||||
return x;
|
||||
}),
|
||||
prepared_statements: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
driver: z.literal('postgres'),
|
||||
|
||||
name: z.string().min(1, 'Name is a required field!'),
|
||||
configuration: z.object({
|
||||
connection_info,
|
||||
read_replicas: z.array(connection_info).optional(),
|
||||
}),
|
||||
replace_configuration: z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean()),
|
||||
customization: z
|
||||
.object({
|
||||
root_fields: z.object({
|
||||
namespace: z.string().optional(),
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
type_names: z.object({
|
||||
prefix: z.string().optional(),
|
||||
suffix: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const getValidationSchema = async () => {
|
||||
return schema;
|
||||
};
|
@ -1,10 +1,9 @@
|
||||
import { getValidationSchema } from './connectDB/getValidationSchema';
|
||||
import { getUISchema } from './connectDB/getUISchema';
|
||||
import { getConfigSchema } from './connectDB/getConfigSchema';
|
||||
|
||||
import { Database } from '..';
|
||||
|
||||
export const postgres: Database = {
|
||||
connectDB: {
|
||||
getUISchema,
|
||||
getValidationSchema,
|
||||
getConfigSchema,
|
||||
},
|
||||
};
|
||||
|
23
console/src/features/DataSource/types.ts
Normal file
23
console/src/features/DataSource/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export type Ref = { $ref: string };
|
||||
|
||||
export type OneOf = { oneOf: (Property | Ref)[]; description?: string };
|
||||
|
||||
export type Property = {
|
||||
description?: string;
|
||||
nullable: boolean;
|
||||
} & (
|
||||
| {
|
||||
type: 'object';
|
||||
properties: Record<string, Ref | Property | OneOf>;
|
||||
}
|
||||
| {
|
||||
type: 'string';
|
||||
enum?: string[];
|
||||
}
|
||||
| {
|
||||
type: 'number';
|
||||
}
|
||||
| {
|
||||
type: 'boolean';
|
||||
}
|
||||
);
|
115
console/src/features/DataSource/utils.ts
Normal file
115
console/src/features/DataSource/utils.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import pickBy from 'lodash.pickby';
|
||||
import get from 'lodash.get';
|
||||
import { z, ZodSchema } from 'zod';
|
||||
import { Property, Ref } from './types';
|
||||
|
||||
export const isProperty = (
|
||||
value: Ref | Property | { oneOf: (Property | Ref)[] }
|
||||
): value is Property => {
|
||||
return 'type' in value;
|
||||
};
|
||||
|
||||
export const isRef = (
|
||||
value: Ref | Property | { oneOf: (Property | Ref)[] }
|
||||
): value is Ref => {
|
||||
return Object.keys(value).includes('$ref');
|
||||
};
|
||||
|
||||
export const isOneOf = (
|
||||
value: Ref | Property | { oneOf: (Property | Ref)[] }
|
||||
): value is { oneOf: (Property | Ref)[] } => {
|
||||
return Object.keys(value).includes('oneOf');
|
||||
};
|
||||
|
||||
export const getPropertyByRef = (
|
||||
property: Ref,
|
||||
otherSchemas: Record<string, Property>
|
||||
) => {
|
||||
const ref = property.$ref;
|
||||
const _property = get(otherSchemas, ref.split('/').slice(2).join('.'));
|
||||
return _property;
|
||||
};
|
||||
|
||||
export const getZodSchema = (
|
||||
property: Property,
|
||||
otherSchemas: Record<string, Property>
|
||||
): ZodSchema => {
|
||||
if (property.type === 'string') {
|
||||
const t = z.string();
|
||||
|
||||
if (!property.nullable)
|
||||
return t.min(
|
||||
1,
|
||||
property.description
|
||||
? `${property.description} is Required`
|
||||
: 'Required!'
|
||||
);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
if (property.type === 'boolean') {
|
||||
return z.preprocess(x => {
|
||||
if (!x) return false;
|
||||
return true;
|
||||
}, z.boolean());
|
||||
}
|
||||
|
||||
if (property.type === 'number') {
|
||||
if (!property.nullable)
|
||||
return z
|
||||
.string()
|
||||
.min(1, 'Required')
|
||||
.transform(x => parseInt(x, 10) || '');
|
||||
return z.string().transform(x => parseInt(x, 10) || '');
|
||||
}
|
||||
|
||||
if (property.type === 'object')
|
||||
return z
|
||||
.object({
|
||||
...Object.entries(property.properties).reduce(
|
||||
(acc, [key, _property]) => {
|
||||
if (isRef(_property)) {
|
||||
const _refProperty = getPropertyByRef(_property, otherSchemas);
|
||||
return {
|
||||
...acc,
|
||||
[key]: getZodSchema(_refProperty, otherSchemas),
|
||||
};
|
||||
}
|
||||
// console.log(_property, "here!1");
|
||||
|
||||
if (isOneOf(_property)) {
|
||||
// console.log(_property, "here!");
|
||||
let _unions: ZodSchema = z.void();
|
||||
|
||||
_property.oneOf.forEach((_oneOfProperty, i) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
let temp: Property;
|
||||
if (isRef(_oneOfProperty))
|
||||
temp = getPropertyByRef(_oneOfProperty, otherSchemas);
|
||||
else temp = _oneOfProperty;
|
||||
|
||||
if (i === 0) {
|
||||
_unions = getZodSchema(temp, otherSchemas);
|
||||
} else {
|
||||
_unions = _unions.or(getZodSchema(temp, otherSchemas));
|
||||
}
|
||||
});
|
||||
|
||||
return { ...acc, [key]: _unions };
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: getZodSchema(_property, otherSchemas),
|
||||
};
|
||||
},
|
||||
{}
|
||||
),
|
||||
})
|
||||
.transform((value: any) => pickBy(value, (d: any) => d !== ''));
|
||||
|
||||
console.log('!', property);
|
||||
return z.void();
|
||||
};
|
@ -6,7 +6,7 @@ import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper';
|
||||
|
||||
export type InputFieldProps = FieldWrapperPassThroughProps & {
|
||||
name: string;
|
||||
type?: 'text' | 'email' | 'password';
|
||||
type?: 'text' | 'email' | 'password' | 'number';
|
||||
className?: string;
|
||||
icon?: ReactElement;
|
||||
iconPosition?: 'start' | 'end';
|
||||
|
Loading…
Reference in New Issue
Block a user