feat (dashboard): add custom types to column types (#3442)
### **PR Type** Enhancement ___ ### **Description** - Add custom column types to database tables - Improve handling of user-defined data types - Update UI components for custom type support - Refactor column type normalization and validation ___ ### Diagram Walkthrough ```mermaid flowchart LR A["Column Type Handling"] -- "Extend" --> B["Custom Types"] B -- "Update" --> C["UI Components"] B -- "Refactor" --> D["Type Normalization"] D -- "Improve" --> E["Data Validation"] ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>15 files</summary><table> <tr> <td><strong>Autocomplete.tsx</strong><dd><code>Enhance Autocomplete component for custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-b185666714ca832d5c45c366618b79862f6b4f03e4f7657c78afa38a52e7c4c2">+27/-3</a> </td> </tr> <tr> <td><strong>BaseColumnForm.tsx</strong><dd><code>Update BaseColumnForm to support custom column types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-9750f922830f8637c2d1b81c5e40128bc4fca7a9349a5314e421353d73bf6f38">+43/-16</a> </td> </tr> <tr> <td><strong>ColumnEditorRow.tsx</strong><dd><code>Modify ColumnEditorRow to handle custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-264f067037cfa5d08dbb97964a9ddb8f6296129441682b78f6984c37051ea3f8">+37/-10</a> </td> </tr> <tr> <td><strong>DataBrowserGrid.tsx</strong><dd><code>Update DataBrowserGrid to display full data type</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-5910fd8730fbe65c60aa5f54031989a7868e944d5958f69535e5684b72ca1396">+6/-11</a> </td> </tr> <tr> <td><strong>DatabaseRecordInputGroup.tsx</strong><dd><code>Adjust DatabaseRecordInputGroup for custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-52b5499e9afc3c5e4929046b487de649d421dda3250a4131462ec710575abc12">+1/-1</a> </td> </tr> <tr> <td><strong>prepareCreateColumnQuery.ts</strong><dd><code>Modify prepareCreateColumnQuery for custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-9b3695fb28760e86fc966e2149082b798664f145a8b64ef66184e55a905f5071">+1/-1</a> </td> </tr> <tr> <td><strong>prepareCreateTableQuery.ts</strong><dd><code>Adjust prepareCreateTableQuery for custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-1458307108df70f7037fa516ccab3a028533cf23f752778fcb09ed8d326530e5">+1/-1</a> </td> </tr> <tr> <td><strong>fetchTable.ts</strong><dd><code>Modify fetchTable to include full data type</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-a58cb7660972ff84991cdd9777de5cf0834485072cbd421f8809638227c36820">+36/-28</a> </td> </tr> <tr> <td><strong>prepareUpdateColumnQuery.ts</strong><dd><code>Adjust prepareUpdateColumnQuery for custom types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-ef957000505e4ba437656c38b4371d4041471ce5a4c193ef381aa55e0c51c308">+1/-1</a> </td> </tr> <tr> <td><strong>dataBrowser.ts</strong><dd><code>Update type definitions for custom column types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-33c6810dbd7e2910c86a15009467a348f064380b0e1dd787ef320b4e7543403b">+2/-3</a> </td> </tr> <tr> <td><strong>index.ts</strong><dd><code>Add new utility for normalizing column types</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-df62570fe4a332639b789274e3db4ea98cc695bc306f6dc1692851280bdb2fde">+1/-0</a> </td> </tr> <tr> <td><strong>normalizeColumnType.ts</strong><dd><code>Implement normalizeColumnType utility function</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-6bc7935091971eb83f99ba700e11f7214599d4d86e41f96d0d8295bdd6441d8f">+11/-0</a> </td> </tr> <tr> <td><strong>normalizeDatabaseColumn.ts</strong><dd><code>Modify normalizeDatabaseColumn to use new utility</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-e00d1c71fcbc63286896b597ce820388987e0a7edb005bda8a13bb0c0813434b">+2/-1</a> </td> </tr> <tr> <td><strong>postgresqlConstants.ts</strong><dd><code>Update PostgreSQL type constants and groups</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-b497da90feca5bff94b0d38b69e519d171d43acc292098054d672a73a89b4717">+6/-8</a> </td> </tr> <tr> <td><strong>DataGridTextCell.tsx</strong><dd><code>Adjust DataGridTextCell for custom type handling</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-d1ed74fe8eb7a61053dfe908966311e13915ad2127ee107b62f725d6c5282492">+1/-1</a> </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>5 files</summary><table> <tr> <td><strong>prepareCreateTableQuery.test.ts</strong><dd><code>Update tests for custom column type support</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-348ba7ca6fc037a9d0de76a24efc36846c634d82755bbf33dd5062a7face06ec">+232/-198</a></td> </tr> <tr> <td><strong>prepareUpdateColumnQuery.test.ts</strong><dd><code>Update tests for custom column type changes</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-65420c7003a95c03b31fc4b0e45f6a22387f81c1ce8e41a2d7cb89cc44dbda26">+557/-494</a></td> </tr> <tr> <td><strong>prepareUpdateTableQuery.test.ts</strong><dd><code>Update tests for custom column type support</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-57c2f882497d653700d68905bb54c891592a6bf302040d3008d624900f1bdf64">+2/-2</a> </td> </tr> <tr> <td><strong>getInputType.test.ts</strong><dd><code>Update tests for input type handling</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-01a507828b9440cd99bd0722ab5b577d8dd1774f2320168ad88222138960e831">+3/-3</a> </td> </tr> <tr> <td><strong>normalizeDatabaseColumn.test.ts</strong><dd><code>Update tests for database column normalization</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-a451c29ffa35243d4dbb462e3a048088c514f8056effedf782fcc57d5235e338">+5/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>rich-dragons-attend.md</strong><dd><code>Add changeset for custom column types feature</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3442/files#diff-bb90b20cf816a7c7bfc628f9daae90b9deff5d8c00f36361190d6147b46fb6be">+5/-0</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
This commit is contained in:
5
.changeset/rich-dragons-attend.md
Normal file
5
.changeset/rich-dragons-attend.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
feat (dashboard): add custom types to column types
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
} from '@/components/ui/v2/Autocomplete';
|
||||
import { Autocomplete } from '@/components/ui/v2/Autocomplete';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import type { MakeRequired } from '@/types/common';
|
||||
import { callAll } from '@/utils/callAll';
|
||||
import type { FilterOptionsState } from '@mui/material';
|
||||
import type { ForwardedRef } from 'react';
|
||||
@@ -53,6 +54,40 @@ export function defaultFilterOptions(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function defaultFilterGroupedOptions(
|
||||
options: AutocompleteOption<string>[],
|
||||
{ inputValue }: FilterOptionsState<AutocompleteOption<string>>,
|
||||
) {
|
||||
const optionsWithGroup = options as MakeRequired<
|
||||
AutocompleteOption<string>,
|
||||
'group'
|
||||
>[];
|
||||
const inputValueLower = inputValue.toLowerCase();
|
||||
const matchedSet = new Set<string>();
|
||||
const otherOptionsSet = new Set<string>();
|
||||
|
||||
optionsWithGroup.forEach((option) => {
|
||||
const optionLabelLower = option.label.toLowerCase();
|
||||
|
||||
if (optionLabelLower.includes(inputValueLower)) {
|
||||
matchedSet.add(option.group);
|
||||
otherOptionsSet.delete(option.group);
|
||||
} else if (!matchedSet.has(option.group)) {
|
||||
otherOptionsSet.add(option.group);
|
||||
}
|
||||
});
|
||||
const matchedOptions = optionsWithGroup.filter((option) =>
|
||||
matchedSet.has(option.group),
|
||||
);
|
||||
const otherOptions = optionsWithGroup.filter((option) =>
|
||||
otherOptionsSet.has(option.group),
|
||||
);
|
||||
|
||||
const result = [...matchedOptions, ...otherOptions];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function ControlledAutocomplete(
|
||||
{
|
||||
controllerProps,
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface AutocompleteProps<
|
||||
TOption extends AutocompleteOption = AutocompleteOption,
|
||||
> extends Omit<
|
||||
MaterialAutocompleteProps<TOption, boolean, boolean, boolean>,
|
||||
'renderInput' | 'autoSelect' | 'componentsProps'
|
||||
'renderInput' | 'autoSelect' | 'componentsProps' | 'isOptionEqualToValue'
|
||||
>,
|
||||
Pick<
|
||||
InputProps,
|
||||
@@ -100,11 +100,20 @@ export interface AutocompleteProps<
|
||||
*
|
||||
* @default 'never'
|
||||
*/
|
||||
showCustomOption?: 'always' | 'never' | 'auto';
|
||||
showCustomOption?: 'always' | 'never' | 'auto' | 'first';
|
||||
/**
|
||||
* Custom option label.
|
||||
*/
|
||||
customOptionLabel?: string | ((customOptionLabel: string) => string);
|
||||
isOptionEqualToValue?: (
|
||||
option: AutocompleteOption<string>,
|
||||
value: string | AutocompleteOption<string>,
|
||||
) => boolean;
|
||||
|
||||
sortByOptions?: (
|
||||
option1: AutocompleteOption<string>,
|
||||
option2: AutocompleteOption<string>,
|
||||
) => number;
|
||||
}
|
||||
|
||||
const StyledTag = styled(Chip)(({ theme }) => ({
|
||||
@@ -213,6 +222,7 @@ function Autocomplete(
|
||||
customOptionLabel: externalCustomOptionLabel,
|
||||
showCustomOption = 'never',
|
||||
'aria-label': ariaLabel,
|
||||
sortByOptions,
|
||||
...props
|
||||
}: AutocompleteProps<AutocompleteOption>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
@@ -263,6 +273,7 @@ function Autocomplete(
|
||||
openOnFocus
|
||||
disablePortal
|
||||
disableClearable
|
||||
autoFocus={false}
|
||||
componentsProps={{
|
||||
...defaultComponentsProps,
|
||||
popper: {
|
||||
@@ -315,7 +326,7 @@ function Autocomplete(
|
||||
}}
|
||||
isOptionEqualToValue={(
|
||||
option,
|
||||
value: string | number | AutocompleteOption<string>,
|
||||
value: string | AutocompleteOption<string>,
|
||||
) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
@@ -391,7 +402,24 @@ function Autocomplete(
|
||||
if (!inputValue) {
|
||||
return filteredOptions;
|
||||
}
|
||||
if (showCustomOption === 'first') {
|
||||
const isInputValueInOptions = filteredOptions.some(
|
||||
(filteredOption) => filteredOption.label === inputValue,
|
||||
);
|
||||
|
||||
return isInputValueInOptions
|
||||
? filteredOptions
|
||||
: [
|
||||
{
|
||||
value: inputValue,
|
||||
label: inputValue,
|
||||
dropdownLabel:
|
||||
customOptionLabel || `Select "${inputValue}"`,
|
||||
custom: Boolean(inputValue),
|
||||
},
|
||||
...filteredOptions,
|
||||
];
|
||||
}
|
||||
if (showCustomOption === 'auto') {
|
||||
const isInputValueInOptions = filteredOptions.some(
|
||||
(filteredOption) => filteredOption.label === inputValue,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import {
|
||||
ControlledAutocomplete,
|
||||
defaultFilterGroupedOptions,
|
||||
} from '@/components/form/ControlledAutocomplete';
|
||||
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
@@ -90,6 +93,10 @@ export default function BaseColumnForm({
|
||||
value: functionName,
|
||||
}),
|
||||
);
|
||||
const [inputValue, setInputValue] = useState<string>();
|
||||
useEffect(() => {
|
||||
setInputValue(type?.label ?? '');
|
||||
}, [type?.label]);
|
||||
|
||||
// react-hook-form's isDirty gets true even if an input field is focused, then
|
||||
// immediately unfocused - we can't rely on that information
|
||||
@@ -132,17 +139,50 @@ export default function BaseColumnForm({
|
||||
id="type"
|
||||
name="type"
|
||||
control={control}
|
||||
aria-label="Type"
|
||||
fullWidth
|
||||
placeholder="Select a column type"
|
||||
label="Type"
|
||||
helperText={errors.type?.message}
|
||||
error={Boolean(errors.type)}
|
||||
hideEmptyHelperText
|
||||
autoHighlight
|
||||
className="col-span-8 py-3"
|
||||
variant="inline"
|
||||
options={postgresTypeGroups}
|
||||
groupBy={(option) => option.group ?? ''}
|
||||
error={Boolean(errors.type)}
|
||||
placeholder="Select a column type"
|
||||
label="Type"
|
||||
hideEmptyHelperText
|
||||
className="col-span-8 py-3"
|
||||
variant="inline"
|
||||
autoHighlight
|
||||
clearOnBlur
|
||||
showCustomOption="first"
|
||||
filterOptions={defaultFilterGroupedOptions}
|
||||
freeSolo
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_event, value) => {
|
||||
// Keep the list scrolled to the top while searching
|
||||
requestAnimationFrame(() => {
|
||||
const listbox = document.querySelector('#type-listbox');
|
||||
if (listbox) {
|
||||
listbox.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
setInputValue(value);
|
||||
}}
|
||||
renderOption={(props, { label, value, custom }) => {
|
||||
if (custom) {
|
||||
return (
|
||||
<OptionBase {...props}>
|
||||
<span>Use type: "{value}"</span>
|
||||
</OptionBase>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OptionBase {...props}>
|
||||
<div className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5">
|
||||
<span>{label}</span>
|
||||
|
||||
<InlineCode>{value}</InlineCode>
|
||||
</div>
|
||||
</OptionBase>
|
||||
);
|
||||
}}
|
||||
onChange={(_event, value) => {
|
||||
setDefaultValueInputText('');
|
||||
|
||||
@@ -155,16 +195,6 @@ export default function BaseColumnForm({
|
||||
setValue('defaultValue', null);
|
||||
}
|
||||
}}
|
||||
noOptionsText="No types found"
|
||||
renderOption={(props, { label, value }) => (
|
||||
<OptionBase {...props}>
|
||||
<div className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5">
|
||||
<span>{label}</span>
|
||||
|
||||
<InlineCode>{value}</InlineCode>
|
||||
</div>
|
||||
</OptionBase>
|
||||
)}
|
||||
/>
|
||||
|
||||
{identityTypes.includes(type?.value) && (
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import {
|
||||
ControlledAutocomplete,
|
||||
defaultFilterGroupedOptions,
|
||||
} from '@/components/form/ControlledAutocomplete';
|
||||
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import type { CheckboxProps } from '@/components/ui/v2/Checkbox';
|
||||
@@ -83,9 +86,15 @@ function NameInput({ index }: FieldArrayInputProps) {
|
||||
}
|
||||
|
||||
function TypeAutocomplete({ index }: FieldArrayInputProps) {
|
||||
const [inputValue, setInputValue] = useState<string>();
|
||||
const { setValue } = useFormContext();
|
||||
const { errors } = useFormState({ name: `columns.${index}.type` });
|
||||
const identityColumnIndex = useWatch({ name: 'identityColumnIndex' });
|
||||
const type = useWatch({ name: `columns.${index}.type` });
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(type?.label ?? '');
|
||||
}, [type?.label]);
|
||||
|
||||
return (
|
||||
<ControlledAutocomplete
|
||||
@@ -96,19 +105,43 @@ function TypeAutocomplete({ index }: FieldArrayInputProps) {
|
||||
groupBy={(option) => option.group ?? ''}
|
||||
placeholder="Select type"
|
||||
hideEmptyHelperText
|
||||
autoHighlight
|
||||
noOptionsText="No types found"
|
||||
freeSolo
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_event, value) => {
|
||||
// Keep the list scrolled to the top while searching
|
||||
requestAnimationFrame(() => {
|
||||
const listbox = document.querySelector(
|
||||
`[id="columns.${index}.type-listbox"]`,
|
||||
);
|
||||
if (listbox) {
|
||||
listbox.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
setInputValue(value);
|
||||
}}
|
||||
clearOnBlur
|
||||
showCustomOption="first"
|
||||
filterOptions={defaultFilterGroupedOptions}
|
||||
error={Boolean(errors?.columns?.[index]?.type)}
|
||||
helperText={(errors?.columns?.[index]?.type as FieldError)?.message}
|
||||
renderOption={(props, { label, value }) => (
|
||||
<OptionBase {...props}>
|
||||
<div className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5">
|
||||
<span>{label}</span>
|
||||
renderOption={(optionProps, { label, value, custom }) => {
|
||||
if (custom) {
|
||||
return (
|
||||
<OptionBase {...optionProps}>
|
||||
<span>Use type: "{value}"</span>
|
||||
</OptionBase>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OptionBase {...optionProps}>
|
||||
<div className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5">
|
||||
<span>{label}</span>
|
||||
|
||||
<InlineCode>{value}</InlineCode>
|
||||
</div>
|
||||
</OptionBase>
|
||||
)}
|
||||
<InlineCode>{value}</InlineCode>
|
||||
</div>
|
||||
</OptionBase>
|
||||
);
|
||||
}}
|
||||
onChange={(_event, value) => {
|
||||
if (typeof value === 'string' || Array.isArray(value)) {
|
||||
return;
|
||||
|
||||
@@ -77,21 +77,16 @@ export function createDataGridColumn(
|
||||
{column.column_name}
|
||||
</span>
|
||||
|
||||
<InlineCode>
|
||||
{column.udt_name}
|
||||
{column.character_maximum_length
|
||||
? `(${column.character_maximum_length})`
|
||||
: null}
|
||||
</InlineCode>
|
||||
<InlineCode>{column.full_data_type}</InlineCode>
|
||||
</div>
|
||||
),
|
||||
id: column.column_name,
|
||||
accessor: column.column_name,
|
||||
sortType: 'basic',
|
||||
width: 200,
|
||||
width: 250,
|
||||
isEditable,
|
||||
type: 'text',
|
||||
specificType: column.udt_name,
|
||||
specificType: column.full_data_type,
|
||||
maxLength: column.character_maximum_length,
|
||||
Cell: DataGridTextCell,
|
||||
isPrimary: column.is_primary,
|
||||
@@ -110,7 +105,7 @@ export function createDataGridColumn(
|
||||
return {
|
||||
...defaultColumnConfiguration,
|
||||
type: 'number',
|
||||
width: 200,
|
||||
width: 250,
|
||||
Cell: DataGridIntegerCell,
|
||||
};
|
||||
}
|
||||
@@ -119,7 +114,7 @@ export function createDataGridColumn(
|
||||
return {
|
||||
...defaultColumnConfiguration,
|
||||
type: 'text',
|
||||
width: 200,
|
||||
width: 250,
|
||||
Cell: DataGridDecimalCell,
|
||||
};
|
||||
}
|
||||
@@ -141,7 +136,7 @@ export function createDataGridColumn(
|
||||
...defaultColumnConfiguration,
|
||||
type: 'text',
|
||||
isCopiable: true,
|
||||
width: 200,
|
||||
width: 250,
|
||||
Cell: DataGridTextCell,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function DatabaseRecordInputGroup({
|
||||
const isMultiline =
|
||||
specificType === 'text' ||
|
||||
specificType === 'bpchar' ||
|
||||
specificType === 'varchar' ||
|
||||
specificType === 'character varying' ||
|
||||
specificType === 'json' ||
|
||||
specificType === 'jsonb';
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function prepareCreateColumnQuery({
|
||||
let args: ReturnType<typeof getPreparedHasuraQuery>[] = [
|
||||
getPreparedHasuraQuery(
|
||||
dataSource,
|
||||
'ALTER TABLE %I.%I ADD %I %I %s %s %s',
|
||||
'ALTER TABLE %I.%I ADD %I %s %s %s %s',
|
||||
schema,
|
||||
table,
|
||||
column.name,
|
||||
|
||||
@@ -2,213 +2,246 @@ import type { DatabaseTable } from '@/features/orgs/projects/database/dataGrid/t
|
||||
import { expect, test } from 'vitest';
|
||||
import prepareCreateTableQuery from './prepareCreateTableQuery';
|
||||
|
||||
test('should prepare a simple query', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text NOT NULL, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with foreign keys', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
},
|
||||
{
|
||||
name: 'author_id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
],
|
||||
foreignKeyRelations: [
|
||||
{
|
||||
name: 'test_table_author_id_fkey',
|
||||
columnName: 'author_id',
|
||||
referencedSchema: 'public',
|
||||
referencedTable: 'authors',
|
||||
referencedColumn: 'id',
|
||||
updateAction: 'RESTRICT',
|
||||
deleteAction: 'RESTRICT',
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text NOT NULL, author_id uuid NOT NULL, PRIMARY KEY (id), FOREIGN KEY (author_id) REFERENCES public.authors (id) ON UPDATE RESTRICT ON DELETE RESTRICT);',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with unique keys', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isUnique: true,
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text UNIQUE NOT NULL, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with nullable columns', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text, is_active bool, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with default values', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
// this is a default value preset
|
||||
defaultValue: {
|
||||
value: 'gen_random_uuid()',
|
||||
label: 'gen_random_uuid()',
|
||||
describe('prepareCreateTableQuery', () => {
|
||||
test('should prepare a simple query', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
// this is a custom default value
|
||||
defaultValue: { value: 'true', label: 'true', custom: true },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text NOT NULL, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
"CREATE TABLE public.test_table (id uuid DEFAULT gen_random_uuid() NOT NULL, name text, is_active bool DEFAULT 'true', PRIMARY KEY (id));",
|
||||
);
|
||||
});
|
||||
test('should prepare a query with foreign keys', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
},
|
||||
{
|
||||
name: 'author_id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
],
|
||||
foreignKeyRelations: [
|
||||
{
|
||||
name: 'test_table_author_id_fkey',
|
||||
columnName: 'author_id',
|
||||
referencedSchema: 'public',
|
||||
referencedTable: 'authors',
|
||||
referencedColumn: 'id',
|
||||
updateAction: 'RESTRICT',
|
||||
deleteAction: 'RESTRICT',
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
test('should prepare a query with an identity column', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'int4', label: 'Integer' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
defaultValue: { value: 'true', label: 'true' },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
identityColumn: 'id',
|
||||
};
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text NOT NULL, author_id uuid NOT NULL, PRIMARY KEY (id), FOREIGN KEY (author_id) REFERENCES public.authors (id) ON UPDATE RESTRICT ON DELETE RESTRICT);',
|
||||
);
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id int4 GENERATED ALWAYS AS IDENTITY, name text, is_active bool DEFAULT true, PRIMARY KEY (id));',
|
||||
);
|
||||
test('should prepare a query with unique keys', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isUnique: true,
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text UNIQUE NOT NULL, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with nullable columns', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name text, is_active bool, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with default values', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
// this is a default value preset
|
||||
defaultValue: {
|
||||
value: 'gen_random_uuid()',
|
||||
label: 'gen_random_uuid()',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
// this is a custom default value
|
||||
defaultValue: { value: 'true', label: 'true', custom: true },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
"CREATE TABLE public.test_table (id uuid DEFAULT gen_random_uuid() NOT NULL, name text, is_active bool DEFAULT 'true', PRIMARY KEY (id));",
|
||||
);
|
||||
});
|
||||
|
||||
test('should prepare a query with an identity column', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'int4', label: 'Integer' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: { value: 'text', label: 'Text' },
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: { value: 'bool', label: 'Boolean' },
|
||||
isNullable: true,
|
||||
defaultValue: { value: 'true', label: 'true' },
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
identityColumn: 'id',
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id int4 GENERATED ALWAYS AS IDENTITY, name text, is_active bool DEFAULT true, PRIMARY KEY (id));',
|
||||
);
|
||||
});
|
||||
test('should prepare a query with no primary key', () => {
|
||||
const table: DatabaseTable = {
|
||||
name: 'test_table',
|
||||
primaryKey: [],
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: { value: 'uuid', label: 'UUID' },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: {
|
||||
value: 'varchar(10)' as any,
|
||||
label: 'varchar(10)',
|
||||
},
|
||||
},
|
||||
],
|
||||
// No primaryKey property set
|
||||
};
|
||||
|
||||
const transaction = prepareCreateTableQuery({
|
||||
dataSource: 'default',
|
||||
schema: 'public',
|
||||
table,
|
||||
});
|
||||
|
||||
expect(transaction).toHaveLength(1);
|
||||
expect(transaction[0].args.sql).toBe(
|
||||
'CREATE TABLE public.test_table (id uuid NOT NULL, name varchar(10) NOT NULL);',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function prepareCreateTableQuery({
|
||||
}: PrepareCreateTableQueryVariables) {
|
||||
let columnsAndConstraints = table.columns
|
||||
.map((column) => {
|
||||
const columnBase = format('%I %I', column.name, column.type.value);
|
||||
const columnBase = format('%I %s', column.name, column.type.value);
|
||||
const isIdentity = table.identityColumn === column.name;
|
||||
|
||||
if (isIdentity) {
|
||||
|
||||
@@ -114,34 +114,42 @@ export default async function fetchTable({
|
||||
args: [
|
||||
getPreparedReadOnlyHasuraQuery(
|
||||
dataSource,
|
||||
`SELECT ROW_TO_JSON(TABLE_DATA) FROM (\
|
||||
SELECT *,\
|
||||
EXISTS (\
|
||||
SELECT NSP.NSPNAME, CLS.RELNAME, ATTR.ATTNAME\
|
||||
FROM PG_INDEX IND\
|
||||
JOIN PG_CLASS CLS ON CLS.OID = IND.INDRELID\
|
||||
JOIN PG_ATTRIBUTE ATTR ON ATTR.ATTRELID = CLS.OID\
|
||||
AND ATTR.ATTNUM = ANY(IND.INDKEY)\
|
||||
JOIN PG_NAMESPACE NSP ON NSP.OID = CLS.RELNAMESPACE\
|
||||
WHERE NSPNAME = %1$L AND RELNAME = %2$L AND ATTR.ATTNAME = COLS.COLUMN_NAME AND INDISPRIMARY\
|
||||
) AS IS_PRIMARY,\
|
||||
EXISTS (\
|
||||
SELECT NSP.NSPNAME, CLS.RELNAME, ATTR.ATTNAME\
|
||||
FROM PG_INDEX IND\
|
||||
JOIN PG_CLASS CLS ON CLS.OID = IND.INDRELID\
|
||||
JOIN PG_ATTRIBUTE ATTR ON ATTR.ATTRELID = CLS.OID\
|
||||
AND ATTR.ATTNUM = ANY(IND.INDKEY)\
|
||||
JOIN PG_NAMESPACE NSP ON NSP.OID = CLS.RELNAMESPACE\
|
||||
WHERE NSPNAME = %1$L AND RELNAME = %2$L AND ATTR.ATTNAME = COLS.COLUMN_NAME AND INDISUNIQUE\
|
||||
) AS IS_UNIQUE,\
|
||||
(\
|
||||
SELECT PG_CATALOG.COL_DESCRIPTION(CLS.OID, COLS.ORDINAL_POSITION::INT)\
|
||||
FROM PG_CATALOG.PG_CLASS CLS\
|
||||
WHERE CLS.OID = (SELECT '%1$I.%2$I'::REGCLASS::OID) AND CLS.RELNAME = COLS.TABLE_NAME\
|
||||
) AS COLUMN_COMMENT\
|
||||
FROM INFORMATION_SCHEMA.COLUMNS COLS\
|
||||
WHERE TABLE_SCHEMA = %1$L AND TABLE_NAME = %2$L\
|
||||
) TABLE_DATA`,
|
||||
`
|
||||
SELECT ROW_TO_JSON(TABLE_DATA) FROM (
|
||||
SELECT *,
|
||||
PG_CATALOG.FORMAT_TYPE(
|
||||
(SELECT ATTTYPID FROM PG_ATTRIBUTE
|
||||
WHERE ATTRELID = (SELECT OID FROM PG_CLASS WHERE RELNAME = %2$L AND RELNAMESPACE = (SELECT OID FROM PG_NAMESPACE WHERE NSPNAME = %1$L))
|
||||
AND ATTNAME = COLS.COLUMN_NAME),
|
||||
(SELECT ATTTYPMOD FROM PG_ATTRIBUTE
|
||||
WHERE ATTRELID = (SELECT OID FROM PG_CLASS WHERE RELNAME = %2$L AND RELNAMESPACE = (SELECT OID FROM PG_NAMESPACE WHERE NSPNAME = %1$L))
|
||||
AND ATTNAME = COLS.COLUMN_NAME)
|
||||
) AS FULL_DATA_TYPE,
|
||||
EXISTS (
|
||||
SELECT NSP.NSPNAME, CLS.RELNAME, ATTR.ATTNAME
|
||||
FROM PG_INDEX IND
|
||||
JOIN PG_CLASS CLS ON CLS.OID = IND.INDRELID
|
||||
JOIN PG_ATTRIBUTE ATTR ON ATTR.ATTRELID = CLS.OID AND ATTR.ATTNUM = ANY(IND.INDKEY)
|
||||
JOIN PG_NAMESPACE NSP ON NSP.OID = CLS.RELNAMESPACE
|
||||
WHERE NSPNAME = %1$L AND RELNAME = %2$L AND ATTR.ATTNAME = COLS.COLUMN_NAME AND INDISPRIMARY
|
||||
) AS IS_PRIMARY,
|
||||
EXISTS (
|
||||
SELECT NSP.NSPNAME, CLS.RELNAME, ATTR.ATTNAME
|
||||
FROM PG_INDEX IND
|
||||
JOIN PG_CLASS CLS ON CLS.OID = IND.INDRELID
|
||||
JOIN PG_ATTRIBUTE ATTR ON ATTR.ATTRELID = CLS.OID AND ATTR.ATTNUM = ANY(IND.INDKEY)
|
||||
JOIN PG_NAMESPACE NSP ON NSP.OID = CLS.RELNAMESPACE
|
||||
WHERE NSPNAME = %1$L AND RELNAME = %2$L AND ATTR.ATTNAME = COLS.COLUMN_NAME AND INDISUNIQUE
|
||||
) AS IS_UNIQUE,
|
||||
(
|
||||
SELECT PG_CATALOG.COL_DESCRIPTION(CLS.OID, COLS.ORDINAL_POSITION::INT)
|
||||
FROM PG_CATALOG.PG_CLASS CLS
|
||||
WHERE CLS.OID = (SELECT '%1$s.%2$s'::REGCLASS::OID) AND CLS.RELNAME = COLS.TABLE_NAME
|
||||
) AS COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS COLS
|
||||
WHERE TABLE_SCHEMA = %1$L AND TABLE_NAME = %2$L
|
||||
) TABLE_DATA;
|
||||
`,
|
||||
schema,
|
||||
table,
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ export default function prepareUpdateColumnQuery({
|
||||
),
|
||||
getPreparedHasuraQuery(
|
||||
dataSource,
|
||||
'ALTER TABLE %I.%I ALTER COLUMN %3$I TYPE %4$I USING %3$I::%4$I',
|
||||
'ALTER TABLE %I.%I ALTER COLUMN %3$I TYPE %4$s USING %3$I::%4$s',
|
||||
schema,
|
||||
table,
|
||||
originalColumn.id,
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('prepareUpdateTableQuery', () => {
|
||||
{
|
||||
id: 'author_id',
|
||||
name: 'age',
|
||||
type: { value: 'int2', label: 'int2' },
|
||||
type: { value: 'numeric(10,2)' as any, label: 'numeric(10,2)' },
|
||||
},
|
||||
],
|
||||
foreignKeyRelations: [],
|
||||
@@ -186,7 +186,7 @@ describe('prepareUpdateTableQuery', () => {
|
||||
'ALTER TABLE public.test_table ALTER COLUMN author_id DROP DEFAULT;',
|
||||
);
|
||||
expect(transaction[1].args.sql).toBe(
|
||||
'ALTER TABLE public.test_table ALTER COLUMN author_id TYPE int2 USING author_id::int2;',
|
||||
'ALTER TABLE public.test_table ALTER COLUMN author_id TYPE numeric(10,2) USING author_id::numeric(10,2);',
|
||||
);
|
||||
expect(transaction[2].args.sql).toBe(
|
||||
'ALTER TABLE public.test_table RENAME COLUMN author_id TO age;',
|
||||
|
||||
@@ -220,7 +220,7 @@ export interface ColumnInsertOptions {
|
||||
/**
|
||||
* User defined column type of a character field in PostgreSQL.
|
||||
*/
|
||||
export type CharacterColumnType = 'varchar' | 'bpchar' | 'text';
|
||||
export type CharacterColumnType = 'character varying' | 'bpchar' | 'text';
|
||||
|
||||
/**
|
||||
* User defined column type of a boolean field in PostgreSQL.
|
||||
@@ -249,7 +249,6 @@ export type DateColumnType =
|
||||
export type NumericColumnType =
|
||||
| 'oid'
|
||||
| 'numeric'
|
||||
| 'decimal'
|
||||
| 'int'
|
||||
| 'int2'
|
||||
| 'int4'
|
||||
|
||||
@@ -17,9 +17,9 @@ describe('getInputType', () => {
|
||||
|
||||
test('should return "text" if the column is text based', () => {
|
||||
expect(getInputType({ type: 'text', specificType: 'text' })).toBe('text');
|
||||
expect(getInputType({ type: 'text', specificType: 'varchar' })).toBe(
|
||||
'text',
|
||||
);
|
||||
expect(
|
||||
getInputType({ type: 'text', specificType: 'character varying' }),
|
||||
).toBe('text');
|
||||
expect(getInputType({ type: 'text', specificType: 'bpchar' })).toBe('text');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as normalizeColumnType } from './normalizeColumnType';
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { NormalizedQueryDataRow } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { postgresTypeGroups } from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
||||
|
||||
function normalizeColumnType(column: NormalizedQueryDataRow) {
|
||||
const label =
|
||||
postgresTypeGroups.find((pt) => pt.value === column.full_data_type)
|
||||
?.label || column.full_data_type;
|
||||
return {
|
||||
label,
|
||||
value: column.full_data_type,
|
||||
custom:
|
||||
column.data_type === 'USER-DEFINED' ||
|
||||
column.full_data_type !== column.udt_name,
|
||||
};
|
||||
}
|
||||
|
||||
export default normalizeColumnType;
|
||||
@@ -33,6 +33,7 @@ const rawColumn: NormalizedQueryDataRow = {
|
||||
udt_catalog: 'postgres',
|
||||
udt_schema: 'pg_catalog',
|
||||
udt_name: 'uuid',
|
||||
full_data_type: 'uuid',
|
||||
scope_catalog: null,
|
||||
scope_schema: null,
|
||||
scope_name: null,
|
||||
@@ -90,6 +91,7 @@ test('should set identity to true if the column is an identity column', () => {
|
||||
data_type: 'int4',
|
||||
column_default: null,
|
||||
is_identity: 'YES',
|
||||
full_data_type: 'int4',
|
||||
};
|
||||
|
||||
const column = normalizeDatabaseColumn(rawIdentityColumn);
|
||||
@@ -103,7 +105,8 @@ test('should set identity to true if the column is an identity column', () => {
|
||||
isNullable: false,
|
||||
type: {
|
||||
value: 'int4',
|
||||
label: 'int4',
|
||||
label: 'integer',
|
||||
custom: false,
|
||||
},
|
||||
defaultValue: null,
|
||||
comment: null,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDefaultValue';
|
||||
|
||||
import { normalizeColumnType } from '@/features/orgs/projects/database/dataGrid/utils/normalizeColumnType';
|
||||
/**
|
||||
* Converts a raw database column to a normalized database column.
|
||||
*
|
||||
@@ -19,7 +20,7 @@ export default function normalizeDatabaseColumn(
|
||||
return {
|
||||
id: rawColumn.column_name,
|
||||
name: rawColumn.column_name,
|
||||
type: { value: rawColumn.udt_name, label: rawColumn.udt_name },
|
||||
type: normalizeColumnType(rawColumn),
|
||||
isPrimary: rawColumn.is_primary,
|
||||
isIdentity: rawColumn.is_identity === 'YES',
|
||||
isNullable: rawColumn.is_nullable === 'YES',
|
||||
|
||||
@@ -29,12 +29,7 @@ export const POSTGRESQL_INTEGER_TYPES = [
|
||||
'oid',
|
||||
];
|
||||
|
||||
export const POSTGRESQL_DECIMAL_TYPES = [
|
||||
'decimal',
|
||||
'numeric',
|
||||
'real',
|
||||
'double precision',
|
||||
];
|
||||
export const POSTGRESQL_DECIMAL_TYPES = ['numeric', 'real', 'double precision'];
|
||||
|
||||
/**
|
||||
* Character data types in PostgreSQL.
|
||||
@@ -77,7 +72,11 @@ export const postgresTypeGroups: {
|
||||
value: ColumnType;
|
||||
}[] = [
|
||||
{ group: 'String types', label: 'text', value: 'text' },
|
||||
{ group: 'String types', label: 'character varying', value: 'varchar' },
|
||||
{
|
||||
group: 'String types',
|
||||
label: 'character varying',
|
||||
value: 'character varying',
|
||||
},
|
||||
{ group: 'String types', label: 'character', value: 'bpchar' },
|
||||
|
||||
{ group: 'UUID types', label: 'uuid', value: 'uuid' },
|
||||
@@ -86,10 +85,13 @@ export const postgresTypeGroups: {
|
||||
{ group: 'Numeric types', label: 'smallint', value: 'int2' },
|
||||
{ group: 'Numeric types', label: 'integer', value: 'int4' },
|
||||
{ group: 'Numeric types', label: 'bigint', value: 'int8' },
|
||||
{ group: 'Numeric types', label: 'decimal', value: 'decimal' },
|
||||
{ group: 'Numeric types', label: 'numeric', value: 'numeric' },
|
||||
{ group: 'Numeric types', label: 'real', value: 'float4' },
|
||||
{ group: 'Numeric types', label: 'double precision', value: 'float8' },
|
||||
{
|
||||
group: 'Numeric types',
|
||||
label: 'double precision',
|
||||
value: 'float8',
|
||||
},
|
||||
{ group: 'Boolean types', label: 'boolean', value: 'bool' },
|
||||
{ group: 'Date types', label: 'date', value: 'date' },
|
||||
{
|
||||
@@ -102,8 +104,16 @@ export const postgresTypeGroups: {
|
||||
label: 'timestamp with time zone',
|
||||
value: 'timestamptz',
|
||||
},
|
||||
{ group: 'Date types', label: 'time without time zone', value: 'time' },
|
||||
{ group: 'Date types', label: 'time with time zone', value: 'timetz' },
|
||||
{
|
||||
group: 'Date types',
|
||||
label: 'time without time zone',
|
||||
value: 'time',
|
||||
},
|
||||
{
|
||||
group: 'Date types',
|
||||
label: 'time with time zone',
|
||||
value: 'timetz',
|
||||
},
|
||||
{ group: 'Date types', label: 'interval', value: 'interval' },
|
||||
{ group: 'Binary types', label: 'bytea', value: 'bytea' },
|
||||
{ group: 'Geometric types', label: 'point', value: 'point' },
|
||||
@@ -164,7 +174,11 @@ export const postgresTypeGroups: {
|
||||
label: 'function with argument types',
|
||||
value: 'regprocedure',
|
||||
},
|
||||
{ group: 'Object Identifier types', label: 'role name', value: 'regrole' },
|
||||
{
|
||||
group: 'Object Identifier types',
|
||||
label: 'role name',
|
||||
value: 'regrole',
|
||||
},
|
||||
{
|
||||
group: 'Object Identifier types',
|
||||
label: 'data type name',
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function DataGridTextCell<TData extends object>({
|
||||
const isMultiline =
|
||||
specificType === 'text' ||
|
||||
specificType === 'bpchar' ||
|
||||
specificType === 'varchar' ||
|
||||
specificType === 'character varying' ||
|
||||
specificType === 'json' ||
|
||||
specificType === 'jsonb';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user