Skip to content

Commit 1b1a2ef

Browse files
committed
feat: add find contact action
1 parent 67688d5 commit 1b1a2ef

File tree

5 files changed

+258
-20
lines changed

5 files changed

+258
-20
lines changed

apps/runner/src/services/runner.service.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,26 @@ export class RunnerService {
459459

460460
let inputs: Record<string, unknown>
461461
try {
462+
// add contact info on-demand
462463
if (previousOutputs.contact?.address) {
463464
previousOutputs.contact = await this.addRequestedContactDetails(
464465
previousOutputs.contact,
465466
workflowAction.inputs,
466467
user,
467468
)
468469
}
470+
for (const [key, value] of Object.entries(previousOutputs)) {
471+
if ((value.contact as any)?.address && !['trigger', 'contact'].includes(key)) {
472+
previousOutputs[key].contact = await this.addRequestedContactDetails(
473+
previousOutputs[key].contact as any,
474+
workflowAction.inputs,
475+
user,
476+
`${key}.contact`,
477+
)
478+
}
479+
}
480+
481+
// add menu info on-demand
469482
if (workflow.type === 'chatbot') {
470483
previousOutputs.menu = await this.addRequestedMenuDetails(
471484
previousOutputs.menu ?? {},
@@ -897,8 +910,13 @@ export class RunnerService {
897910
}
898911
}
899912

900-
async addRequestedContactDetails(contact: Record<string, any>, inputs: Record<string, unknown>, user: User) {
901-
const contactKeys = findOutputKeys(inputs, 'contact')
913+
async addRequestedContactDetails(
914+
contact: Record<string, any>,
915+
inputs: Record<string, unknown>,
916+
user: User,
917+
inputsKey: string = 'contact',
918+
) {
919+
const contactKeys = findOutputKeys(inputs, inputsKey)
902920
for (const key of contactKeys) {
903921
if (!contact[key]) {
904922
contact[key] = await this.contactService.resolveContactData(contact.address, key, user)

libs/definitions/src/definition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface RunResponse {
6262
cursor?: string // cursor for pagination when fetching all items
6363
credits?: number // number of credits used
6464
learnResponseWorkflow?: boolean
65+
contact?: string
6566
}
6667

6768
export type GetAsyncSchemasProps = OperationRunOptions & {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { RunResponse } from '@app/definitions/definition'
2+
import { OperationOffChain } from '@app/definitions/opertion-offchain'
3+
import { BadRequestException } from '@nestjs/common'
4+
import { ContactService } from 'apps/api/src/chat/services/contact.service'
5+
import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
6+
import { isAddress } from 'ethers/lib/utils'
7+
import { JSONSchema7 } from 'json-schema'
8+
import { expressionsToQuery, LogicExpression } from '../../logic/logic.common'
9+
10+
export class FindContactAction extends OperationOffChain {
11+
key = 'findContact'
12+
name = 'Find a contact'
13+
description = 'Find a contact matching the given filters'
14+
version = '1.0.0'
15+
16+
inputs: JSONSchema7 = {
17+
required: ['filter'],
18+
properties: {
19+
filter: {
20+
title: 'Filter',
21+
$ref: '#/$defs/OrFilters',
22+
},
23+
},
24+
$defs: {
25+
Filter: {
26+
type: 'object',
27+
required: ['leftValue', 'comparator'],
28+
properties: {
29+
leftValue: {
30+
title: 'Field',
31+
type: 'string',
32+
enum: ['address', 'tags'],
33+
enumNames: ['Address', 'Tags'],
34+
},
35+
},
36+
allOf: [
37+
{
38+
if: {
39+
properties: {
40+
leftValue: { const: 'address' },
41+
},
42+
},
43+
then: {
44+
properties: {
45+
comparator: {
46+
title: 'Comparator',
47+
type: 'string',
48+
oneOf: [
49+
{ title: 'Equals', const: '=' },
50+
{ title: 'Not Equal', const: '!=' },
51+
],
52+
default: '=',
53+
},
54+
rightValue: { title: 'Value', type: 'string' },
55+
},
56+
},
57+
},
58+
{
59+
if: {
60+
properties: {
61+
leftValue: { const: 'tags' },
62+
},
63+
},
64+
then: {
65+
properties: {
66+
comparator: {
67+
title: 'Comparator',
68+
type: 'string',
69+
oneOf: [
70+
{ title: 'Contains', const: 'contains' },
71+
{ title: 'Not Contain', const: '!contains' },
72+
],
73+
default: 'contains',
74+
},
75+
rightValue: { title: 'Value', type: 'string' },
76+
},
77+
},
78+
},
79+
],
80+
},
81+
AndFilters: {
82+
type: 'array',
83+
items: { $ref: '#/$defs/Filter' },
84+
minItems: 1,
85+
'x-addLabel': 'And',
86+
},
87+
OrFilters: {
88+
type: 'array',
89+
items: { $ref: '#/$defs/AndFilters' },
90+
minItems: 1,
91+
'x-addLabel': 'Or',
92+
},
93+
},
94+
} as JSONSchema7
95+
outputs: JSONSchema7 = {
96+
properties: {
97+
contact: {
98+
type: 'object',
99+
'x-type': 'contact',
100+
} as JSONSchema7,
101+
},
102+
}
103+
104+
async run({ inputs, user }: OperationRunOptions): Promise<RunResponse> {
105+
if (!Array.isArray(inputs.filter)) {
106+
throw new BadRequestException('Invalid filter')
107+
}
108+
const orFilters = inputs.filter as LogicExpression[][]
109+
110+
// validate filters
111+
for (const orFilter of orFilters) {
112+
for (const andFilter of orFilter) {
113+
if (!['address', 'tags'].includes(andFilter.leftValue)) {
114+
throw new BadRequestException(`Invalid filter left value: ${andFilter.leftValue}`)
115+
}
116+
if (andFilter.leftValue === 'address') {
117+
if (!isAddress(andFilter.rightValue)) {
118+
throw new BadRequestException('The address provided is invalid')
119+
}
120+
if (!['=', '!='].includes(andFilter.comparator)) {
121+
throw new BadRequestException(`Invalid address filter comparator: ${andFilter.comparator}`)
122+
}
123+
}
124+
if (andFilter.leftValue === 'tags') {
125+
if (!['contains', '!contains'].includes(andFilter.comparator)) {
126+
throw new BadRequestException(`Invalid tag filter comparator: ${andFilter.comparator}`)
127+
}
128+
}
129+
}
130+
}
131+
132+
const query = expressionsToQuery(inputs.filter)
133+
const contact = await ContactService.instance.findOne({ ...query, owner: user })
134+
135+
if (contact) {
136+
return {
137+
outputs: {
138+
contact: {
139+
address: contact.address,
140+
tags: contact.tags,
141+
},
142+
},
143+
}
144+
}
145+
return {
146+
outputs: {
147+
contact: {},
148+
},
149+
}
150+
}
151+
}

libs/definitions/src/integration-definitions/contacts/contacts.definition.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SingleIntegrationDefinition } from '@app/definitions/single-integration.definition'
22
import { AddTagContactAction } from './actions/add-tag-contact.action'
33
import { CreateContactAction } from './actions/create-contact.action copy'
4+
import { FindContactAction } from './actions/find-contact.action'
45
import { RemoveTagContactAction } from './actions/remove-tag-contact.action'
56
import { ContactTaggedTrigger } from './triggers/contact-tagged.trigger'
67
import { NewContactTrigger } from './triggers/new-contact.trigger'
@@ -11,5 +12,10 @@ export class ContactsDefinition extends SingleIntegrationDefinition {
1112
schemaUrl = null
1213

1314
triggers = [new NewContactTrigger(), new ContactTaggedTrigger()]
14-
actions = [new CreateContactAction(), new AddTagContactAction(), new RemoveTagContactAction()]
15+
actions = [
16+
new CreateContactAction(),
17+
new AddTagContactAction(),
18+
new RemoveTagContactAction(),
19+
new FindContactAction(),
20+
]
1521
}

libs/definitions/src/integration-definitions/logic/logic.common.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ export interface LogicExpression {
44
rightValue: any
55
}
66

7+
export const LOGIC_COMPARATORS = [
8+
{ title: 'Equals', 'x-const': '=' },
9+
{ title: 'Not Equal', 'x-const': '!=' },
10+
{ title: 'Contains', 'x-const': 'contains' },
11+
{ title: 'Not Contain', 'x-const': '!contains' },
12+
{ title: 'Starts With', 'x-const': 'startsWith' },
13+
{ title: 'Not Start With', 'x-const': '!startsWith' },
14+
{ title: 'Ends With', 'x-const': 'endsWith' },
15+
{ title: 'Not End With', 'x-const': '!endsWith' },
16+
{ title: 'Greather Than', 'x-const': '>' },
17+
{ title: 'Greather or Equal Than', 'x-const': '>=' },
18+
{ title: 'Less Than', 'x-const': '<' },
19+
{ title: 'Less or Equal Than', 'x-const': '<=' },
20+
{ title: 'Text Length Greather Than', 'x-const': 'length>' },
21+
{ title: 'Text Length Less Than', 'x-const': 'length<' },
22+
]
23+
724
export const LOGIC_FIELD_DEFS = {
825
Expression: {
926
type: 'object',
@@ -13,22 +30,7 @@ export const LOGIC_FIELD_DEFS = {
1330
comparator: {
1431
title: 'Comparator',
1532
type: 'string',
16-
oneOf: [
17-
{ title: 'Equals', 'x-const': '=' },
18-
{ title: 'Not Equal', 'x-const': '!=' },
19-
{ title: 'Contains', 'x-const': 'contains' },
20-
{ title: 'Not Contain', 'x-const': '!contains' },
21-
{ title: 'Starts With', 'x-const': 'startsWith' },
22-
{ title: 'Not Start With', 'x-const': '!startsWith' },
23-
{ title: 'Ends With', 'x-const': 'endsWith' },
24-
{ title: 'Not End With', 'x-const': '!endsWith' },
25-
{ title: 'Greather Than', 'x-const': '>' },
26-
{ title: 'Greather or Equal Than', 'x-const': '>=' },
27-
{ title: 'Less Than', 'x-const': '<' },
28-
{ title: 'Less or Equal Than', 'x-const': '<=' },
29-
{ title: 'Text Length Greather Than', 'x-const': 'length>' },
30-
{ title: 'Text Length Less Than', 'x-const': 'length<' },
31-
],
33+
oneOf: LOGIC_COMPARATORS,
3234
},
3335
rightValue: { title: 'Value', type: 'string' },
3436
},
@@ -80,6 +82,66 @@ export function logicEvaluateNotNegatedCondition(expression: LogicExpression): b
8082
return `${leftValue}`.length > Number(rightValue)
8183
case 'length<':
8284
return `${leftValue}`.length < Number(rightValue)
85+
default:
86+
throw new Error(`Unknown expression comparator ${comparator}`)
87+
}
88+
}
89+
90+
export function expressionsToQuery(orExpressions: LogicExpression[][]): any {
91+
// If there's only one `orExpression` and one condition inside it
92+
if (orExpressions.length === 1 && orExpressions[0].length === 1) {
93+
return expressionToQuery(orExpressions[0][0])
94+
}
95+
96+
// If there's only one group of `andExpressions`, no need for `$or`
97+
if (orExpressions.length === 1) {
98+
return { $and: orExpressions[0].map((expression) => expressionToQuery(expression)) }
99+
}
100+
101+
// Return the general structure otherwise
102+
return {
103+
$or: orExpressions.map((andExpressions) => {
104+
// If there's only one condition inside an `andExpressions` group, no need for `$and`
105+
if (andExpressions.length === 1) {
106+
return expressionToQuery(andExpressions[0])
107+
}
108+
return { $and: andExpressions.map((expression) => expressionToQuery(expression)) }
109+
}),
110+
}
111+
}
112+
113+
export function expressionToQuery(expression: LogicExpression): any {
114+
const { leftValue, comparator, rightValue } = expression
115+
switch (comparator) {
116+
case '=':
117+
return { [leftValue]: rightValue }
118+
case '!=':
119+
return { [leftValue]: { $ne: rightValue } }
120+
case 'contains':
121+
return { [leftValue]: { $regex: rightValue, $options: 'i' } }
122+
case '!contains':
123+
return { [leftValue]: { $not: { $regex: rightValue, $options: 'i' } } }
124+
case 'startsWith':
125+
return { [leftValue]: { $regex: `^${rightValue}`, $options: 'i' } }
126+
case '!startsWith':
127+
return { [leftValue]: { $not: { $regex: `^${rightValue}`, $options: 'i' } } }
128+
case 'endsWith':
129+
return { [leftValue]: { $regex: `${rightValue}$`, $options: 'i' } }
130+
case '!endsWith':
131+
return { [leftValue]: { $not: { $regex: `${rightValue}$`, $options: 'i' } } }
132+
case '>':
133+
return { [leftValue]: { $gt: Number(rightValue) } }
134+
case '>=':
135+
return { [leftValue]: { $gte: Number(rightValue) } }
136+
case '<':
137+
return { [leftValue]: { $lt: Number(rightValue) } }
138+
case '<=':
139+
return { [leftValue]: { $lte: Number(rightValue) } }
140+
case 'length>':
141+
return { [leftValue]: { $where: `this.${leftValue}.length > ${Number(rightValue)}` } }
142+
case 'length<':
143+
return { [leftValue]: { $where: `this.${leftValue}.length < ${Number(rightValue)}` } }
144+
default:
145+
throw new Error(`Unknown expression comparator ${comparator}`)
83146
}
84-
throw new Error(`Unknown expression comparator ${comparator}`)
85147
}

0 commit comments

Comments
 (0)