@integration-app/sdk
Advanced tools
Comparing version 1.2.3 to 1.2.4
{ | ||
"name": "@integration-app/sdk", | ||
"version": "1.2.3", | ||
"version": "1.2.4", | ||
"description": "JavaScript SDK for Integration.app", | ||
@@ -5,0 +5,0 @@ "author": "Integration.app", |
@@ -41,2 +41,6 @@ import qs from 'qs' | ||
getUniqueIdentifier() { | ||
return this.getPath() | ||
} | ||
async get(): Promise<Element> { | ||
@@ -144,2 +148,6 @@ return this.options.client.get(this.getPath()) | ||
getUniqueIdentifier() { | ||
return this.getPath() | ||
} | ||
async get(): Promise<ElementInstance> { | ||
@@ -146,0 +154,0 @@ return this.options.client.get(this.getPath()) |
@@ -20,2 +20,4 @@ import { FieldMappingDirection } from '../field-mappings' | ||
RunJavascript = 'run-javascript', | ||
/** | ||
@@ -110,2 +112,6 @@ * @deprecated | ||
}, | ||
[ActionType.RunJavascript]: { | ||
name: 'Run Javascript', | ||
description: 'Run custom Javascript code', | ||
}, | ||
[ActionType.ApiRequest]: { | ||
@@ -112,0 +118,0 @@ name: 'API Request', |
@@ -16,3 +16,7 @@ import { ConnectorSpec } from '.' | ||
export type ConnectorAuthType = typeof CONNECTOR_AUTH_TYPES[number] | ||
export type ConnectorAuthOptions = { key: string; title?: string }[] | ||
export type ConnectorAuthOptions = { | ||
key: string | ||
title?: string | ||
type: ConnectorAuthType | ||
}[] | ||
@@ -19,0 +23,0 @@ interface ConnectorAuthMethodArgs { |
@@ -60,4 +60,5 @@ import { HttpRequestMethod } from '../http-requests' | ||
binary?: boolean | ||
stream?: boolean | ||
dynamicOptions?: (input: RestApiClientInput) => RestApiClientOptions | ||
onError?: (error: any) => any | ||
} |
@@ -41,3 +41,2 @@ import { DataSchema } from '../data-schema' | ||
hasAuth?: boolean | ||
hasReadme?: boolean | ||
@@ -44,0 +43,0 @@ hasDefaultParameters?: boolean |
import { | ||
getValueByLocator, | ||
getVariableLocators, | ||
removeEmptyFields, | ||
processValue, | ||
transformVars, | ||
} from './formula-processors' | ||
describe('processValue', () => { | ||
it('should return original value if there are no formulas', () => { | ||
expect(processValue('value', undefined)).toEqual('value') | ||
expect(processValue(5, undefined)).toEqual(5) | ||
expect(processValue(true, undefined)).toEqual(true) | ||
expect(processValue([1], undefined)).toEqual([1]) | ||
expect(processValue({ key: 'value' }, undefined)).toEqual({ key: 'value' }) | ||
// Empty-ish values - should not erase them | ||
expect(processValue('', undefined)).toEqual('') | ||
expect(processValue(0, undefined)).toEqual(0) | ||
expect(processValue(false, undefined)).toEqual(false) | ||
expect(processValue([], undefined)).toEqual([]) | ||
expect(processValue({}, undefined)).toEqual({}) | ||
}) | ||
it('should remove values if formula was resolved to nothing', () => { | ||
expect(processValue({ $var: 'nonExistentField' }, undefined)).toEqual( | ||
undefined, | ||
) | ||
expect(processValue([{ $var: 'nonExistentField' }], undefined)).toEqual( | ||
undefined, | ||
) | ||
expect( | ||
processValue({ key: { $var: 'nonExistentField' } }, 'value'), | ||
).toEqual(undefined) | ||
}) | ||
}) | ||
describe('getValueByLocator', () => { | ||
@@ -31,49 +60,2 @@ it('should return value at object property locator', () => { | ||
describe('removeEmptyFields', () => { | ||
it('should work with empty value', () => { | ||
expect(removeEmptyFields(undefined)).toEqual(undefined) | ||
expect(removeEmptyFields(null)).toEqual(null) | ||
}) | ||
it('should work with simple data', () => { | ||
expect(removeEmptyFields(0)).toEqual(0) | ||
expect(removeEmptyFields(1)).toEqual(1) | ||
expect(removeEmptyFields('')).toEqual('') | ||
expect(removeEmptyFields('a')).toEqual('a') | ||
expect(removeEmptyFields(true)).toEqual(true) | ||
expect(removeEmptyFields(false)).toEqual(false) | ||
expect(removeEmptyFields({ a: 'b' })).toEqual({ a: 'b' }) | ||
expect(removeEmptyFields([1])).toEqual([1]) | ||
}) | ||
it('should work with nested datas', () => { | ||
const value = { | ||
list: [ | ||
{ | ||
name: 'a', | ||
}, | ||
], | ||
} | ||
expect(removeEmptyFields(value)).toEqual(value) | ||
}) | ||
it('should remove empty lists from value', () => { | ||
expect(removeEmptyFields([])).toEqual(undefined) | ||
expect(removeEmptyFields({ a: [] })).toEqual(undefined) | ||
expect(removeEmptyFields({ a: [], b: 1 })).toEqual({ b: 1 }) | ||
expect(removeEmptyFields({ a: [[]], b: 1 })).toEqual({ b: 1 }) | ||
}) | ||
it('should remove empty objects', () => { | ||
expect(removeEmptyFields({})).toEqual(undefined) | ||
expect(removeEmptyFields({ a: {} })).toEqual(undefined) | ||
expect(removeEmptyFields({ a: {}, b: 1 })).toEqual({ b: 1 }) | ||
expect(removeEmptyFields({ a: [{}], b: 1 })).toEqual({ b: 1 }) | ||
}) | ||
it('should deep remove empty values', () => { | ||
expect(removeEmptyFields({ a: [{ b: [{}] }] })).toEqual(undefined) | ||
}) | ||
}) | ||
describe('transformVars', () => { | ||
@@ -80,0 +62,0 @@ it('should transform $var formulas', () => { |
@@ -45,2 +45,4 @@ import { BadRequestError, ConfigurationError } from '../errors' | ||
if (Array.isArray(value)) { | ||
if (!value?.length) return value | ||
const result = [] | ||
@@ -59,2 +61,4 @@ for (const valueItem of value) { | ||
} else { | ||
if (Object.keys(value).length === 0) return value | ||
const result = {} | ||
@@ -222,36 +226,2 @@ for (const [key, val] of Object.entries(value)) { | ||
export function removeEmptyFields(value: any): any { | ||
if (typeof value === 'object') { | ||
if (Array.isArray(value)) { | ||
const result = [] | ||
for (const valueItem of value) { | ||
const processedVal = removeEmptyFields(valueItem) | ||
if (processedVal !== undefined) { | ||
/* FIXME: strictNullCheck temporary fix */ | ||
// @ts-expect-error TS(2345): Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message | ||
result.push(processedVal) | ||
} | ||
} | ||
return result.length > 0 ? result : undefined | ||
} else if (value === null) { | ||
return value | ||
} else { | ||
const result = {} | ||
for (const [key, val] of Object.entries(value)) { | ||
const processedVal = removeEmptyFields(val) | ||
if (processedVal !== undefined) { | ||
result[key] = processedVal | ||
} | ||
} | ||
if (Object.keys(result).length > 0) { | ||
return result | ||
} else { | ||
return undefined | ||
} | ||
} | ||
} else { | ||
return value | ||
} | ||
} | ||
function executeLocatorStepObjectProperty( | ||
@@ -258,0 +228,0 @@ value: any, |
@@ -41,2 +41,15 @@ import { Formula } from '.' | ||
export function hasFormulas(value) { | ||
if (isFormula(value)) { | ||
return true | ||
} | ||
if (Array.isArray(value)) { | ||
return value.some((v) => hasFormulas(v)) | ||
} else if (typeof value === 'object' && value !== null) { | ||
return Object.values(value).some((v) => hasFormulas(v)) | ||
} else { | ||
return false | ||
} | ||
} | ||
export function getFormula(value): Formula.Base { | ||
@@ -43,0 +56,0 @@ if (typeof value === 'object' && value !== null) { |
@@ -1,2 +0,2 @@ | ||
import { buildData } from '.' | ||
import { buildData, hasFormulas } from '.' | ||
@@ -199,4 +199,2 @@ describe('DataBuilder', () => { | ||
it('should skip objects without fields', () => { | ||
expect(buildData({})).toEqual(undefined) | ||
expect( | ||
@@ -225,4 +223,2 @@ buildData({ | ||
it('should skip arrays without values', () => { | ||
expect(buildData([])).toEqual(undefined) | ||
expect( | ||
@@ -254,1 +250,35 @@ buildData({ | ||
}) | ||
describe('hasFormulas', () => { | ||
it('should return false for values without formulas', () => { | ||
expect(hasFormulas('value')).toBe(false) | ||
expect(hasFormulas(42)).toBe(false) | ||
expect(hasFormulas(true)).toBe(false) | ||
expect(hasFormulas(null)).toBe(false) | ||
expect(hasFormulas(undefined)).toBe(false) | ||
expect(hasFormulas([])).toBe(false) | ||
expect(hasFormulas({})).toBe(false) | ||
expect(hasFormulas({ key: 'value' })).toBe(false) | ||
}) | ||
it('should return true for values that are formulas', () => { | ||
expect(hasFormulas({ $var: 'value' })).toBe(true) | ||
expect( | ||
hasFormulas({ $lookup: { uri: 'uri', query: { field: 'value' } } }), | ||
).toBe(true) | ||
expect(hasFormulas({ $map: { value: 'value', mapping: [] } })).toBe(true) | ||
}) | ||
it('should return true for values inside object keys', () => { | ||
expect(hasFormulas({ key: { $var: 'value' } })).toBe(true) | ||
}) | ||
it('should return true for values inside array items', () => { | ||
expect(hasFormulas([{ $var: 'value' }])).toBe(true) | ||
}) | ||
it('should return true for nested formulas', () => { | ||
expect(hasFormulas({ key: { subKey: { $var: 'value' } } })).toBe(true) | ||
expect(hasFormulas({ key: { subKey: [{ $var: 'value' }] } })).toBe(true) | ||
}) | ||
}) |
@@ -14,2 +14,4 @@ import { ErrorData } from '../errors' | ||
outputFileUri?: string | ||
errors?: ErrorData[] | ||
@@ -55,2 +57,4 @@ } | ||
outputFileUri?: string | ||
errors: ErrorData[] | ||
@@ -57,0 +61,0 @@ } |
import { ErrorData } from '../errors' | ||
import { PaginationResponse, PaginationQuery } from '../entity-repository' | ||
import { User } from '../users' | ||
import { Customer } from '../users' | ||
import { FlowInstance } from '../flows' | ||
@@ -10,3 +10,2 @@ import { Connection } from '../connections' | ||
export * from './accessors' | ||
export * from './api' | ||
@@ -57,3 +56,3 @@ export enum FlowRunState { | ||
user?: User | ||
user?: Customer | ||
@@ -118,2 +117,2 @@ input?: any | ||
export class FindFlowRunsResponse extends PaginationResponse<FlowRun> {} | ||
export interface FindFlowRunsResponse extends PaginationResponse<FlowRun> {} |
@@ -6,11 +6,1 @@ export * from './accessors' | ||
export * from './utils' | ||
export { | ||
getChildNodeKeys, | ||
getDownstreamNodeKeys, | ||
getFlowNode, | ||
getOrderedNodeKeys, | ||
getParentNodeKeys, | ||
getRootNodeKeys, | ||
getUpstreamNodeKeys, | ||
} from './utils' |
import { FieldMappingDirection } from '../../field-mappings' | ||
import { FlowNode } from '../types' | ||
@@ -15,2 +16,3 @@ /* FIXME: temporary fix to pass eslint test */ | ||
version?: number | ||
getSubFlowRootNodeKey?: (node: FlowNode) => string | ||
} | ||
@@ -17,0 +19,0 @@ |
@@ -6,2 +6,3 @@ import { FlowNodeSpec } from './base' | ||
description: 'Execute one or more steps for each item on a list.', | ||
getSubFlowRootNodeKey: (node) => node.config?.rootNodeKey, | ||
}) |
import { Flow, FlowNodeType, hasCycles } from '.' | ||
import { getOrderedNodeKeys, getParentNodeKeys } from './utils' | ||
import { getParentNodeKeys } from './utils' | ||
@@ -37,83 +37,2 @@ describe('flows/utils', () => { | ||
describe('getOrderedNodeKeys', () => { | ||
it('should return nodes in order taking dependencies in account', () => { | ||
// Structure: | ||
// root-1 root 2 | ||
// | | | ||
// | | | ||
// | child-1 | ||
// | / | \ | ||
// | / | \ | ||
// child-2-1 child-2-2 \ | ||
// | | \ \ | ||
// | | \ \ | ||
// child-3-1 child-3-2 child-3-3 | ||
const flow = { | ||
nodes: { | ||
'root-1': { | ||
links: [ | ||
{ | ||
key: 'child-2-1', | ||
}, | ||
], | ||
}, | ||
'root-2': { | ||
links: [ | ||
{ | ||
key: 'child-1', | ||
}, | ||
], | ||
}, | ||
'child-1': { | ||
links: [ | ||
{ | ||
key: 'child-2-1', | ||
}, | ||
{ | ||
key: 'child-2-2', | ||
}, | ||
{ | ||
key: 'child-3-3', | ||
}, | ||
], | ||
}, | ||
'child-2-1': { | ||
links: [ | ||
{ | ||
key: 'child-3-1', | ||
}, | ||
], | ||
}, | ||
'child-2-2': { | ||
links: [ | ||
{ | ||
key: 'child-3-2', | ||
}, | ||
{ | ||
key: 'child-3-3', | ||
}, | ||
], | ||
}, | ||
'child-3-1': {}, | ||
'child-3-2': {}, | ||
'child-3-3': {}, | ||
}, | ||
} | ||
const keysInOrder = [ | ||
'root-1', | ||
'root-2', | ||
'child-1', | ||
'child-2-1', | ||
'child-2-2', | ||
'child-3-3', | ||
'child-3-1', | ||
'child-3-2', | ||
] | ||
expect(getOrderedNodeKeys(flow as any as Flow)).toEqual(keysInOrder) | ||
}) | ||
}) | ||
describe('hasCycles', () => { | ||
@@ -193,2 +112,20 @@ it('should return false for an empty graph', () => { | ||
it('should return false for a flow with a nodes that is entered twice', () => { | ||
const nodes = { | ||
a: { | ||
type: 'type1', | ||
links: [{ key: 'b' }, { key: 'c' }], | ||
}, | ||
b: { | ||
type: 'type2', | ||
links: [{ key: 'c' }], | ||
}, | ||
c: { | ||
type: 'type3', | ||
}, | ||
} | ||
expect(hasCycles(nodes)).toBe(false) | ||
}) | ||
it('should return true for a flow with a cycle', () => { | ||
@@ -266,3 +203,47 @@ const nodes = { | ||
}) | ||
it('should return true if a node is used inside forEach and after forEach', () => { | ||
const nodes = { | ||
trigger: { | ||
type: FlowNodeType.ApiTrigger, | ||
links: [{ key: 'forEach' }], | ||
}, | ||
forEach: { | ||
type: FlowNodeType.ForEachV2, | ||
config: { | ||
rootNodeKey: 'insideForEach', | ||
}, | ||
links: [{ key: 'insideForEach' }], | ||
}, | ||
insideForEach: { | ||
type: FlowNodeType.TransformData, | ||
links: [], | ||
}, | ||
} | ||
expect(hasCycles(nodes)).toBe(true) | ||
}) | ||
it('should return false for a correct flow with forEach', () => { | ||
const nodes = { | ||
trigger: { | ||
type: FlowNodeType.ApiTrigger, | ||
links: [{ key: 'forEach' }], | ||
}, | ||
forEach: { | ||
type: FlowNodeType.ForEachV2, | ||
config: { | ||
rootNodeKey: 'insideForEach', | ||
}, | ||
links: [], | ||
}, | ||
insideForEach: { | ||
type: FlowNodeType.TransformData, | ||
links: [], | ||
}, | ||
} | ||
expect(hasCycles(nodes)).toBe(false) | ||
}) | ||
}) | ||
}) |
@@ -63,8 +63,27 @@ import { toHeaderCase } from 'js-convert-case' | ||
*/ | ||
export function getRootNodeKeys(flow: Flow | FlowInstance): string[] { | ||
const allNodeKeys = Object.keys(flow.nodes ?? {}) | ||
const allLinkKeys = Object.values(flow.nodes ?? {}) | ||
export function getRootNodeKeys({ | ||
nodes, | ||
}: { | ||
nodes?: Record<string, FlowNode> | ||
}): string[] { | ||
const allNodeKeys = Object.keys(nodes ?? {}) | ||
const allLinkKeys = Object.values(nodes ?? {}) | ||
.flatMap((n) => n.links?.map((l) => l.key) ?? []) | ||
.filter(Boolean) | ||
return allNodeKeys.filter((k) => !allLinkKeys.includes(k)) | ||
const allSubFlowRootNodeKeys: string[] = [] | ||
for (const nodeKey of allNodeKeys) { | ||
const node = nodes?.[nodeKey] | ||
if (node && FLOW_NODE_SPECS[node.type]?.getSubFlowRootNodeKey) { | ||
const subFlowRootNodeKey = | ||
FLOW_NODE_SPECS[node.type].getSubFlowRootNodeKey(node) | ||
if (subFlowRootNodeKey) { | ||
allSubFlowRootNodeKeys.push(subFlowRootNodeKey) | ||
} | ||
} | ||
} | ||
const allNonRootNodeKeys = allLinkKeys.concat(allSubFlowRootNodeKeys) | ||
return allNodeKeys.filter((k) => !allNonRootNodeKeys.includes(k)) | ||
} | ||
@@ -135,35 +154,2 @@ | ||
/** | ||
* Returns keys of flow nodes in order of execution. | ||
* If order of execution is not possible to determine (i.e. two triggers) - the returned order is arbitrary. | ||
* | ||
* This function allows executing operations on nodes that require upstream nodes to be processed first, | ||
* such as node setup. | ||
*/ | ||
export function getOrderedNodeKeys(flow: Flow | FlowInstance): string[] { | ||
const rootNodeKeys = getRootNodeKeys(flow) | ||
const orderedNodeKeys = rootNodeKeys | ||
let moreIterations = true | ||
// Going layer after layer and adding children | ||
// but only if all of their parents are already in the list | ||
while (moreIterations) { | ||
moreIterations = false | ||
for (const nodeKey of orderedNodeKeys) { | ||
for (const childNodeKey of getChildNodeKeys(flow, nodeKey)) { | ||
if (!orderedNodeKeys.includes(childNodeKey)) { | ||
const parentNodeKeys = getParentNodeKeys(flow, childNodeKey) | ||
if (parentNodeKeys.every((k) => orderedNodeKeys.includes(k))) { | ||
orderedNodeKeys.push(childNodeKey) | ||
moreIterations = true | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return orderedNodeKeys | ||
} | ||
/** | ||
* Detects cycles in the flow. | ||
@@ -176,48 +162,58 @@ */ | ||
const visited = new Set<string>() | ||
const stack = new Set<string>() | ||
let hasCycles = false | ||
const allVisited = new Set<string>() | ||
for (const nodeKey in nodes) { | ||
if (dfs(nodeKey, visited, stack, nodes)) { | ||
return true | ||
function visitNode(nodeKey: string, visited: string[]): string[] { | ||
if (!nodeKey) return visited | ||
if (visited.includes(nodeKey)) { | ||
hasCycles = true | ||
return visited | ||
} | ||
} | ||
return false | ||
} | ||
const node = nodes?.[nodeKey] | ||
if (!node) return visited | ||
/** | ||
* Depth-first search | ||
*/ | ||
function dfs( | ||
nodeKey: string, | ||
visited: Set<string>, | ||
stack: Set<string>, | ||
nodes: Record<string, FlowNode>, | ||
): boolean { | ||
if (stack.has(nodeKey)) { | ||
return true | ||
} | ||
visited = [...visited, nodeKey] | ||
allVisited.add(nodeKey) | ||
if (visited.has(nodeKey)) { | ||
return false | ||
} | ||
if (FLOW_NODE_SPECS[node?.type]?.getSubFlowRootNodeKey) { | ||
visited = [ | ||
...visited, | ||
...visitNode( | ||
FLOW_NODE_SPECS[node.type].getSubFlowRootNodeKey(node), | ||
visited, | ||
), | ||
] | ||
} | ||
visited.add(nodeKey) | ||
stack.add(nodeKey) | ||
const node = nodes[nodeKey] | ||
if (node && node.links) { | ||
for (const link of node.links) { | ||
/* FIXME: strictNullCheck temporary fix */ | ||
// @ts-expect-error TS(2345): Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message | ||
if (dfs(link.key, visited, stack, nodes)) { | ||
return true | ||
if (node && node.links) { | ||
let visitedSubNodes: string[] = [] | ||
for (const link of node.links) { | ||
if (link.key) { | ||
visitedSubNodes = [ | ||
...visitedSubNodes, | ||
...visitNode(link.key, visited), | ||
] | ||
} | ||
} | ||
visited = [...visited, ...Array.from(new Set(visitedSubNodes))] | ||
} | ||
return visited | ||
} | ||
stack.delete(nodeKey) | ||
const rootNodeKeys = getRootNodeKeys({ nodes }) | ||
return false | ||
for (const nodeKey of rootNodeKeys) { | ||
visitNode(nodeKey, []) | ||
} | ||
if (allVisited.size !== Object.keys(nodes).length) { | ||
// If we didn't visit all the nodes, it's because we couldn't find all the root nodes | ||
// due to cycles. | ||
hasCycles = true | ||
} | ||
return hasCycles | ||
} |
@@ -24,2 +24,6 @@ import { IntegrationAppApiClient } from '../api-client' | ||
getUniqueIdentifier() { | ||
return this.endpoint | ||
} | ||
async get(query: { autoCreate?: boolean } = {}): Promise<Element> { | ||
@@ -26,0 +30,0 @@ return this.client.get(this.uri('', query)) |
@@ -15,2 +15,6 @@ import { IntegrationAppApiClient } from '../api-client' | ||
getUniqueIdentifier() { | ||
return this.endpoint | ||
} | ||
async get(): Promise<Element> { | ||
@@ -17,0 +21,0 @@ return this.client.get(this.endpoint) |
@@ -77,4 +77,2 @@ import { Connection } from '../connections' | ||
hasUdm?: boolean | ||
hasAuth?: boolean | ||
} | ||
@@ -81,0 +79,0 @@ |
@@ -23,2 +23,3 @@ import { structuredClone } from '../_helper' | ||
import deals from './deals' | ||
import dealStages from './deal-stages' | ||
import documents from './documents' | ||
@@ -68,2 +69,3 @@ import drives from './drives' | ||
DEALS = 'deals', | ||
DEAL_STAGES = 'deal-stages', | ||
DOCUMENTS = 'documents', | ||
@@ -112,2 +114,3 @@ DRIVES = 'drives', | ||
[UDM.DEALS]: deals, | ||
[UDM.DEAL_STAGES]: dealStages, | ||
[UDM.DOCUMENTS]: documents, | ||
@@ -114,0 +117,0 @@ [UDM.DRIVES]: drives, |
@@ -85,3 +85,12 @@ import { CreateActionRequest } from '../actions' | ||
} | ||
export interface UserWorkspaceSettings { | ||
id: string | ||
userId: string | ||
workspaceId: string | ||
testCustomerId: string | ||
} | ||
export interface WorkspaceElements { | ||
@@ -88,0 +97,0 @@ flows?: Record<string, CreateFlowRequest> |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
4930725
79790