Skip to content

Commit ff60f3b

Browse files
committed
feat: add contact tags and fields
1 parent f11a50e commit ff60f3b

File tree

7 files changed

+109
-28
lines changed

7 files changed

+109
-28
lines changed

apps/api/src/chat/entities/contact.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { OwnedEntity } from '@app/common/decorators/owned-entity.decorator'
44
import { jsonProp } from '@app/common/decorators/props/json-prop.decorator'
55
import { Reference } from '@app/common/typings/mongodb'
66
import { Injectable } from '@nestjs/common'
7-
import { Field, ObjectType } from '@nestjs/graphql'
7+
import { Field, InputType, ObjectType } from '@nestjs/graphql'
88
import { Authorize } from '@ptc-org/nestjs-query-graphql'
99
import { Index, prop } from '@typegoose/typegoose'
1010
import { getAddress, isAddress } from 'ethers/lib/utils'
1111
import { GraphQLString } from 'graphql'
12+
import { GraphQLJSONObject } from 'graphql-type-json'
1213
import { User } from '../../users/entities/user'
1314

1415
@Injectable()
@@ -33,3 +34,24 @@ export class Contact extends BaseEntity {
3334
@jsonProp()
3435
fields?: Record<string, any>
3536
}
37+
38+
@InputType()
39+
export class CreateContactInput {
40+
@Field()
41+
address: string
42+
43+
@Field(() => [GraphQLString], { nullable: true })
44+
tags?: string[]
45+
46+
@Field(() => GraphQLJSONObject, { nullable: true })
47+
fields?: Record<string, any>
48+
}
49+
50+
@InputType()
51+
export class UpdateContactInput {
52+
@Field(() => [GraphQLString], { nullable: true })
53+
tags?: string[]
54+
55+
@Field(() => GraphQLJSONObject, { nullable: true })
56+
fields?: Record<string, any>
57+
}

apps/api/src/chat/resolvers/contact.resolver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import { GraphqlGuard } from '../../auth/guards/graphql.guard'
1616
import { IntegrationAccountService } from '../../integration-accounts/services/integration-account.service'
1717
import { ResultPayload } from '../../users/payloads/user.payloads'
1818
import { UserService } from '../../users/services/user.service'
19-
import { Contact } from '../entities/contact'
19+
import { Contact, CreateContactInput, UpdateContactInput } from '../entities/contact'
2020
import { ContactService } from '../services/contact.service'
2121

2222
@Resolver(() => Contact)
2323
@UseGuards(GraphqlGuard)
2424
@UseInterceptors(AuthorizerInterceptor(Contact))
2525
export class ContactResolver extends BaseResolver(Contact, {
26+
CreateDTOClass: CreateContactInput,
27+
UpdateDTOClass: UpdateContactInput,
2628
guards: [GraphqlGuard],
2729
enableTotalCount: true,
2830
}) {

apps/api/src/chat/services/contact.service.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { uniq } from 'lodash'
1616
import { ObjectId, WriteError } from 'mongodb'
1717
import { InjectModel } from 'nestjs-typegoose'
1818
import { User } from '../../users/entities/user'
19+
import { UserService } from '../../users/services/user.service'
1920
import { Contact } from '../entities/contact'
2021

2122
@Injectable()
@@ -26,6 +27,7 @@ export class ContactService extends BaseService<Contact> {
2627
constructor(
2728
@InjectModel(Contact) protected readonly model: ReturnModelType<typeof Contact>,
2829
@InjectQueue('contacts') private contactsQueue: Queue,
30+
private userService: UserService,
2931
) {
3032
super(model)
3133
ContactService.instance = this
@@ -140,6 +142,7 @@ export class ContactService extends BaseService<Contact> {
140142
const existingTags = contact.tags.map((tag) => tag.toLowerCase())
141143
const tagsAdded = tags.filter((tag) => !existingTags.includes(tag.toLowerCase()))
142144
await this.onTagsAdded(contact, tagsAdded)
145+
await this.userService.addContactDataKeys(contact.owner._id, [contact])
143146
return newTags
144147
}
145148
}
@@ -176,6 +179,7 @@ export class ContactService extends BaseService<Contact> {
176179
if (contact.tags?.length) {
177180
await this.onTagsAdded(contact, contact.tags)
178181
}
182+
await this.userService.addContactDataKeys(contact.owner._id, [contact])
179183
}
180184

181185
async afterCreateMany(contacts: Contact[]) {
@@ -195,14 +199,15 @@ export class ContactService extends BaseService<Contact> {
195199
tags: contactsWithTags.flatMap((contact) => contact.tags),
196200
})
197201
}
202+
await this.userService.addContactDataKeys(contacts[0].owner._id, contacts)
198203
}
199204

200205
async updateOne(
201206
id: string,
202207
update: Partial<Contact>,
203208
opts?: UpdateOneOptions<Contact> | undefined,
204209
): Promise<Contact> {
205-
if (!update.tags) {
210+
if (!update.tags && !update.fields) {
206211
return super.updateOne(id, update, opts)
207212
}
208213

@@ -212,11 +217,17 @@ export class ContactService extends BaseService<Contact> {
212217
throw new NotFoundException(`Contact with id ${id} not found`)
213218
}
214219
const contact = await super.updateOne(id, update, opts)
215-
const existingTags = contactBefore.tags.map((tag) => tag.toLowerCase())
216-
const tagsAdded = update.tags.filter((tag) => !existingTags.includes(tag.toLowerCase()))
217-
if (tagsAdded?.length) {
218-
await this.onTagsAdded(contact, tagsAdded)
220+
221+
if (update.tags) {
222+
const existingTags = contactBefore.tags.map((tag) => tag.toLowerCase())
223+
const tagsAdded = update.tags.filter((tag) => !existingTags.includes(tag.toLowerCase()))
224+
if (tagsAdded?.length) {
225+
await this.onTagsAdded(contact, tagsAdded)
226+
}
219227
}
228+
229+
await this.userService.addContactDataKeys(contact.owner._id, [contact])
230+
220231
return contact
221232
}
222233

apps/api/src/users/entities/user.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { Authorize } from '@ptc-org/nestjs-query-graphql'
55
import { prop } from '@typegoose/typegoose'
66
import { IsEmail } from 'class-validator'
77
import { getAddress, isAddress } from 'ethers/lib/utils'
8+
import { GraphQLString } from 'graphql'
89
import { GraphQLJSONObject } from 'graphql-type-json'
910
import { Schema } from 'mongoose'
10-
import { PlanConfig, defaultPlan, plansConfig } from '../config/plans.config'
11+
import { defaultPlan, PlanConfig, plansConfig } from '../config/plans.config'
1112
import { UserAuthorizer } from '../resolvers/user.authorizer'
1213

1314
@ObjectType()
@@ -106,6 +107,14 @@ export class User extends BaseEntity {
106107
@prop()
107108
limits?: Record<string, number>
108109

110+
@Field(() => [GraphQLString], { nullable: true })
111+
@prop()
112+
contactTags?: string[]
113+
114+
@Field(() => [GraphQLString], { nullable: true })
115+
@prop()
116+
contactFields?: string[]
117+
109118
get planConfig(): PlanConfig {
110119
return plansConfig[this.plan]
111120
}

apps/api/src/users/services/user.service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SiweMessage } from 'siwe'
1111
import { SecurityUtils } from '../../../../../libs/common/src/utils/security.utils'
1212
import { EmailService } from '../../../../../libs/emails/src/services/email.service'
1313
import { EmailVerificationTemplate } from '../../../../../libs/emails/src/templates/emailVerificationTemplate'
14+
import { Contact } from '../../chat/entities/contact'
1415
import { User } from '../entities/user'
1516

1617
@Injectable()
@@ -109,6 +110,44 @@ export class UserService extends BaseService<User> {
109110
return plainVerificationToken
110111
}
111112

113+
async addContactDataKeys(userId: ObjectId, contact: Contact[]): Promise<void> {
114+
const allTags = contact.flatMap((contact) => contact.tags)
115+
const uniqueTags = Array.from(new Set(allTags))
116+
117+
const allFields = contact.flatMap((contact) => Object.keys(contact.fields ?? {}))
118+
const uniqueFields = Array.from(new Set(allFields))
119+
120+
if (!uniqueTags.length && !uniqueFields.length) {
121+
return
122+
}
123+
124+
const user = await this.findById(userId.toString())
125+
if (!user) {
126+
throw new NotFoundException(`User ${userId} not found`)
127+
}
128+
129+
const existingTags = user.contactTags ?? []
130+
const existingFields = user.contactFields ?? []
131+
132+
const newTags = uniqueTags.filter((tag) => !existingTags.includes(tag))
133+
const newFields = uniqueFields.filter((field) => !existingFields.includes(field))
134+
135+
if (!newTags.length && !newFields.length) {
136+
return
137+
}
138+
139+
await this.updateOneNative(
140+
{ _id: userId },
141+
{
142+
$addToSet: {
143+
...(newTags.length && { contactTags: { $each: newTags } }),
144+
...(newFields.length && { contactFields: { $each: newFields } }),
145+
},
146+
},
147+
)
148+
this.logger.log(`Added ${newTags.length} tags and ${newFields.length} fields to user ${userId}`)
149+
}
150+
112151
async syncIndexes() {
113152
return this.model.syncIndexes()
114153
}

generated/graphql.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -734,26 +734,23 @@ export interface DeleteOneWorkflowTriggerInput {
734734
}
735735

736736
export interface CreateOneContactInput {
737-
contact: CreateContact;
737+
contact: CreateContactInput;
738738
}
739739

740-
export interface CreateContact {
741-
id?: Nullable<string>;
742-
createdAt?: Nullable<DateTime>;
743-
address?: Nullable<string>;
740+
export interface CreateContactInput {
741+
address: string;
744742
tags?: Nullable<string[]>;
743+
fields?: Nullable<JSONObject>;
745744
}
746745

747746
export interface UpdateOneContactInput {
748747
id: string;
749-
update: UpdateContact;
748+
update: UpdateContactInput;
750749
}
751750

752-
export interface UpdateContact {
753-
id?: Nullable<string>;
754-
createdAt?: Nullable<DateTime>;
755-
address?: Nullable<string>;
751+
export interface UpdateContactInput {
756752
tags?: Nullable<string[]>;
753+
fields?: Nullable<JSONObject>;
757754
}
758755

759756
export interface DeleteOneContactInput {
@@ -918,6 +915,8 @@ export interface User {
918915
subscribedToNotifications: boolean;
919916
subscribedToNewsletter: boolean;
920917
features?: Nullable<JSONObject>;
918+
contactTags?: Nullable<string[]>;
919+
contactFields?: Nullable<string[]>;
921920
}
922921

923922
export interface IntegrationAccount {

generated/schema.graphql

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type User {
1616
subscribedToNotifications: Boolean!
1717
subscribedToNewsletter: Boolean!
1818
features: JSONObject
19+
contactTags: [String!]
20+
contactFields: [String!]
1921
}
2022

2123
"""
@@ -1962,29 +1964,26 @@ input DeleteOneWorkflowTriggerInput {
19621964

19631965
input CreateOneContactInput {
19641966
"""The record to create"""
1965-
contact: CreateContact!
1967+
contact: CreateContactInput!
19661968
}
19671969

1968-
input CreateContact {
1969-
id: ID
1970-
createdAt: DateTime
1971-
address: String
1970+
input CreateContactInput {
1971+
address: String!
19721972
tags: [String!]
1973+
fields: JSONObject
19731974
}
19741975

19751976
input UpdateOneContactInput {
19761977
"""The id of the record to update"""
19771978
id: ID!
19781979

19791980
"""The update to apply."""
1980-
update: UpdateContact!
1981+
update: UpdateContactInput!
19811982
}
19821983

1983-
input UpdateContact {
1984-
id: ID
1985-
createdAt: DateTime
1986-
address: String
1984+
input UpdateContactInput {
19871985
tags: [String!]
1986+
fields: JSONObject
19881987
}
19891988

19901989
input DeleteOneContactInput {

0 commit comments

Comments
 (0)