Skip to content

Commit

Permalink
✨ Added support for enumerated values
Browse files Browse the repository at this point in the history
  • Loading branch information
MiloradFilipovic committed May 9, 2023
1 parent 3743981 commit 589d17a
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const fieldsUi = computed<INodeProperties[]>(() => {
default: field.type === 'boolean' ? false : '',
required: field.required,
description: getFieldDescription(field),
options: field.options,
};
});
});
Expand Down Expand Up @@ -376,8 +377,7 @@ defineExpose({
&.hasIssues {
.parameterIssues {
float: none;
align-self: flex-end;
padding-bottom: var(--spacing-2xs);
padding-top: var(--spacing-xl);
}
input,
input:focus {
Expand Down
38 changes: 36 additions & 2 deletions packages/nodes-base/nodes/Postgres/v2/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IDataObject, INode, INodeExecutionData } from 'n8n-workflow';
import type { IDataObject, INode, INodeExecutionData, INodePropertyOptions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import type {
Expand All @@ -13,6 +13,8 @@ import type {
WhereClause,
} from './interfaces';

const ENUM_VALUES_REGEX = /\{(.+?)\}/gm;

export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] {
if (!Array.isArray(data)) {
return [{ json: data }];
Expand Down Expand Up @@ -342,7 +344,7 @@ export async function isColumnUnique(
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
inner join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu
on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
where
WHERE
tc.CONSTRAINT_TYPE = 'UNIQUE'
or tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
and tc.TABLE_NAME = $1
Expand All @@ -353,6 +355,38 @@ export async function isColumnUnique(
return unique.some((u) => u.column_name === column);
}

export async function getEnumValues(
db: PgpDatabase,
schema: string,
table: string,
columnName: string,
): Promise<INodePropertyOptions[]> {
const options: INodePropertyOptions[] = [];
// First get the type name based on the column name
const enumName = await db.one(
'SELECT udt_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3',
[schema, table, columnName],
);
if (enumName) {
// Then get the values based on the type name
const fetchedValues = await db.one('SELECT enum_range(null::$1:name)', [enumName.udt_name]);
if (fetchedValues.enum_range) {
// Column values are returned as a string like {value1,value2,value3}
ENUM_VALUES_REGEX.lastIndex = 0;
const extractedValues = ENUM_VALUES_REGEX.exec(fetchedValues.enum_range as string);
if (extractedValues) {
extractedValues[1].split(',').forEach((value) => {
options.push({
name: value,
value,
});
});
}
}
}
return options;
}

export async function doesRowExist(
db: PgpDatabase,
schema: string,
Expand Down
17 changes: 10 additions & 7 deletions packages/nodes-base/nodes/Postgres/v2/methods/resourceMapping.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ILoadOptionsFunctions, ResourceMapperFields, FieldType } from 'n8n-workflow';
import { getTableSchema, isColumnUnique } from '../helpers/utils';
import type { ILoadOptionsFunctions, ResourceMapperFields, FieldType, INode, INodePropertyOptions } from 'n8n-workflow';
import { getEnumValues, getTableSchema, isColumnUnique } from '../helpers/utils';
import { Connections } from '../transport';
import type { ConnectionsData } from '../helpers/interfaces';

const fieldTypeMapping: Record<FieldType, string[]> = {
const fieldTypeMapping: Partial<Record<FieldType, string[]>> = {
string: ['text', 'varchar', 'character varying', 'character', 'char'],
number: [
'integer',
Expand All @@ -27,19 +27,18 @@ const fieldTypeMapping: Record<FieldType, string[]> = {
],
time: ['time', 'time without time zone', 'time with time zone'],
object: ['json', 'jsonb'],
array: [],
options: ['enum', 'USER-DEFINED'],
};

function mapPostgresType(postgresType: string): FieldType {
let mappedType: FieldType = 'string';

for (const t of Object.keys(fieldTypeMapping)) {
const postgresTypes = fieldTypeMapping[t as FieldType];
if (postgresTypes.includes(postgresType)) {
if (postgresTypes?.includes(postgresType)) {
mappedType = t as FieldType;
}
}

return mappedType;
}

Expand All @@ -64,15 +63,19 @@ export async function getMappingColumns(
const fields = await Promise.all(
columns.map(async (col) => {
const canBeUsedToMatch = await isColumnUnique(db, table, col.column_name);
const type = mapPostgresType(col.data_type);
const options =
type === 'options' ? await getEnumValues(db, schema, table, col.column_name) : undefined;
return {
id: col.column_name,
displayName: col.column_name,
match: fieldsToMatch.includes(col.column_name),
required: col.is_nullable !== 'YES',
defaultMatch: col.column_name === 'id',
display: true,
type: mapPostgresType(col.data_type),
type,
canBeUsedToMatch,
options,
};
}),
);
Expand Down
11 changes: 10 additions & 1 deletion packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1962,9 +1962,18 @@ export interface ResourceMapperField {
display: boolean;
type?: FieldType;
removed?: boolean;
options?: INodePropertyOptions[];
}

export type FieldType = 'string' | 'number' | 'dateTime' | 'boolean' | 'time' | 'array' | 'object';
export type FieldType =
| 'string'
| 'number'
| 'dateTime'
| 'boolean'
| 'time'
| 'array'
| 'object'
| 'options';

export type ResourceMapperValue = {
mappingMode: string;
Expand Down
19 changes: 13 additions & 6 deletions packages/workflow/src/NodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
NodeParameterValue,
WebhookHttpMethod,
FieldType,
INodePropertyOptions,
} from './Interfaces';
import {
isBoolean,
Expand Down Expand Up @@ -1106,7 +1107,11 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
return nodeIssues;
}

export const validateFieldType = (value: unknown, type: FieldType): boolean => {
export const validateFieldType = (
value: unknown,
type: FieldType,
options?: INodePropertyOptions[],
): boolean => {
if (value === null || value === undefined) return true;
switch (type.toLocaleLowerCase()) {
case 'number': {
Expand All @@ -1127,6 +1132,9 @@ export const validateFieldType = (value: unknown, type: FieldType): boolean => {
case 'array': {
return Array.isArray(value);
}
case 'options': {
return options?.some((option) => option.value === value) || false;
}
default: {
return true;
}
Expand Down Expand Up @@ -1281,18 +1289,17 @@ export function getParameterIssues(
nodeProperties.typeOptions?.resourceMapper?.fieldWords?.singular || 'Field';
fieldWordSingular = fieldWordSingular.charAt(0).toUpperCase() + fieldWordSingular.slice(1);
value.schema.forEach((field) => {
const fieldValue = value.value ? value.value[field.id] : undefined;
const fieldValue = value.value ? value.value[field.id] : null;
const key = `${nodeProperties.name}.${field.id}`;
const fieldErrors: string[] = [];
if (field.required) {
if (value.value === null || (value.value && value.value[field.id] === null)) {
if (value.value === null || fieldValue === null || fieldValue === undefined) {
const error = `${fieldWordSingular} "${field.id}" is required`;
fieldErrors.push(error);
}
}
if (field.type && !validateFieldType(fieldValue, field.type)) {
console.log(field.id, fieldValue, field.type);
const error = `${fieldWordSingular} "${field.id}" is not a valid ${field.type}`;
if (field.type && !validateFieldType(fieldValue, field.type, field.options)) {
const error = `${fieldWordSingular} value for '${field.id}'' is not valid for type '${field.type}'`;
fieldErrors.push(error);
}
if (fieldErrors.length > 0) {
Expand Down

0 comments on commit 589d17a

Please sign in to comment.