first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,418 @@
import type { INodeProperties } from 'n8n-workflow';
export const documentOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['document'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a document',
action: 'Create a document',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a document',
action: 'Delete a document',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a document',
action: 'Get a document',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve many documents',
action: 'Get many documents',
},
{
name: 'Update',
value: 'update',
description: 'Update a document',
action: 'Update a document',
},
],
default: 'create',
},
];
export const documentFields: INodeProperties[] = [
// ----------------------------------
// document: getAll
// ----------------------------------
{
displayName: 'DocType Name or ID',
name: 'docType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description:
'DocType whose documents to retrieve. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
placeholder: 'Customer',
displayOptions: {
show: {
resource: ['document'],
operation: ['getAll'],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['document'],
operation: ['getAll'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 10,
description: 'Max number of results to return',
displayOptions: {
show: {
resource: ['document'],
operation: ['getAll'],
returnAll: [false],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['document'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Field Names or IDs',
name: 'fields',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getDocFilters',
loadOptionsDependsOn: ['docType'],
},
default: [],
description:
'Comma-separated list of fields to return. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
placeholder: 'name,country',
},
{
displayName: 'Filters',
name: 'filters',
type: 'fixedCollection',
default: {},
placeholder: 'Add Filter',
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Property',
name: 'customProperty',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: ['docType'],
},
default: '',
},
{
displayName: 'Operator',
name: 'operator',
type: 'options',
default: 'is',
options: [
{
name: 'EQUALS, or GREATER',
value: 'equalsGreater',
},
{
name: 'EQUALS, or LESS',
value: 'equalsLess',
},
{
name: 'IS',
value: 'is',
},
{
name: 'IS GREATER',
value: 'greater',
},
{
name: 'IS LESS',
value: 'less',
},
{
name: 'IS NOT',
value: 'isNot',
},
],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the operator condition',
},
],
},
],
},
],
},
// ----------------------------------
// document: create
// ----------------------------------
{
displayName: 'DocType Name or ID',
name: 'docType',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
required: true,
description:
'DocType you would like to create. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
placeholder: 'Customer',
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
},
{
displayName: 'Properties',
name: 'properties',
type: 'fixedCollection',
placeholder: 'Add Property',
required: true,
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
options: [
{
displayName: 'Property',
name: 'customProperty',
placeholder: 'Add Property',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: ['docType'],
},
default: [],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
// ----------------------------------
// document: get
// ----------------------------------
{
displayName: 'DocType Name or ID',
name: 'docType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description:
'The type of document you would like to get. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
displayOptions: {
show: {
resource: ['document'],
operation: ['get'],
},
},
required: true,
},
{
displayName: 'Document Name',
name: 'documentName',
type: 'string',
default: '',
description: 'The name (ID) of document you would like to get',
displayOptions: {
show: {
resource: ['document'],
operation: ['get'],
},
},
required: true,
},
// ----------------------------------
// document: delete
// ----------------------------------
{
displayName: 'DocType Name or ID',
name: 'docType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description:
'The type of document you would like to delete. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
displayOptions: {
show: {
resource: ['document'],
operation: ['delete'],
},
},
required: true,
},
{
displayName: 'Document Name',
name: 'documentName',
type: 'string',
default: '',
description: 'The name (ID) of document you would like to get',
displayOptions: {
show: {
resource: ['document'],
operation: ['delete'],
},
},
required: true,
},
// ----------------------------------
// document: update
// ----------------------------------
{
displayName: 'DocType Name or ID',
name: 'docType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description:
'The type of document you would like to update. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
displayOptions: {
show: {
resource: ['document'],
operation: ['update'],
},
},
required: true,
},
{
displayName: 'Document Name',
name: 'documentName',
type: 'string',
default: '',
description: 'The name (ID) of document you would like to get',
displayOptions: {
show: {
resource: ['document'],
operation: ['update'],
},
},
required: true,
},
{
displayName: 'Properties',
name: 'properties',
type: 'fixedCollection',
placeholder: 'Add Property',
description: 'Properties of request body',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['document'],
operation: ['update'],
},
},
options: [
{
displayName: 'Property',
name: 'customProperty',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: ['docType'],
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
];
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.erpNext",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Finance & Accounting", "Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/erpnext/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.erpnext/"
}
]
}
}
@@ -0,0 +1,292 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { documentFields, documentOperations } from './DocumentDescription';
import { erpNextApiRequest, erpNextApiRequestAllItems } from './GenericFunctions';
import type { DocumentProperties } from './utils';
import { processNames, toSQL } from './utils';
export class ERPNext implements INodeType {
description: INodeTypeDescription = {
displayName: 'ERPNext',
name: 'erpNext',
icon: 'file:erpnext.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume ERPNext API',
defaults: {
name: 'ERPNext',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'erpNextApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Document',
value: 'document',
},
],
default: 'document',
},
...documentOperations,
...documentFields,
],
};
methods = {
loadOptions: {
async getDocTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const data = await erpNextApiRequestAllItems.call(
this,
'data',
'GET',
'/api/resource/DocType',
{},
);
const docTypes = data.map(({ name }: { name: string }) => {
return { name, value: encodeURI(name) };
});
return processNames(docTypes);
},
async getDocFilters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const docType = this.getCurrentNodeParameter('docType') as string;
const { data } = await erpNextApiRequest.call(
this,
'GET',
`/api/resource/DocType/${docType}`,
{},
);
const docFields = data.fields.map(
({ label, fieldname }: { label: string; fieldname: string }) => {
return { name: label, value: fieldname };
},
);
docFields.unshift({ name: '*', value: '*' });
return processNames(docFields);
},
async getDocFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const docType = this.getCurrentNodeParameter('docType') as string;
const { data } = await erpNextApiRequest.call(
this,
'GET',
`/api/resource/DocType/${docType}`,
{},
);
const docFields = data.fields.map(
({ label, fieldname }: { label: string; fieldname: string }) => {
return { name: label, value: fieldname };
},
);
return processNames(docFields);
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
let responseData;
const body: IDataObject = {};
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/Resources/post_api_resource_Webhook
// https://frappeframework.com/docs/user/en/guides/integration/rest_api/manipulating_documents
if (resource === 'document') {
// *********************************************************************
// document
// *********************************************************************
if (operation === 'get') {
// ----------------------------------
// document: get
// ----------------------------------
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType___DocumentName_
const docType = this.getNodeParameter('docType', i) as string;
const documentName = this.getNodeParameter('documentName', i) as string;
responseData = await erpNextApiRequest.call(
this,
'GET',
`/api/resource/${docType}/${documentName}`,
);
responseData = responseData.data;
}
if (operation === 'getAll') {
// ----------------------------------
// document: getAll
// ----------------------------------
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/get_api_resource__DocType_
const docType = this.getNodeParameter('docType', i) as string;
const endpoint = `/api/resource/${docType}`;
const { fields, filters } = this.getNodeParameter('options', i) as {
fields: string[];
filters: {
customProperty: Array<{ field: string; operator: string; value: string }>;
};
};
// fields=["test", "example", "hi"]
if (fields) {
if (fields.includes('*')) {
qs.fields = JSON.stringify(['*']);
} else {
qs.fields = JSON.stringify(fields);
}
}
// filters=[["Person","first_name","=","Jane"]]
// TODO: filters not working
if (filters) {
qs.filters = JSON.stringify(
filters.customProperty.map((filter) => {
return [docType, filter.field, toSQL(filter.operator), filter.value];
}),
);
}
const returnAll = this.getNodeParameter('returnAll', i);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i);
qs.limit_page_length = limit;
qs.limit_start = 0;
responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.data;
} else {
responseData = await erpNextApiRequestAllItems.call(
this,
'data',
'GET',
endpoint,
{},
qs,
);
}
} else if (operation === 'create') {
// ----------------------------------
// document: create
// ----------------------------------
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/post_api_resource__DocType_
const properties = this.getNodeParameter('properties', i) as DocumentProperties;
if (!properties.customProperty.length) {
throw new NodeOperationError(
this.getNode(),
'Please enter at least one property for the document to create.',
{ itemIndex: i },
);
}
properties.customProperty.forEach((property) => {
body[property.field] = property.value;
});
const docType = this.getNodeParameter('docType', i) as string;
responseData = await erpNextApiRequest.call(
this,
'POST',
`/api/resource/${docType}`,
body,
);
responseData = responseData.data;
} else if (operation === 'delete') {
// ----------------------------------
// document: delete
// ----------------------------------
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/delete_api_resource__DocType___DocumentName_
const docType = this.getNodeParameter('docType', i) as string;
const documentName = this.getNodeParameter('documentName', i) as string;
responseData = await erpNextApiRequest.call(
this,
'DELETE',
`/api/resource/${docType}/${documentName}`,
);
} else if (operation === 'update') {
// ----------------------------------
// document: update
// ----------------------------------
// https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/General/put_api_resource__DocType___DocumentName_
const properties = this.getNodeParameter('properties', i) as DocumentProperties;
if (!properties.customProperty.length) {
throw new NodeOperationError(
this.getNode(),
'Please enter at least one property for the document to update.',
{ itemIndex: i },
);
}
properties.customProperty.forEach((property) => {
body[property.field] = property.value;
});
const docType = this.getNodeParameter('docType', i) as string;
const documentName = this.getNodeParameter('documentName', i) as string;
responseData = await erpNextApiRequest.call(
this,
'PUT',
`/api/resource/${docType}/${documentName}`,
body,
);
responseData = responseData.data;
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
return [returnData];
}
}
@@ -0,0 +1,98 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IDataObject,
IHookFunctions,
IWebhookFunctions,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
/**
* Return the base API URL based on the user's environment.
*/
const getBaseUrl = ({ environment, domain, subdomain }: ERPNextApiCredentials) =>
environment === 'cloudHosted' ? `https://${subdomain}.${domain}` : domain;
export async function erpNextApiRequest(
this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
query: IDataObject = {},
uri?: string,
option: IDataObject = {},
) {
const credentials = await this.getCredentials<ERPNextApiCredentials>('erpNextApi');
const baseUrl = getBaseUrl(credentials);
let options: IRequestOptions = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method,
body,
qs: query,
uri: uri || `${baseUrl}${resource}`,
json: true,
rejectUnauthorized: !credentials.allowUnauthorizedCerts,
};
options = Object.assign({}, options, option);
if (!Object.keys(options.body as IDataObject).length) {
delete options.body;
}
if (!Object.keys(options.qs as IDataObject).length) {
delete options.qs;
}
try {
return await this.helpers.requestWithAuthentication.call(this, 'erpNextApi', options);
} catch (error) {
if (error.statusCode === 403) {
throw new NodeApiError(this.getNode(), { message: 'DocType unavailable.' });
}
if (error.statusCode === 307) {
throw new NodeApiError(this.getNode(), {
message: 'Please ensure the subdomain is correct.',
});
}
throw error;
}
}
export async function erpNextApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
resource: string,
body: IDataObject,
query: IDataObject = {},
) {
const returnData: any[] = [];
let responseData;
query.limit_start = 0;
query.limit_page_length = 1000;
do {
responseData = await erpNextApiRequest.call(this, method, resource, body, query);
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
query.limit_start += query.limit_page_length - 1;
} while (responseData.data && responseData.data.length > 0);
return returnData;
}
type ERPNextApiCredentials = {
apiKey: string;
apiSecret: string;
environment: 'cloudHosted' | 'selfHosted';
subdomain?: string;
domain?: string;
allowUnauthorizedCerts?: boolean;
};
@@ -0,0 +1,233 @@
{
"type": "object",
"properties": {
"annual_revenue": {
"type": "integer"
},
"base_opportunity_amount": {
"type": "integer"
},
"base_total": {
"type": "integer"
},
"company": {
"type": "string"
},
"contact_email": {
"type": "string"
},
"contact_person": {
"type": "string"
},
"conversion_rate": {
"type": "integer"
},
"country": {
"type": "string"
},
"creation": {
"type": "string"
},
"currency": {
"type": "string"
},
"customer_name": {
"type": "string"
},
"disabled": {
"type": "integer"
},
"docstatus": {
"type": "integer"
},
"doctype": {
"type": "string"
},
"idx": {
"type": "integer"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"__unsaved": {
"type": "integer"
},
"actual_qty": {
"type": "integer"
},
"additional_cost": {
"type": "integer"
},
"allow_alternative_item": {
"type": "integer"
},
"allow_zero_valuation_rate": {
"type": "integer"
},
"amount": {
"type": "number"
},
"barcode": {
"type": "string"
},
"basic_amount": {
"type": "number"
},
"basic_rate": {
"type": "number"
},
"conversion_factor": {
"type": "integer"
},
"cost_center": {
"type": "string"
},
"creation": {
"type": "string"
},
"description": {
"type": "string"
},
"docstatus": {
"type": "integer"
},
"doctype": {
"type": "string"
},
"expense_account": {
"type": "string"
},
"has_item_scanned": {
"type": "integer"
},
"idx": {
"type": "integer"
},
"is_finished_item": {
"type": "integer"
},
"is_scrap_item": {
"type": "integer"
},
"item_code": {
"type": "string"
},
"item_group": {
"type": "string"
},
"item_name": {
"type": "string"
},
"modified": {
"type": "string"
},
"modified_by": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"parent": {
"type": "string"
},
"parentfield": {
"type": "string"
},
"parenttype": {
"type": "string"
},
"qty": {
"type": "integer"
},
"rate": {
"type": "integer"
},
"retain_sample": {
"type": "integer"
},
"sample_quantity": {
"type": "integer"
},
"set_basic_rate_manually": {
"type": "integer"
},
"stock_uom": {
"type": "string"
},
"t_warehouse": {
"type": "string"
},
"transfer_qty": {
"type": "integer"
},
"transferred_qty": {
"type": "integer"
},
"uom": {
"type": "string"
},
"use_serial_batch_fields": {
"type": "integer"
},
"valuation_rate": {
"type": "number"
}
}
}
},
"language": {
"type": "string"
},
"modified": {
"type": "string"
},
"modified_by": {
"type": "string"
},
"name": {
"type": "string"
},
"naming_series": {
"type": "string"
},
"no_of_employees": {
"type": "string"
},
"opportunity_amount": {
"type": "integer"
},
"opportunity_from": {
"type": "string"
},
"opportunity_type": {
"type": "string"
},
"owner": {
"type": "string"
},
"party_name": {
"type": "string"
},
"probability": {
"type": "integer"
},
"status": {
"type": "string"
},
"title": {
"type": "string"
},
"total": {
"type": "integer"
},
"transaction_date": {
"type": "string"
}
},
"version": 3
}
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"creation": {
"type": "string"
},
"docstatus": {
"type": "integer"
},
"doctype": {
"type": "string"
},
"idx": {
"type": "integer"
},
"modified": {
"type": "string"
},
"modified_by": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path fill="#7574FF" d="M512 448c0 35.2-28.8 64-64 64H64c-35.2 0-64-28.8-64-64V64C0 28.8 28.8 0 64 0h384c35.2 0 64 28.8 64 64z"/><path fill="#FFF" d="M150.483 371.684V141.15c0-15.167 9.534-25.133 23.833-25.133h162.5c13.866 0 20.8 6.933 20.8 18.633v2.6c0 12.133-6.934 18.633-20.8 18.633h-141.7v78.434H304.75c14.3 0 20.8 6.066 20.8 17.767v1.3c0 12.133-6.934 18.633-20.8 18.633H195.117v84.934h144.3c13.867 0 20.367 6.066 20.367 17.767v2.167c0 12.566-6.5 19.5-20.367 19.5h-165.1c-14.3-.001-23.834-9.534-23.834-24.701"/></svg>

After

Width:  |  Height:  |  Size: 586 B

@@ -0,0 +1,28 @@
import flow from 'lodash/flow';
import sortBy from 'lodash/sortBy';
import uniqBy from 'lodash/uniqBy';
export type DocumentProperties = {
customProperty: Array<{ field: string; value: string }>;
};
type DocFields = Array<{ name: string; value: string }>;
const ensureName = (docFields: DocFields) => docFields.filter((o) => o.name);
const sortByName = (docFields: DocFields) => sortBy(docFields, ['name']);
const uniqueByName = (docFields: DocFields) => uniqBy(docFields, (o) => o.name);
export const processNames = flow(ensureName, sortByName, uniqueByName);
export const toSQL = (operator: string) => {
const operators: { [key: string]: string } = {
is: '=',
isNot: '!=',
greater: '>',
less: '<',
equalsGreater: '>=',
equalsLess: '<=',
};
return operators[operator];
};