diff --git a/.drone.yml b/.drone.yml index 517b7b2fe..2cfbb9062 100644 --- a/.drone.yml +++ b/.drone.yml @@ -28,6 +28,7 @@ pipeline: branch: - master - develop + - staging event: - push @@ -90,6 +91,7 @@ pipeline: branch: - master - develop + - staging event: - push diff --git a/.env.sample b/.env.sample index 6278715ee..5427922a6 100644 --- a/.env.sample +++ b/.env.sample @@ -68,3 +68,5 @@ PORT_CRONS=3600 # workers PORT_WORKERS=3700 + +USE_BRAND_RESTRICTIONS=false \ No newline at end of file diff --git a/src/__tests__/activityLogQueries.test.ts b/src/__tests__/activityLogQueries.test.ts index ea617f659..6e1f7cf45 100644 --- a/src/__tests__/activityLogQueries.test.ts +++ b/src/__tests__/activityLogQueries.test.ts @@ -1,71 +1,71 @@ -import * as faker from 'faker'; -import { graphqlRequest } from '../db/connection'; -import { activityLogFactory } from '../db/factories'; -import { ActivityLogs } from '../db/models'; -import { ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, ACTIVITY_TYPES } from '../db/models/definitions/constants'; - -import './setup.ts'; - -describe('activityLogQueries', () => { - const commonParamDefs = ` - $contentType: String!, - $contentId: String!, - $activityType: String!, - $limit: Int, - `; - - const commonParams = ` - contentType: $contentType - contentId: $contentId - activityType: $activityType - limit: $limit - `; - - const qryActivityLogs = ` - query activityLogs(${commonParamDefs}) { - activityLogs(${commonParams}) { - _id - action - id - createdAt - content - by { - type - details { - avatar - fullName - position - } - } - } - } - `; - - afterEach(async () => { - // Clearing test data - await ActivityLogs.deleteMany({}); - }); - - test('Activity log list', async () => { - const contentType = ACTIVITY_CONTENT_TYPES.CUSTOMER; - const activityType = ACTIVITY_TYPES.INTERNAL_NOTE; - const contentId = faker.random.uuid(); - - for (let i = 0; i < 3; i++) { - await activityLogFactory({ - activity: { type: activityType, action: ACTIVITY_ACTIONS.CREATE }, - contentType: { type: contentType, id: contentId }, - }); - } - - const args = { contentType, activityType, contentId }; - - const responses = await graphqlRequest(qryActivityLogs, 'activityLogs', args); - - expect(responses.length).toBe(3); - - const responsesWithLimit = await graphqlRequest(qryActivityLogs, 'activityLogs', { ...args, limit: 2 }); - - expect(responsesWithLimit.length).toBe(2); - }); -}); +import * as faker from 'faker'; +import { graphqlRequest } from '../db/connection'; +import { activityLogFactory } from '../db/factories'; +import { ActivityLogs } from '../db/models'; +import { ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, ACTIVITY_TYPES } from '../db/models/definitions/constants'; + +import './setup.ts'; + +describe('activityLogQueries', () => { + const commonParamDefs = ` + $contentType: String!, + $contentId: String!, + $activityType: String!, + $limit: Int, + `; + + const commonParams = ` + contentType: $contentType + contentId: $contentId + activityType: $activityType + limit: $limit + `; + + const qryActivityLogs = ` + query activityLogs(${commonParamDefs}) { + activityLogs(${commonParams}) { + _id + action + id + createdAt + content + by { + type + details { + avatar + fullName + position + } + } + } + } + `; + + afterEach(async () => { + // Clearing test data + await ActivityLogs.deleteMany({}); + }); + + test('Activity log list', async () => { + const contentType = ACTIVITY_CONTENT_TYPES.CUSTOMER; + const activityType = ACTIVITY_TYPES.INTERNAL_NOTE; + const contentId = faker.random.uuid(); + + for (let i = 0; i < 3; i++) { + await activityLogFactory({ + activity: { type: activityType, action: ACTIVITY_ACTIONS.CREATE }, + contentType: { type: contentType, id: contentId }, + }); + } + + const args = { contentType, activityType, contentId }; + + const responses = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(responses.length).toBe(3); + + const responsesWithLimit = await graphqlRequest(qryActivityLogs, 'activityLogs', { ...args, limit: 2 }); + + expect(responsesWithLimit.length).toBe(2); + }); +}); diff --git a/src/__tests__/brandQueries.test.ts b/src/__tests__/brandQueries.test.ts index 11b4222d2..e2d72e6e1 100644 --- a/src/__tests__/brandQueries.test.ts +++ b/src/__tests__/brandQueries.test.ts @@ -1,93 +1,93 @@ -import { graphqlRequest } from '../db/connection'; -import { brandFactory } from '../db/factories'; -import { Brands } from '../db/models'; - -import './setup.ts'; - -describe('brandQueries', () => { - afterEach(async () => { - // Clearing test data - await Brands.deleteMany({}); - }); - - test('Brands', async () => { - const args = { - page: 1, - perPage: 2, - }; - - await brandFactory({}); - await brandFactory({}); - await brandFactory({}); - - const qry = ` - query brands($page: Int $perPage: Int) { - brands(page: $page perPage: $perPage) { - _id - name - description - code - userId - createdAt - emailConfig - integrations { _id } - } - } - `; - - const response = await graphqlRequest(qry, 'brands', args); - - expect(response.length).toBe(2); - }); - - test('Brand detail', async () => { - const qry = ` - query brandDetail($_id: String!) { - brandDetail(_id: $_id) { - _id - } - } - `; - - const brand = await brandFactory({}); - - const response = await graphqlRequest(qry, 'brandDetail', { _id: brand._id }); - - expect(response._id).toBe(brand._id); - }); - - test('Get brand total count', async () => { - const qry = ` - query brandsTotalCount { - brandsTotalCount - } - `; - - await brandFactory({}); - await brandFactory({}); - await brandFactory({}); - - const brandsCount = await graphqlRequest(qry, 'brandsTotalCount'); - - expect(brandsCount).toBe(3); - }); - - test('Get last brand', async () => { - const qry = ` - query brandsGetLast { - brandsGetLast { - _id - } - } - `; - - await brandFactory({}); - await brandFactory({}); - - const brand = await brandFactory({}); - - const lastBrand = await graphqlRequest(qry, 'brandsGetLast'); - - expect(lastBrand._id).toBe(brand._id); - }); -}); +import { graphqlRequest } from '../db/connection'; +import { brandFactory } from '../db/factories'; +import { Brands } from '../db/models'; + +import './setup.ts'; + +describe('brandQueries', () => { + afterEach(async () => { + // Clearing test data + await Brands.deleteMany({}); + }); + + test('Brands', async () => { + const args = { + page: 1, + perPage: 2, + }; + + await brandFactory({}); + await brandFactory({}); + await brandFactory({}); + + const qry = ` + query brands($page: Int $perPage: Int) { + brands(page: $page perPage: $perPage) { + _id + name + description + code + userId + createdAt + emailConfig + integrations { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'brands', args); + + expect(response.length).toBe(2); + }); + + test('Brand detail', async () => { + const qry = ` + query brandDetail($_id: String!) { + brandDetail(_id: $_id) { + _id + } + } + `; + + const brand = await brandFactory({}); + + const response = await graphqlRequest(qry, 'brandDetail', { _id: brand._id }); + + expect(response._id).toBe(brand._id); + }); + + test('Get brand total count', async () => { + const qry = ` + query brandsTotalCount { + brandsTotalCount + } + `; + + await brandFactory({}); + await brandFactory({}); + await brandFactory({}); + + const brandsCount = await graphqlRequest(qry, 'brandsTotalCount'); + + expect(brandsCount).toBe(3); + }); + + test('Get last brand', async () => { + const qry = ` + query brandsGetLast { + brandsGetLast { + _id + } + } + `; + + await brandFactory({}); + await brandFactory({}); + + const brand = await brandFactory({}); + + const lastBrand = await graphqlRequest(qry, 'brandsGetLast'); + + expect(lastBrand._id).toBe(brand._id); + }); +}); diff --git a/src/__tests__/customerQueries.test.ts b/src/__tests__/customerQueries.test.ts index e4c48e617..a77bf17ca 100644 --- a/src/__tests__/customerQueries.test.ts +++ b/src/__tests__/customerQueries.test.ts @@ -1,474 +1,474 @@ -import * as faker from 'faker'; -import * as moment from 'moment'; -import { graphqlRequest } from '../db/connection'; -import { customerFactory, formFactory, integrationFactory, segmentFactory, tagsFactory } from '../db/factories'; -import { Customers, Segments, Tags } from '../db/models'; - -import './setup.ts'; - -const count = response => { - return Object.keys(response).length; -}; - -describe('customerQueries', () => { - const commonParamDefs = ` - $page: Int, - $perPage: Int, - $segment: String, - $tag: String, - $ids: [String], - $searchValue: String, - $form: String, - $startDate: String, - $endDate: String, - $lifecycleState: String, - $leadStatus: String - `; - - const commonParams = ` - page: $page - perPage: $perPage - segment: $segment - tag: $tag - ids: $ids - searchValue: $searchValue - form: $form - startDate: $startDate - endDate: $endDate - lifecycleState: $lifecycleState - leadStatus: $leadStatus - `; - - const qryCustomers = ` - query customers(${commonParamDefs}) { - customers(${commonParams}) { - _id - createdAt - modifiedAt - integrationId - firstName - lastName - primaryEmail - emails - primaryPhone - phones - isUser - tagIds - remoteAddress - internalNotes - location - visitorContactInfo - customFieldsData - messengerData - ownerId - position - department - leadStatus - lifecycleState - hasAuthority - description - doNotDisturb - links { - linkedIn - twitter - facebook - youtube - github - website - } - companies { _id } - conversations { _id } - deals { _id } - getIntegrationData - getMessengerCustomData - getTags { _id } - owner { _id } - } - } - `; - - const qryCustomersMain = ` - query customersMain(${commonParamDefs}) { - customersMain(${commonParams}) { - list { - _id - firstName - lastName - primaryEmail - primaryPhone - tagIds - } - totalCount - } - } - `; - - const qryCount = ` - query customerCounts(${commonParamDefs} $byFakeSegment: JSON, $only: String) { - customerCounts(${commonParams} byFakeSegment: $byFakeSegment, only: $only) - } - `; - - const firstName = faker.name.firstName(); - const lastName = faker.name.lastName(); - const primaryEmail = faker.internet.email(); - const primaryPhone = '12345678'; - - afterEach(async () => { - // Clearing test data - await Customers.deleteMany({}); - await Segments.deleteMany({}); - await Tags.deleteMany({}); - }); - - test('Customers', async () => { - const integration = await integrationFactory(); - await customerFactory({ integrationId: integration._id }, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCustomers, 'customers', args); - - expect(responses.length).toBe(3); - }); - - test('Customers filtered by ids', async () => { - const customer1 = await customerFactory({}, true); - const customer2 = await customerFactory({}, true); - const customer3 = await customerFactory({}, true); - - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const ids = [customer1._id, customer2._id, customer3._id]; - - const responses = await graphqlRequest(qryCustomers, 'customers', { ids }); - - expect(responses.length).toBe(3); - }); - - test('Customers filtered by tag', async () => { - const tag = await tagsFactory({}); - - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ tagIds: [tag._id] }, true); - await customerFactory({ tagIds: [tag._id] }, true); - - const tagResponse = await Tags.findOne({}, '_id'); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - tag: tagResponse ? tagResponse._id : '', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by leadStatus', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ leadStatus: 'new' }, true); - await customerFactory({ leadStatus: 'new' }, true); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - leadStatus: 'new', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by lifecycleState', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - lifecycleState: 'subscriber', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by segment', async () => { - await customerFactory({ firstName }, true); - await customerFactory({}, true); - - const args = { - contentType: 'customer', - conditions: [ - { - field: 'firstName', - operator: 'c', - value: firstName, - type: 'string', - }, - ], - }; - - const segment = await segmentFactory(args); - - const response = await graphqlRequest(qryCustomers, 'customers', { - segment: segment._id, - }); - - expect(response.length).toBe(1); - }); - - test('Customers filtered by search value', async () => { - await customerFactory({ firstName }, true); - await customerFactory({ lastName }, true); - await customerFactory({ primaryPhone, phones: [primaryPhone] }, true); - await customerFactory({ primaryEmail, emails: [primaryEmail] }, true); - - // customers by firstName ============== - let responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: firstName, - }); - - expect(responses.length).toBe(1); - expect(responses[0].firstName).toBe(firstName); - - // customers by lastName =========== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: lastName, - }); - - expect(responses.length).toBe(1); - expect(responses[0].lastName).toBe(lastName); - - // customers by email ========== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: primaryEmail, - }); - - expect(responses.length).toBe(1); - expect(responses[0].primaryEmail).toBe(primaryEmail); - - // customers by phone ============== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: primaryPhone, - }); - - expect(responses.length).toBe(1); - expect(responses[0].primaryPhone).toBe(primaryPhone); - }); - - test('Main customers', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(3); - expect(responses.totalCount).toBe(4); - }); - - test('Count customers', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - // Creating test data - await segmentFactory({ contentType: 'customer' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'bySegment', - }); - - expect(count(response.bySegment)).toBe(1); - }); - - test('Customer count by tag', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - await tagsFactory({ type: 'company' }); - await tagsFactory({ type: 'customer' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byTag', - }); - - expect(count(response.byTag)).toBe(1); - }); - - test('Customer count by segment', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - await segmentFactory({ contentType: 'customer' }); - await segmentFactory({ contentType: 'company' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'bySegment', - }); - - expect(count(response.bySegment)).toBe(1); - }); - - test('Customer count by fake segment', async () => { - await customerFactory({ lastName }, true); - - const byFakeSegment = { - contentType: 'customer', - conditions: [ - { - field: 'lastName', - operator: 'c', - value: lastName, - type: 'string', - }, - ], - }; - - const response = await graphqlRequest(qryCount, 'customerCounts', { - byFakeSegment, - }); - - expect(response.byFakeSegment).toBe(1); - }); - - test('Customer count by leadStatus', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ leadStatus: 'new' }, true); - await customerFactory({ leadStatus: 'new' }, true); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byLeadStatus', - }); - - expect(response.byLeadStatus.open).toBe(2); - expect(response.byLeadStatus.new).toBe(2); - }); - - test('Customer count by lifecycleState', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byLifecycleState', - }); - - expect(response.byLifecycleState.subscriber).toBe(2); - expect(response.byLifecycleState.lead).toBe(2); - }); - - test('Customer detail', async () => { - const customer = await customerFactory({}, true); - - const qry = ` - query customerDetail($_id: String!) { - customerDetail(_id: $_id) { - _id - } - } - `; - - const response = await graphqlRequest(qry, 'customerDetail', { - _id: customer._id, - }); - - expect(response._id).toBe(customer._id); - }); - - test('Customer filtered by submitted form', async () => { - const customer = await customerFactory({}, true); - let submissions = [{ customerId: customer._id, submittedAt: new Date() }]; - const form = await formFactory({ submissions }); - - const testCustomer = await customerFactory({}, true); - - submissions = [ - { customerId: testCustomer._id, submittedAt: new Date() }, - { customerId: customer._id, submittedAt: new Date() }, - ]; - - const testForm = await formFactory({ submissions }); - - let responses = await graphqlRequest(qryCustomersMain, 'customersMain', { - form: form._id, - }); - - expect(responses.list.length).toBe(1); - - responses = await graphqlRequest(qryCustomersMain, 'customersMain', { - form: testForm._id, - }); - - expect(responses.list.length).toBe(2); - }); - - test('Customer filtered by submitted form with startDate and endDate', async () => { - const customer = await customerFactory({}, true); - const customer1 = await customerFactory({}, true); - const customer2 = await customerFactory({}, true); - - const startDate = '2018-04-03 10:00'; - const endDate = '2018-04-03 18:00'; - - // Creating 3 submissions for form - const submissions = [ - { - customerId: customer._id, - submittedAt: moment(startDate) - .add(5, 'days') - .toDate(), - }, - { - customerId: customer1._id, - submittedAt: moment(startDate) - .add(20, 'days') - .toDate(), - }, - { - customerId: customer2._id, - submittedAt: moment(startDate) - .add(1, 'hours') - .toDate(), - }, - ]; - - const form = await formFactory({ submissions }); - - let args = { - startDate, - endDate, - form: form._id, - }; - - let responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(1); - - args = { - startDate, - endDate: moment(endDate) - .add(25, 'days') - .format('YYYY-MM-DD HH:mm'), - form: form._id, - }; - - responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(3); - }); - - test('Customer filtered by default selector', async () => { - const integration = await integrationFactory({}); - await Customers.createCustomer({ integrationId: integration._id }); - await customerFactory({}, true); - await customerFactory({}, true); - - const responses = await graphqlRequest(qryCustomersMain, 'customersMain', {}); - - expect(responses.list.length).toBe(2); - }); -}); +import * as faker from 'faker'; +import * as moment from 'moment'; +import { graphqlRequest } from '../db/connection'; +import { customerFactory, formFactory, integrationFactory, segmentFactory, tagsFactory } from '../db/factories'; +import { Customers, Segments, Tags } from '../db/models'; + +import './setup.ts'; + +const count = response => { + return Object.keys(response).length; +}; + +describe('customerQueries', () => { + const commonParamDefs = ` + $page: Int, + $perPage: Int, + $segment: String, + $tag: String, + $ids: [String], + $searchValue: String, + $form: String, + $startDate: String, + $endDate: String, + $lifecycleState: String, + $leadStatus: String + `; + + const commonParams = ` + page: $page + perPage: $perPage + segment: $segment + tag: $tag + ids: $ids + searchValue: $searchValue + form: $form + startDate: $startDate + endDate: $endDate + lifecycleState: $lifecycleState + leadStatus: $leadStatus + `; + + const qryCustomers = ` + query customers(${commonParamDefs}) { + customers(${commonParams}) { + _id + createdAt + modifiedAt + integrationId + firstName + lastName + primaryEmail + emails + primaryPhone + phones + isUser + tagIds + remoteAddress + internalNotes + location + visitorContactInfo + customFieldsData + messengerData + ownerId + position + department + leadStatus + lifecycleState + hasAuthority + description + doNotDisturb + links { + linkedIn + twitter + facebook + youtube + github + website + } + companies { _id } + conversations { _id } + deals { _id } + getIntegrationData + getMessengerCustomData + getTags { _id } + owner { _id } + } + } + `; + + const qryCustomersMain = ` + query customersMain(${commonParamDefs}) { + customersMain(${commonParams}) { + list { + _id + firstName + lastName + primaryEmail + primaryPhone + tagIds + } + totalCount + } + } + `; + + const qryCount = ` + query customerCounts(${commonParamDefs} $byFakeSegment: JSON, $only: String) { + customerCounts(${commonParams} byFakeSegment: $byFakeSegment, only: $only) + } + `; + + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + const primaryEmail = faker.internet.email(); + const primaryPhone = '12345678'; + + afterEach(async () => { + // Clearing test data + await Customers.deleteMany({}); + await Segments.deleteMany({}); + await Tags.deleteMany({}); + }); + + test('Customers', async () => { + const integration = await integrationFactory(); + await customerFactory({ integrationId: integration._id }, true); + await customerFactory({}, true); + await customerFactory({}, true); + + const args = { page: 1, perPage: 3 }; + const responses = await graphqlRequest(qryCustomers, 'customers', args); + + expect(responses.length).toBe(3); + }); + + test('Customers filtered by ids', async () => { + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + const customer3 = await customerFactory({}, true); + + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({}, true); + + const ids = [customer1._id, customer2._id, customer3._id]; + + const responses = await graphqlRequest(qryCustomers, 'customers', { ids }); + + expect(responses.length).toBe(3); + }); + + test('Customers filtered by tag', async () => { + const tag = await tagsFactory({}); + + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({ tagIds: [tag._id] }, true); + await customerFactory({ tagIds: [tag._id] }, true); + + const tagResponse = await Tags.findOne({}, '_id'); + + const responses = await graphqlRequest(qryCustomers, 'customers', { + tag: tagResponse ? tagResponse._id : '', + }); + + expect(responses.length).toBe(2); + }); + + test('Customers filtered by leadStatus', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({ leadStatus: 'new' }, true); + await customerFactory({ leadStatus: 'new' }, true); + + const responses = await graphqlRequest(qryCustomers, 'customers', { + leadStatus: 'new', + }); + + expect(responses.length).toBe(2); + }); + + test('Customers filtered by lifecycleState', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({ lifecycleState: 'subscriber' }, true); + await customerFactory({ lifecycleState: 'subscriber' }, true); + + const responses = await graphqlRequest(qryCustomers, 'customers', { + lifecycleState: 'subscriber', + }); + + expect(responses.length).toBe(2); + }); + + test('Customers filtered by segment', async () => { + await customerFactory({ firstName }, true); + await customerFactory({}, true); + + const args = { + contentType: 'customer', + conditions: [ + { + field: 'firstName', + operator: 'c', + value: firstName, + type: 'string', + }, + ], + }; + + const segment = await segmentFactory(args); + + const response = await graphqlRequest(qryCustomers, 'customers', { + segment: segment._id, + }); + + expect(response.length).toBe(1); + }); + + test('Customers filtered by search value', async () => { + await customerFactory({ firstName }, true); + await customerFactory({ lastName }, true); + await customerFactory({ primaryPhone, phones: [primaryPhone] }, true); + await customerFactory({ primaryEmail, emails: [primaryEmail] }, true); + + // customers by firstName ============== + let responses = await graphqlRequest(qryCustomers, 'customers', { + searchValue: firstName, + }); + + expect(responses.length).toBe(1); + expect(responses[0].firstName).toBe(firstName); + + // customers by lastName =========== + responses = await graphqlRequest(qryCustomers, 'customers', { + searchValue: lastName, + }); + + expect(responses.length).toBe(1); + expect(responses[0].lastName).toBe(lastName); + + // customers by email ========== + responses = await graphqlRequest(qryCustomers, 'customers', { + searchValue: primaryEmail, + }); + + expect(responses.length).toBe(1); + expect(responses[0].primaryEmail).toBe(primaryEmail); + + // customers by phone ============== + responses = await graphqlRequest(qryCustomers, 'customers', { + searchValue: primaryPhone, + }); + + expect(responses.length).toBe(1); + expect(responses[0].primaryPhone).toBe(primaryPhone); + }); + + test('Main customers', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({}, true); + + const args = { page: 1, perPage: 3 }; + const responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); + + expect(responses.list.length).toBe(3); + expect(responses.totalCount).toBe(4); + }); + + test('Count customers', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + + // Creating test data + await segmentFactory({ contentType: 'customer' }); + + const response = await graphqlRequest(qryCount, 'customerCounts', { + only: 'bySegment', + }); + + expect(count(response.bySegment)).toBe(1); + }); + + test('Customer count by tag', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + + await tagsFactory({ type: 'company' }); + await tagsFactory({ type: 'customer' }); + + const response = await graphqlRequest(qryCount, 'customerCounts', { + only: 'byTag', + }); + + expect(count(response.byTag)).toBe(1); + }); + + test('Customer count by segment', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + + await segmentFactory({ contentType: 'customer' }); + await segmentFactory({ contentType: 'company' }); + + const response = await graphqlRequest(qryCount, 'customerCounts', { + only: 'bySegment', + }); + + expect(count(response.bySegment)).toBe(1); + }); + + test('Customer count by fake segment', async () => { + await customerFactory({ lastName }, true); + + const byFakeSegment = { + contentType: 'customer', + conditions: [ + { + field: 'lastName', + operator: 'c', + value: lastName, + type: 'string', + }, + ], + }; + + const response = await graphqlRequest(qryCount, 'customerCounts', { + byFakeSegment, + }); + + expect(response.byFakeSegment).toBe(1); + }); + + test('Customer count by leadStatus', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({ leadStatus: 'new' }, true); + await customerFactory({ leadStatus: 'new' }, true); + + const response = await graphqlRequest(qryCount, 'customerCounts', { + only: 'byLeadStatus', + }); + + expect(response.byLeadStatus.open).toBe(2); + expect(response.byLeadStatus.new).toBe(2); + }); + + test('Customer count by lifecycleState', async () => { + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({ lifecycleState: 'subscriber' }, true); + await customerFactory({ lifecycleState: 'subscriber' }, true); + + const response = await graphqlRequest(qryCount, 'customerCounts', { + only: 'byLifecycleState', + }); + + expect(response.byLifecycleState.subscriber).toBe(2); + expect(response.byLifecycleState.lead).toBe(2); + }); + + test('Customer detail', async () => { + const customer = await customerFactory({}, true); + + const qry = ` + query customerDetail($_id: String!) { + customerDetail(_id: $_id) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'customerDetail', { + _id: customer._id, + }); + + expect(response._id).toBe(customer._id); + }); + + test('Customer filtered by submitted form', async () => { + const customer = await customerFactory({}, true); + let submissions = [{ customerId: customer._id, submittedAt: new Date() }]; + const form = await formFactory({ submissions }); + + const testCustomer = await customerFactory({}, true); + + submissions = [ + { customerId: testCustomer._id, submittedAt: new Date() }, + { customerId: customer._id, submittedAt: new Date() }, + ]; + + const testForm = await formFactory({ submissions }); + + let responses = await graphqlRequest(qryCustomersMain, 'customersMain', { + form: form._id, + }); + + expect(responses.list.length).toBe(1); + + responses = await graphqlRequest(qryCustomersMain, 'customersMain', { + form: testForm._id, + }); + + expect(responses.list.length).toBe(2); + }); + + test('Customer filtered by submitted form with startDate and endDate', async () => { + const customer = await customerFactory({}, true); + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + + const startDate = '2018-04-03 10:00'; + const endDate = '2018-04-03 18:00'; + + // Creating 3 submissions for form + const submissions = [ + { + customerId: customer._id, + submittedAt: moment(startDate) + .add(5, 'days') + .toDate(), + }, + { + customerId: customer1._id, + submittedAt: moment(startDate) + .add(20, 'days') + .toDate(), + }, + { + customerId: customer2._id, + submittedAt: moment(startDate) + .add(1, 'hours') + .toDate(), + }, + ]; + + const form = await formFactory({ submissions }); + + let args = { + startDate, + endDate, + form: form._id, + }; + + let responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); + + expect(responses.list.length).toBe(1); + + args = { + startDate, + endDate: moment(endDate) + .add(25, 'days') + .format('YYYY-MM-DD HH:mm'), + form: form._id, + }; + + responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); + + expect(responses.list.length).toBe(3); + }); + + test('Customer filtered by default selector', async () => { + const integration = await integrationFactory({}); + await Customers.createCustomer({ integrationId: integration._id }); + await customerFactory({}, true); + await customerFactory({}, true); + + const responses = await graphqlRequest(qryCustomersMain, 'customersMain', {}); + + expect(responses.list.length).toBe(2); + }); +}); diff --git a/src/apolloClient.ts b/src/apolloClient.ts index d1aa7d571..bd9832e1d 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -3,7 +3,6 @@ import * as dotenv from 'dotenv'; import { EngagesAPI, IntegrationsAPI } from './data/dataSources'; import resolvers from './data/resolvers'; import typeDefs from './data/schema'; -import { getEnv } from './data/utils'; import { Conversations, Customers } from './db/models'; import { graphqlPubsub } from './pubsub'; import { get, getArray, set, setArray } from './redisClient'; @@ -11,7 +10,7 @@ import { get, getArray, set, setArray } from './redisClient'; // load environment variables dotenv.config(); -const NODE_ENV = getEnv({ name: 'NODE_ENV' }); +const { NODE_ENV, USE_BRAND_RESTRICTIONS } = process.env; let playground: PlaygroundConfig = false; @@ -41,8 +40,41 @@ const apolloServer = new ApolloServer({ playground, uploads: false, context: ({ req, res }) => { + if (!req || NODE_ENV === 'test') { + return {}; + } + + const user = req.user; + + if (USE_BRAND_RESTRICTIONS !== 'true') { + return { + brandIdSelector: {}, + docModifier: doc => doc, + commonQuerySelector: {}, + user, + res, + }; + } + + let brandIds = []; + let brandIdSelector = {}; + + if (user && !user.isOwner) { + brandIds = user.brandIds || []; + brandIdSelector = { _id: { $in: brandIds } }; + } + + let scopeBrandIds = JSON.parse(req.cookies.scopeBrandIds || '[]'); + + if (scopeBrandIds.length === 0) { + scopeBrandIds = brandIds; + } + return { - user: req && req.user, + brandIdSelector, + docModifier: doc => ({ ...doc, scopeBrandIds }), + commonQuerySelector: { scopeBrandIds: { $in: scopeBrandIds } }, + user, res, }; }, diff --git a/src/db/models/utils.ts b/src/data/modules/coc/utils.ts similarity index 72% rename from src/db/models/utils.ts rename to src/data/modules/coc/utils.ts index 67a3b9c2a..8e33e8270 100644 --- a/src/db/models/utils.ts +++ b/src/data/modules/coc/utils.ts @@ -1,25 +1,5 @@ -import * as Random from 'meteor-random'; -import { COMPANY_BASIC_INFOS, CUSTOMER_BASIC_INFOS } from '../../data/constants'; -import { Fields } from './'; - -/* - * Mongoose field options wrapper - */ -export const field = options => { - const { pkey, type, optional } = options; - - if (type === String && !pkey && !optional) { - options.validate = /\S+/; - } - - // TODO: remove - if (pkey) { - options.type = String; - options.default = () => Random.id(); - } - - return options; -}; +import { Fields } from '../../../db/models'; +import { COMPANY_BASIC_INFOS, CUSTOMER_BASIC_INFOS } from '../../constants'; // Checking field names, All field names must be configured correctly export const checkFieldNames = async (type: string, fields: string[]) => { diff --git a/src/data/modules/insights/utils.ts b/src/data/modules/insights/utils.ts index d2890ebad..0d9335187 100644 --- a/src/data/modules/insights/utils.ts +++ b/src/data/modules/insights/utils.ts @@ -1,620 +1,620 @@ -import * as moment from 'moment'; -import * as _ from 'underscore'; -import { ConversationMessages, Conversations, Deals, Integrations, Pipelines, Stages, Users } from '../../../db/models'; -import { IStageDocument } from '../../../db/models/definitions/boards'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; -import { IUser } from '../../../db/models/definitions/users'; -import { INSIGHT_TYPES } from '../../constants'; -import { fixDate } from '../../utils'; -import { getDateFieldAsStr } from './aggregationUtils'; -import { - IDealListArgs, - IDealSelector, - IFilterSelector, - IFixDates, - IGenerateChartData, - IGenerateMessage, - IGeneratePunchCard, - IGenerateResponseData, - IGenerateTimeIntervals, - IGenerateUserChartData, - IListArgs, - IMessageSelector, - IResponseUserData, - IStageSelector, -} from './types'; - -/** - * Return filterSelector - * @param args - */ -export const getFilterSelector = (args: IListArgs): any => { - const selector: IFilterSelector = { integration: {} }; - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - if (integrationIds) { - selector.integration.kind = { $in: integrationIds.split(',') }; - } - - if (brandIds) { - selector.integration.brandId = { $in: brandIds.split(',') }; - } - - selector.createdAt = { $gte: start, $lte: end }; - - return selector; -}; - -/** - * Return filterSelector - * @param args - */ -export const getDealSelector = async (args: IDealListArgs): Promise => { - const { startDate, endDate, boardId, pipelineIds, status } = args; - const { start, end } = fixDates(startDate, endDate); - - const selector: IDealSelector = {}; - const date = { - $gte: start, - $lte: end, - }; - - // If status is either won or lost, modified date is more important - if (status) { - selector.modifiedAt = date; - } else { - selector.createdAt = date; - } - - const stageSelector: IStageSelector = {}; - - if (status) { - stageSelector.probability = status; - } - - let stages: IStageDocument[] = []; - - if (boardId) { - if (pipelineIds) { - stageSelector.pipelineId = { $in: pipelineIds.split(',') }; - } else { - const pipelines = await Pipelines.find({ boardId }); - stageSelector.pipelineId = { $in: pipelines.map(p => p._id) }; - } - - stages = await Stages.find(stageSelector); - selector.stageId = { $in: stages.map(s => s._id) }; - } else { - if (status) { - stages = await Stages.find(stageSelector); - selector.stageId = { $in: stages.map(s => s._id) }; - } - } - - return selector; -}; - -/** - * Return conversationSelect for aggregation - * @param args - * @param conversationSelector - * @param selectIds - */ -export const getConversationSelector = async ( - filterSelector: any, - conversationSelector: any = {}, - fieldName: string = 'createdAt', -): Promise => { - if (Object.keys(filterSelector.integration).length > 0) { - const integrationIds = await Integrations.find(filterSelector.integration).select('_id'); - conversationSelector.integrationId = { $in: integrationIds.map(row => row._id) }; - } - - if (!conversationSelector[fieldName]) { - conversationSelector[fieldName] = filterSelector.createdAt; - } - - return { ...conversationSelector, ...noConversationSelector }; -}; -/** - * - * @param summaries - * @param collection - * @param selector - */ -export const getSummaryData = async ({ - start, - end, - selector, - collection, - dateFieldName = 'createdAt', -}: { - start: Date; - end: Date; - selector: any; - collection: any; - dateFieldName?: string; -}): Promise => { - const intervals = generateTimeIntervals(start, end); - const summaries: Array<{ title?: string; count?: number }> = []; - - // finds a respective message counts for different time intervals. - for (const interval of intervals) { - const facetMessageSelector = { ...selector }; - - facetMessageSelector[dateFieldName] = { - $gte: interval.start.toDate(), - $lte: interval.end.toDate(), - }; - const [intervalCount] = await collection.aggregate([ - { - $match: facetMessageSelector, - }, - { - $group: { - _id: null, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - count: 1, - }, - }, - ]); - - summaries.push({ - title: interval.title, - count: intervalCount ? intervalCount.count : 0, - }); - } - - return summaries; -}; - -/** - * Builds messages find query selector. - */ -export const getMessageSelector = async ({ args, createdAt }: IGenerateMessage): Promise => { - const messageSelector: any = { - fromBot: { $exists: false }, - userId: args.type === 'response' ? { $ne: null } : null, - }; - - const filterSelector = getFilterSelector(args); - messageSelector.createdAt = filterSelector.createdAt; - - // While searching by integration - if (Object.keys(filterSelector.integration).length > 0) { - const selector = await getConversationSelector(filterSelector, { createdAt }); - - const conversationIds = await Conversations.find(selector).select('_id'); - - const rawConversationIds = conversationIds.map(obj => obj._id); - messageSelector.conversationId = { $in: rawConversationIds }; - } - - return messageSelector; -}; -/** - * Fix trend for missing values because from then aggregation, - * it could return missing values for some dates. This method - * will assign 0 values for missing x values. - * @param startDate - * @param endDate - * @param data - */ -export const fixChartData = async (data: any[], hintX: string, hintY: string): Promise => { - const results = {}; - data.map(row => { - results[row[hintX]] = row[hintY]; - }); - - return Object.keys(results) - .sort() - .map(key => { - return { x: moment(key).format('MM-DD'), y: results[key] }; - }); -}; - -/** - * Populates message collection into date range - * by given duration and loop count for chart data. - */ -export const generateChartDataBySelector = async ({ - selector, - type = INSIGHT_TYPES.CONVERSATION, - dateFieldName = '$createdAt', -}: { - selector: IMessageSelector; - type?: string; - dateFieldName?: string; -}): Promise => { - const pipelineStages = [ - { - $match: selector, - }, - { - $project: { - date: getDateFieldAsStr({ fieldName: dateFieldName }), - }, - }, - { - $group: { - _id: '$date', - y: { $sum: 1 }, - }, - }, - { - $project: { - x: '$_id', - y: 1, - _id: 0, - }, - }, - { - $sort: { - x: 1, - }, - }, - ]; - - if (type === INSIGHT_TYPES.DEAL) { - return Deals.aggregate([pipelineStages]); - } - - return ConversationMessages.aggregate([pipelineStages]); -}; - -export const generatePunchData = async ( - collection: any, - selector: object, - user: IUser, -): Promise => { - const pipelineStages = [ - { - $match: selector, - }, - { - $project: { - hour: { $hour: { date: '$createdAt', timezone: '+08' } }, - date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), - }, - }, - { - $group: { - _id: { - hour: '$hour', - date: '$date', - }, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - hour: '$_id.hour', - date: '$_id.date', - count: 1, - }, - }, - ]; - - return collection.aggregate(pipelineStages); -}; - -/** - * Populates message collection into date range - * by given duration and loop count for chart data. - */ - -export const generateChartDataByCollection = async (collection: any): Promise => { - const results = {}; - - collection.map(obj => { - const date = moment(obj.createdAt).format('YYYY-MM-DD'); - - results[date] = (results[date] || 0) + 1; - }); - - return Object.keys(results) - .sort() - .map(key => { - return { x: moment(key).format('MM-DD'), y: results[key] }; - }); -}; - -/** - * Generates time intervals for main report - */ -export const generateTimeIntervals = (start: Date, end: Date): IGenerateTimeIntervals[] => { - const month = moment(end).month(); - - return [ - { - title: 'In time range', - start: moment(start), - end: moment(end), - }, - { - title: 'This month', - start: moment(1, 'DD'), - end: moment(), - }, - { - title: 'This week', - start: moment(end).weekday(0), - end: moment(end), - }, - { - title: 'Today', - start: moment(end).add(-1, 'days'), - end: moment(end), - }, - { - title: 'Last 30 days', - start: moment(end).add(-30, 'days'), - end: moment(end), - }, - { - title: 'Last month', - start: moment(month + 1, 'MM').subtract(1, 'months'), - end: moment(month + 1, 'MM'), - }, - { - title: 'Last week', - start: moment(end).weekday(-7), - end: moment(end).weekday(0), - }, - { - title: 'Yesterday', - start: moment(end).add(-2, 'days'), - end: moment(end).add(-1, 'days'), - }, - ]; -}; - -/** - * Generate chart data for given user - */ -export const generateUserChartData = async ({ - userId, - userMessages, -}: { - userId: string; - userMessages: IMessageDocument[]; -}): Promise => { - const user = await Users.findOne({ _id: userId }); - const userData = await generateChartDataByCollection(userMessages); - - if (!user) { - return { - graph: userData, - }; - } - - const userDetail = user.details; - - return { - fullName: userDetail ? userDetail.fullName : '', - avatar: userDetail ? userDetail.avatar : '', - graph: userData, - }; -}; - -export const fixDates = (startValue: string, endValue: string, count?: number): IFixDates => { - // convert given value or get today - const endDate = fixDate(endValue); - - const startDateDefaultValue = new Date( - moment(endDate) - .add(count ? count * -1 : -7, 'days') - .toString(), - ); - - // convert given value or generate from endDate - const startDate = fixDate(startValue, startDateDefaultValue); - - return { start: startDate, end: endDate }; -}; - -export const getSummaryDates = (endValue: string): any => { - // convert given value or get today - const endDate = fixDate(endValue); - - const month = moment(endDate).month(); - const startDate = new Date( - moment(month + 1, 'MM') - .subtract(1, 'months') - .toString(), - ); - - return { $gte: startDate, $lte: endDate }; -}; - -/** - * Generate response chart data. - */ -export const generateResponseData = async ( - responseData: IMessageDocument[], - responseUserData: IResponseUserData, - allResponseTime: number, -): Promise => { - // preparing trend chart data - const trend = await generateChartDataByCollection(responseData); - - // Average response time for all messages - const time = Math.floor(allResponseTime / responseData.length); - - const teamMembers: any = []; - - const userIds = _.uniq(_.pluck(responseData, 'userId')); - - for (const userId of userIds) { - const { responseTime, count, summaries } = responseUserData[userId]; - - // Average response time for users. - const avgResTime = Math.floor(responseTime / count); - - // preparing each team member's chart data - teamMembers.push({ - data: await generateUserChartData({ - userId, - userMessages: responseData.filter(message => userId === message.userId), - }), - time: avgResTime, - summaries, - }); - } - - return { trend, time, teamMembers }; -}; - -export const getTimezone = (user: IUser): string => { - return (user.details ? user.details.location : '+08') || '+08'; -}; - -export const noConversationSelector = { - $or: [ - { userId: { $exists: true }, messageCount: { $gt: 1 } }, - { - userId: { $exists: false }, - $or: [ - { - closedAt: { $exists: true }, - closedUserId: { $exists: true }, - status: CONVERSATION_STATUSES.CLOSED, - }, - { - status: { $ne: CONVERSATION_STATUSES.CLOSED }, - }, - ], - }, - ], -}; - -export const timeIntervals: any[] = [ - { name: '0-5 second', count: 5 }, - { name: '6-10 second', count: 10 }, - { name: '11-15 second', count: 15 }, - { name: '16-20 second', count: 20 }, - { name: '21-25 second', count: 25 }, - { name: '26-30 second', count: 30 }, - { name: '31-35 second', count: 35 }, - { name: '36-40 second', count: 40 }, - { name: '41-45 second', count: 45 }, - { name: '46-50 second', count: 50 }, - { name: '51-55 second', count: 55 }, - { name: '56-60 second', count: 60 }, - { name: '1-2 min', count: 120 }, - { name: '2-3 min', count: 180 }, - { name: '3-4 min', count: 240 }, - { name: '4-5 min', count: 300 }, - { name: '5+ min' }, -]; - -export const timeIntervalBranches = () => { - const copyTimeIntervals = [...timeIntervals]; - copyTimeIntervals.pop(); - - return copyTimeIntervals.map(t => ({ - case: { $lte: ['$firstRespondTime', t.count] }, - then: t.name, - })); -}; - -/** - * Return conversationSelect for aggregation - * @param filterSelector - * @param conversationSelector - * @param messageSelector - */ -export const getConversationSelectorToMsg = async ( - integrationIds: string, - brandIds: string, - conversationSelector: any = {}, -): Promise => { - const filterSelector: IFilterSelector = { integration: {} }; - if (integrationIds) { - filterSelector.integration.kind = { $in: integrationIds.split(',') }; - } - - if (brandIds) { - filterSelector.integration.brandId = { $in: brandIds.split(',') }; - } - - if (Object.keys(filterSelector.integration).length > 0) { - const integrationIdsList = await Integrations.find(filterSelector.integration).select('_id'); - conversationSelector.integrationId = { $in: integrationIdsList.map(row => row._id) }; - } - return { ...conversationSelector }; -}; - -export const getConversationSelectorByMsg = async ( - integrationIds: string, - brandIds: string, - conversationSelector: any = {}, - messageSelector: any = {}, -): Promise => { - const conversationFinder = await getConversationSelectorToMsg(integrationIds, brandIds, conversationSelector); - const conversationIds = await Conversations.find(conversationFinder).select('_id'); - - const rawConversationIds = await conversationIds.map(obj => obj._id); - messageSelector.conversationId = { $in: rawConversationIds }; - - return { ...messageSelector }; -}; - -export const getConversationReportLookup = async (): Promise => { - return { - lookupPrevMsg: { - $lookup: { - from: 'conversation_messages', - let: { checkConversation: '$conversationId', checkAt: '$createdAt' }, - pipeline: [ - { - $match: { - $expr: { - $and: [{ $eq: ['$conversationId', '$$checkConversation'] }, { $lt: ['$createdAt', '$$checkAt'] }], - }, - }, - }, - { - $project: { - conversationId: 1, - createdAt: 1, - internal: 1, - userId: 1, - customerId: 1, - sizeMentionedIds: { $size: '$mentionedUserIds' }, - }, - }, - ], - as: 'prevMsgs', - }, - }, - prevMsgSlice: { - $addFields: { prevMsg: { $slice: ['$prevMsgs', -1] } }, - }, - diffSecondCalc: { - $addFields: { - diffSec: { - $divide: [{ $subtract: ['$createdAt', '$prevMsg.createdAt'] }, 1000], - }, - }, - }, - firstProject: { - $project: { - conversationId: 1, - createdAt: 1, - internal: 1, - userId: 1, - customerId: 1, - prevMsg: 1, - }, - }, - }; -}; +import * as moment from 'moment'; +import * as _ from 'underscore'; +import { ConversationMessages, Conversations, Deals, Integrations, Pipelines, Stages, Users } from '../../../db/models'; +import { IStageDocument } from '../../../db/models/definitions/boards'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; +import { IUser } from '../../../db/models/definitions/users'; +import { INSIGHT_TYPES } from '../../constants'; +import { fixDate } from '../../utils'; +import { getDateFieldAsStr } from './aggregationUtils'; +import { + IDealListArgs, + IDealSelector, + IFilterSelector, + IFixDates, + IGenerateChartData, + IGenerateMessage, + IGeneratePunchCard, + IGenerateResponseData, + IGenerateTimeIntervals, + IGenerateUserChartData, + IListArgs, + IMessageSelector, + IResponseUserData, + IStageSelector, +} from './types'; + +/** + * Return filterSelector + * @param args + */ +export const getFilterSelector = (args: IListArgs): any => { + const selector: IFilterSelector = { integration: {} }; + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + if (integrationIds) { + selector.integration.kind = { $in: integrationIds.split(',') }; + } + + if (brandIds) { + selector.integration.brandId = { $in: brandIds.split(',') }; + } + + selector.createdAt = { $gte: start, $lte: end }; + + return selector; +}; + +/** + * Return filterSelector + * @param args + */ +export const getDealSelector = async (args: IDealListArgs): Promise => { + const { startDate, endDate, boardId, pipelineIds, status } = args; + const { start, end } = fixDates(startDate, endDate); + + const selector: IDealSelector = {}; + const date = { + $gte: start, + $lte: end, + }; + + // If status is either won or lost, modified date is more important + if (status) { + selector.modifiedAt = date; + } else { + selector.createdAt = date; + } + + const stageSelector: IStageSelector = {}; + + if (status) { + stageSelector.probability = status; + } + + let stages: IStageDocument[] = []; + + if (boardId) { + if (pipelineIds) { + stageSelector.pipelineId = { $in: pipelineIds.split(',') }; + } else { + const pipelines = await Pipelines.find({ boardId }); + stageSelector.pipelineId = { $in: pipelines.map(p => p._id) }; + } + + stages = await Stages.find(stageSelector); + selector.stageId = { $in: stages.map(s => s._id) }; + } else { + if (status) { + stages = await Stages.find(stageSelector); + selector.stageId = { $in: stages.map(s => s._id) }; + } + } + + return selector; +}; + +/** + * Return conversationSelect for aggregation + * @param args + * @param conversationSelector + * @param selectIds + */ +export const getConversationSelector = async ( + filterSelector: any, + conversationSelector: any = {}, + fieldName: string = 'createdAt', +): Promise => { + if (Object.keys(filterSelector.integration).length > 0) { + const integrationIds = await Integrations.find(filterSelector.integration).select('_id'); + conversationSelector.integrationId = { $in: integrationIds.map(row => row._id) }; + } + + if (!conversationSelector[fieldName]) { + conversationSelector[fieldName] = filterSelector.createdAt; + } + + return { ...conversationSelector, ...noConversationSelector }; +}; +/** + * + * @param summaries + * @param collection + * @param selector + */ +export const getSummaryData = async ({ + start, + end, + selector, + collection, + dateFieldName = 'createdAt', +}: { + start: Date; + end: Date; + selector: any; + collection: any; + dateFieldName?: string; +}): Promise => { + const intervals = generateTimeIntervals(start, end); + const summaries: Array<{ title?: string; count?: number }> = []; + + // finds a respective message counts for different time intervals. + for (const interval of intervals) { + const facetMessageSelector = { ...selector }; + + facetMessageSelector[dateFieldName] = { + $gte: interval.start.toDate(), + $lte: interval.end.toDate(), + }; + const [intervalCount] = await collection.aggregate([ + { + $match: facetMessageSelector, + }, + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + count: 1, + }, + }, + ]); + + summaries.push({ + title: interval.title, + count: intervalCount ? intervalCount.count : 0, + }); + } + + return summaries; +}; + +/** + * Builds messages find query selector. + */ +export const getMessageSelector = async ({ args, createdAt }: IGenerateMessage): Promise => { + const messageSelector: any = { + fromBot: { $exists: false }, + userId: args.type === 'response' ? { $ne: null } : null, + }; + + const filterSelector = getFilterSelector(args); + messageSelector.createdAt = filterSelector.createdAt; + + // While searching by integration + if (Object.keys(filterSelector.integration).length > 0) { + const selector = await getConversationSelector(filterSelector, { createdAt }); + + const conversationIds = await Conversations.find(selector).select('_id'); + + const rawConversationIds = conversationIds.map(obj => obj._id); + messageSelector.conversationId = { $in: rawConversationIds }; + } + + return messageSelector; +}; +/** + * Fix trend for missing values because from then aggregation, + * it could return missing values for some dates. This method + * will assign 0 values for missing x values. + * @param startDate + * @param endDate + * @param data + */ +export const fixChartData = async (data: any[], hintX: string, hintY: string): Promise => { + const results = {}; + data.map(row => { + results[row[hintX]] = row[hintY]; + }); + + return Object.keys(results) + .sort() + .map(key => { + return { x: moment(key).format('MM-DD'), y: results[key] }; + }); +}; + +/** + * Populates message collection into date range + * by given duration and loop count for chart data. + */ +export const generateChartDataBySelector = async ({ + selector, + type = INSIGHT_TYPES.CONVERSATION, + dateFieldName = '$createdAt', +}: { + selector: IMessageSelector; + type?: string; + dateFieldName?: string; +}): Promise => { + const pipelineStages = [ + { + $match: selector, + }, + { + $project: { + date: getDateFieldAsStr({ fieldName: dateFieldName }), + }, + }, + { + $group: { + _id: '$date', + y: { $sum: 1 }, + }, + }, + { + $project: { + x: '$_id', + y: 1, + _id: 0, + }, + }, + { + $sort: { + x: 1, + }, + }, + ]; + + if (type === INSIGHT_TYPES.DEAL) { + return Deals.aggregate([pipelineStages]); + } + + return ConversationMessages.aggregate([pipelineStages]); +}; + +export const generatePunchData = async ( + collection: any, + selector: object, + user: IUser, +): Promise => { + const pipelineStages = [ + { + $match: selector, + }, + { + $project: { + hour: { $hour: { date: '$createdAt', timezone: '+08' } }, + date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), + }, + }, + { + $group: { + _id: { + hour: '$hour', + date: '$date', + }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + hour: '$_id.hour', + date: '$_id.date', + count: 1, + }, + }, + ]; + + return collection.aggregate(pipelineStages); +}; + +/** + * Populates message collection into date range + * by given duration and loop count for chart data. + */ + +export const generateChartDataByCollection = async (collection: any): Promise => { + const results = {}; + + collection.map(obj => { + const date = moment(obj.createdAt).format('YYYY-MM-DD'); + + results[date] = (results[date] || 0) + 1; + }); + + return Object.keys(results) + .sort() + .map(key => { + return { x: moment(key).format('MM-DD'), y: results[key] }; + }); +}; + +/** + * Generates time intervals for main report + */ +export const generateTimeIntervals = (start: Date, end: Date): IGenerateTimeIntervals[] => { + const month = moment(end).month(); + + return [ + { + title: 'In time range', + start: moment(start), + end: moment(end), + }, + { + title: 'This month', + start: moment(1, 'DD'), + end: moment(), + }, + { + title: 'This week', + start: moment(end).weekday(0), + end: moment(end), + }, + { + title: 'Today', + start: moment(end).add(-1, 'days'), + end: moment(end), + }, + { + title: 'Last 30 days', + start: moment(end).add(-30, 'days'), + end: moment(end), + }, + { + title: 'Last month', + start: moment(month + 1, 'MM').subtract(1, 'months'), + end: moment(month + 1, 'MM'), + }, + { + title: 'Last week', + start: moment(end).weekday(-7), + end: moment(end).weekday(0), + }, + { + title: 'Yesterday', + start: moment(end).add(-2, 'days'), + end: moment(end).add(-1, 'days'), + }, + ]; +}; + +/** + * Generate chart data for given user + */ +export const generateUserChartData = async ({ + userId, + userMessages, +}: { + userId: string; + userMessages: IMessageDocument[]; +}): Promise => { + const user = await Users.findOne({ _id: userId }); + const userData = await generateChartDataByCollection(userMessages); + + if (!user) { + return { + graph: userData, + }; + } + + const userDetail = user.details; + + return { + fullName: userDetail ? userDetail.fullName : '', + avatar: userDetail ? userDetail.avatar : '', + graph: userData, + }; +}; + +export const fixDates = (startValue: string, endValue: string, count?: number): IFixDates => { + // convert given value or get today + const endDate = fixDate(endValue); + + const startDateDefaultValue = new Date( + moment(endDate) + .add(count ? count * -1 : -7, 'days') + .toString(), + ); + + // convert given value or generate from endDate + const startDate = fixDate(startValue, startDateDefaultValue); + + return { start: startDate, end: endDate }; +}; + +export const getSummaryDates = (endValue: string): any => { + // convert given value or get today + const endDate = fixDate(endValue); + + const month = moment(endDate).month(); + const startDate = new Date( + moment(month + 1, 'MM') + .subtract(1, 'months') + .toString(), + ); + + return { $gte: startDate, $lte: endDate }; +}; + +/** + * Generate response chart data. + */ +export const generateResponseData = async ( + responseData: IMessageDocument[], + responseUserData: IResponseUserData, + allResponseTime: number, +): Promise => { + // preparing trend chart data + const trend = await generateChartDataByCollection(responseData); + + // Average response time for all messages + const time = Math.floor(allResponseTime / responseData.length); + + const teamMembers: any = []; + + const userIds = _.uniq(_.pluck(responseData, 'userId')); + + for (const userId of userIds) { + const { responseTime, count, summaries } = responseUserData[userId]; + + // Average response time for users. + const avgResTime = Math.floor(responseTime / count); + + // preparing each team member's chart data + teamMembers.push({ + data: await generateUserChartData({ + userId, + userMessages: responseData.filter(message => userId === message.userId), + }), + time: avgResTime, + summaries, + }); + } + + return { trend, time, teamMembers }; +}; + +export const getTimezone = (user: IUser): string => { + return (user.details ? user.details.location : '+08') || '+08'; +}; + +export const noConversationSelector = { + $or: [ + { userId: { $exists: true }, messageCount: { $gt: 1 } }, + { + userId: { $exists: false }, + $or: [ + { + closedAt: { $exists: true }, + closedUserId: { $exists: true }, + status: CONVERSATION_STATUSES.CLOSED, + }, + { + status: { $ne: CONVERSATION_STATUSES.CLOSED }, + }, + ], + }, + ], +}; + +export const timeIntervals: any[] = [ + { name: '0-5 second', count: 5 }, + { name: '6-10 second', count: 10 }, + { name: '11-15 second', count: 15 }, + { name: '16-20 second', count: 20 }, + { name: '21-25 second', count: 25 }, + { name: '26-30 second', count: 30 }, + { name: '31-35 second', count: 35 }, + { name: '36-40 second', count: 40 }, + { name: '41-45 second', count: 45 }, + { name: '46-50 second', count: 50 }, + { name: '51-55 second', count: 55 }, + { name: '56-60 second', count: 60 }, + { name: '1-2 min', count: 120 }, + { name: '2-3 min', count: 180 }, + { name: '3-4 min', count: 240 }, + { name: '4-5 min', count: 300 }, + { name: '5+ min' }, +]; + +export const timeIntervalBranches = () => { + const copyTimeIntervals = [...timeIntervals]; + copyTimeIntervals.pop(); + + return copyTimeIntervals.map(t => ({ + case: { $lte: ['$firstRespondTime', t.count] }, + then: t.name, + })); +}; + +/** + * Return conversationSelect for aggregation + * @param filterSelector + * @param conversationSelector + * @param messageSelector + */ +export const getConversationSelectorToMsg = async ( + integrationIds: string, + brandIds: string, + conversationSelector: any = {}, +): Promise => { + const filterSelector: IFilterSelector = { integration: {} }; + if (integrationIds) { + filterSelector.integration.kind = { $in: integrationIds.split(',') }; + } + + if (brandIds) { + filterSelector.integration.brandId = { $in: brandIds.split(',') }; + } + + if (Object.keys(filterSelector.integration).length > 0) { + const integrationIdsList = await Integrations.find(filterSelector.integration).select('_id'); + conversationSelector.integrationId = { $in: integrationIdsList.map(row => row._id) }; + } + return { ...conversationSelector }; +}; + +export const getConversationSelectorByMsg = async ( + integrationIds: string, + brandIds: string, + conversationSelector: any = {}, + messageSelector: any = {}, +): Promise => { + const conversationFinder = await getConversationSelectorToMsg(integrationIds, brandIds, conversationSelector); + const conversationIds = await Conversations.find(conversationFinder).select('_id'); + + const rawConversationIds = await conversationIds.map(obj => obj._id); + messageSelector.conversationId = { $in: rawConversationIds }; + + return { ...messageSelector }; +}; + +export const getConversationReportLookup = async (): Promise => { + return { + lookupPrevMsg: { + $lookup: { + from: 'conversation_messages', + let: { checkConversation: '$conversationId', checkAt: '$createdAt' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$conversationId', '$$checkConversation'] }, { $lt: ['$createdAt', '$$checkAt'] }], + }, + }, + }, + { + $project: { + conversationId: 1, + createdAt: 1, + internal: 1, + userId: 1, + customerId: 1, + sizeMentionedIds: { $size: '$mentionedUserIds' }, + }, + }, + ], + as: 'prevMsgs', + }, + }, + prevMsgSlice: { + $addFields: { prevMsg: { $slice: ['$prevMsgs', -1] } }, + }, + diffSecondCalc: { + $addFields: { + diffSec: { + $divide: [{ $subtract: ['$createdAt', '$prevMsg.createdAt'] }, 1000], + }, + }, + }, + firstProject: { + $project: { + conversationId: 1, + createdAt: 1, + internal: 1, + userId: 1, + customerId: 1, + prevMsg: 1, + }, + }, + }; +}; diff --git a/src/data/permissions/wrappers.ts b/src/data/permissions/wrappers.ts index 48371df04..f2c06cf6b 100644 --- a/src/data/permissions/wrappers.ts +++ b/src/data/permissions/wrappers.ts @@ -16,12 +16,14 @@ export const checkLogin = (user: IUserDocument) => { export const permissionWrapper = (cls: any, methodName: string, checkers: any) => { const oldMethod = cls[methodName]; - cls[methodName] = (root, args, { user, dataSources }) => { + cls[methodName] = (root, args, context) => { + const { user } = context; + for (const checker of checkers) { checker(user); } - return oldMethod(root, args, { user, dataSources }); + return oldMethod(root, args, context); }; }; diff --git a/src/data/resolvers/boards.ts b/src/data/resolvers/boards.ts index 36241ab38..830a4f4ae 100644 --- a/src/data/resolvers/boards.ts +++ b/src/data/resolvers/boards.ts @@ -1,9 +1,9 @@ import { Pipelines } from '../../db/models'; import { IBoardDocument } from '../../db/models/definitions/boards'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; export default { - pipelines(board: IBoardDocument, {}, { user }: { user: IUserDocument }) { + pipelines(board: IBoardDocument, {}, { user }: IContext) { if (user.isOwner) { return Pipelines.find({ boardId: board._id }); } diff --git a/src/data/resolvers/deals.ts b/src/data/resolvers/deals.ts index c38449ffc..bb525441f 100644 --- a/src/data/resolvers/deals.ts +++ b/src/data/resolvers/deals.ts @@ -1,6 +1,6 @@ import { Companies, Customers, Pipelines, Products, Stages, Users } from '../../db/models'; import { IDealDocument } from '../../db/models/definitions/deals'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { @@ -71,7 +71,7 @@ export default { return Stages.findOne({ _id: deal.stageId }); }, - isWatched(deal: IDealDocument, _args, { user }: { user: IUserDocument }) { + isWatched(deal: IDealDocument, _args, { user }: IContext) { const watchedUserIds = deal.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { diff --git a/src/data/resolvers/mutations/boards.ts b/src/data/resolvers/mutations/boards.ts index fad157cd5..accb14b1f 100644 --- a/src/data/resolvers/mutations/boards.ts +++ b/src/data/resolvers/mutations/boards.ts @@ -1,6 +1,6 @@ import { Boards, Pipelines, Stages } from '../../../db/models'; import { IBoard, IOrderInput, IPipeline, IStageDocument } from '../../../db/models/definitions/boards'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; import { checkPermission } from '../boardUtils'; @@ -20,9 +20,10 @@ const boardMutations = { /** * Create new board */ - async boardsAdd(_root, doc: IBoard, { user }: { user: IUserDocument }) { + async boardsAdd(_root, doc: IBoard, { user, docModifier }: IContext) { await checkPermission(doc.type, user, 'boardsAdd'); - const board = await Boards.createBoard({ userId: user._id, ...doc }); + + const board = await Boards.createBoard(docModifier({ userId: user._id, ...doc })); await putCreateLog( { @@ -40,11 +41,11 @@ const boardMutations = { /** * Edit board */ - async boardsEdit(_root, { _id, ...doc }: IBoardsEdit, { user }: { user: IUserDocument }) { + async boardsEdit(_root, { _id, ...doc }: IBoardsEdit, { user, docModifier }: IContext) { await checkPermission(doc.type, user, 'boardsEdit'); const board = await Boards.findOne({ _id }); - const updated = await Boards.updateBoard(_id, doc); + const updated = await Boards.updateBoard(_id, docModifier(doc)); if (board) { await putUpdateLog( @@ -64,7 +65,7 @@ const boardMutations = { /** * Remove board */ - async boardsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async boardsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const board = await Boards.findOne({ _id }); if (board) { @@ -88,7 +89,7 @@ const boardMutations = { /** * Create new pipeline */ - async pipelinesAdd(_root, { stages, ...doc }: IPipelinesAdd, { user }: { user: IUserDocument }) { + async pipelinesAdd(_root, { stages, ...doc }: IPipelinesAdd, { user }: IContext) { await checkPermission(doc.type, user, 'pipelinesAdd'); return Pipelines.createPipeline({ userId: user._id, ...doc }, stages); @@ -97,7 +98,7 @@ const boardMutations = { /** * Edit pipeline */ - async pipelinesEdit(_root, { _id, stages, ...doc }: IPipelinesEdit, { user }: { user: IUserDocument }) { + async pipelinesEdit(_root, { _id, stages, ...doc }: IPipelinesEdit, { user }: IContext) { await checkPermission(doc.type, user, 'pipelinesEdit'); return Pipelines.updatePipeline(_id, doc, stages); @@ -113,11 +114,7 @@ const boardMutations = { /** * Watch pipeline */ - async pipelinesWatch( - _root, - { _id, isAdd, type }: { _id: string; isAdd: boolean; type: string }, - { user }: { user: IUserDocument }, - ) { + async pipelinesWatch(_root, { _id, isAdd, type }: { _id: string; isAdd: boolean; type: string }, { user }: IContext) { await checkPermission(type, user, 'pipelinesWatch'); const pipeline = await Pipelines.findOne({ _id }); @@ -132,7 +129,7 @@ const boardMutations = { /** * Remove pipeline */ - async pipelinesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async pipelinesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const pipeline = await Pipelines.findOne({ _id }); if (pipeline) { diff --git a/src/data/resolvers/mutations/brands.ts b/src/data/resolvers/mutations/brands.ts index 0b3d651f2..a95335716 100644 --- a/src/data/resolvers/mutations/brands.ts +++ b/src/data/resolvers/mutations/brands.ts @@ -1,7 +1,7 @@ import { Brands } from '../../../db/models'; import { IBrand, IBrandEmailConfig } from '../../../db/models/definitions/brands'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IBrandsEdit extends IBrand { @@ -12,7 +12,7 @@ const brandMutations = { /** * Create new brand */ - async brandsAdd(_root, doc: IBrand, { user }: { user: IUserDocument }) { + async brandsAdd(_root, doc: IBrand, { user }: IContext) { const brand = await Brands.createBrand({ userId: user._id, ...doc }); await putCreateLog( @@ -31,7 +31,7 @@ const brandMutations = { /** * Update brand */ - async brandsEdit(_root, { _id, ...fields }: IBrandsEdit, { user }: { user: IUserDocument }) { + async brandsEdit(_root, { _id, ...fields }: IBrandsEdit, { user }: IContext) { const brand = await Brands.findOne({ _id }); const updated = await Brands.updateBrand(_id, fields); @@ -53,7 +53,7 @@ const brandMutations = { /** * Delete brand */ - async brandsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async brandsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const brand = await Brands.findOne({ _id }); const removed = await Brands.removeBrand(_id); @@ -77,7 +77,7 @@ const brandMutations = { async brandsConfigEmail( _root, { _id, emailConfig }: { _id: string; emailConfig: IBrandEmailConfig }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const brand = await Brands.findOne({ _id }); const updated = await Brands.updateEmailConfig(_id, emailConfig); diff --git a/src/data/resolvers/mutations/channels.ts b/src/data/resolvers/mutations/channels.ts index 2882fd220..93de7e0fa 100644 --- a/src/data/resolvers/mutations/channels.ts +++ b/src/data/resolvers/mutations/channels.ts @@ -3,6 +3,7 @@ import { IChannel, IChannelDocument } from '../../../db/models/definitions/chann import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import utils, { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; import { checkUserIds } from './notifications'; @@ -42,7 +43,7 @@ const channelMutations = { /** * Create a new channel and send notifications to its members bar the creator */ - async channelsAdd(_root, doc: IChannel, { user }: { user: IUserDocument }) { + async channelsAdd(_root, doc: IChannel, { user }: IContext) { const channel = await Channels.createChannel(doc, user._id); await sendChannelNotifications(channel, 'invited', user); @@ -63,7 +64,7 @@ const channelMutations = { /** * Update channel data */ - async channelsEdit(_root, { _id, ...doc }: IChannelsEdit, { user }: { user: IUserDocument }) { + async channelsEdit(_root, { _id, ...doc }: IChannelsEdit, { user }: IContext) { const channel = await Channels.findOne({ _id }); if (!channel) { @@ -97,7 +98,7 @@ const channelMutations = { /** * Remove a channel */ - async channelsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async channelsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const channel = await Channels.findOne({ _id }); if (!channel) { diff --git a/src/data/resolvers/mutations/companies.ts b/src/data/resolvers/mutations/companies.ts index 2503c41d6..4e20a7b7e 100644 --- a/src/data/resolvers/mutations/companies.ts +++ b/src/data/resolvers/mutations/companies.ts @@ -1,7 +1,7 @@ import { Companies } from '../../../db/models'; import { ICompany } from '../../../db/models/definitions/companies'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface ICompaniesEdit extends ICompany { @@ -12,8 +12,8 @@ const companyMutations = { /** * Create new company also adds Company registration log */ - async companiesAdd(_root, doc: ICompany, { user }: { user: IUserDocument }) { - const company = await Companies.createCompany(doc, user); + async companiesAdd(_root, doc: ICompany, { user, docModifier }: IContext) { + const company = await Companies.createCompany(docModifier(doc), user); await putCreateLog( { @@ -31,9 +31,9 @@ const companyMutations = { /** * Updates a company */ - async companiesEdit(_root, { _id, ...doc }: ICompaniesEdit, { user }: { user: IUserDocument }) { + async companiesEdit(_root, { _id, ...doc }: ICompaniesEdit, { user, docModifier }: IContext) { const company = await Companies.findOne({ _id }); - const updated = await Companies.updateCompany(_id, doc); + const updated = await Companies.updateCompany(_id, docModifier(doc)); if (company) { await putUpdateLog( @@ -60,7 +60,7 @@ const companyMutations = { /** * Remove companies */ - async companiesRemove(_root, { companyIds }: { companyIds: string[] }, { user }: { user: IUserDocument }) { + async companiesRemove(_root, { companyIds }: { companyIds: string[] }, { user }: IContext) { for (const companyId of companyIds) { const company = await Companies.findOne({ _id: companyId }); // Removing every company and modules associated with diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 058ff3c6f..853614eca 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -10,6 +10,7 @@ import { debugExternalApi } from '../../../debuggers'; import { graphqlPubsub } from '../../../pubsub'; import { IntegrationsAPI } from '../../dataSources'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import utils from '../../utils'; interface IConversationMessageAdd { @@ -153,7 +154,7 @@ const conversationMutations = { /** * Create new message in conversation */ - async conversationMessageAdd(_root, doc: IConversationMessageAdd, { user }: { user: IUserDocument }) { + async conversationMessageAdd(_root, doc: IConversationMessageAdd, { user }: IContext) { const conversation = await Conversations.findOne({ _id: doc.conversationId, }); @@ -243,7 +244,7 @@ const conversationMutations = { async conversationsAssign( _root, { conversationIds, assignedUserId }: { conversationIds: string[]; assignedUserId: string }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const conversations: IConversationDocument[] = await Conversations.assignUserConversation( conversationIds, @@ -261,7 +262,7 @@ const conversationMutations = { /** * Unassign employee from conversation */ - async conversationsUnassign(_root, { _ids }: { _ids: string[] }, { user }: { user: IUserDocument }) { + async conversationsUnassign(_root, { _ids }: { _ids: string[] }, { user }: IContext) { const oldConversations = await Conversations.find({ _id: { $in: _ids } }); const updatedConversations = await Conversations.unassignUserConversation(_ids); @@ -280,11 +281,7 @@ const conversationMutations = { /** * Change conversation status */ - async conversationsChangeStatus( - _root, - { _ids, status }: { _ids: string[]; status: string }, - { user }: { user: IUserDocument }, - ) { + async conversationsChangeStatus(_root, { _ids, status }: { _ids: string[]; status: string }, { user }: IContext) { const { conversations } = await Conversations.checkExistanceConversations(_ids); await Conversations.changeStatusConversation(_ids, status, user._id); @@ -349,7 +346,7 @@ const conversationMutations = { /** * Conversation mark as read */ - async conversationMarkAsRead(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async conversationMarkAsRead(_root, { _id }: { _id: string }, { user }: IContext) { return Conversations.markAsReadConversation(_id, user._id); }, }; diff --git a/src/data/resolvers/mutations/customers.ts b/src/data/resolvers/mutations/customers.ts index ca2b1bd8d..849aca689 100644 --- a/src/data/resolvers/mutations/customers.ts +++ b/src/data/resolvers/mutations/customers.ts @@ -1,8 +1,7 @@ import { Customers } from '../../../db/models'; - import { ICustomer } from '../../../db/models/definitions/customers'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface ICustomersEdit extends ICustomer { @@ -13,8 +12,8 @@ const customerMutations = { /** * Create new customer also adds Customer registration log */ - async customersAdd(_root, doc: ICustomer, { user }: { user: IUserDocument }) { - const customer = await Customers.createCustomer(doc, user); + async customersAdd(_root, doc: ICustomer, { user, docModifier }: IContext) { + const customer = await Customers.createCustomer(docModifier(doc), user); await putCreateLog( { @@ -32,9 +31,9 @@ const customerMutations = { /** * Update customer */ - async customersEdit(_root, { _id, ...doc }: ICustomersEdit, { user }: { user: IUserDocument }) { + async customersEdit(_root, { _id, ...doc }: ICustomersEdit, { user, docModifier }: IContext) { const customer = await Customers.findOne({ _id }); - const updated = await Customers.updateCustomer(_id, doc); + const updated = await Customers.updateCustomer(_id, docModifier(doc)); if (customer) { await putUpdateLog( @@ -68,7 +67,7 @@ const customerMutations = { /** * Remove customers */ - async customersRemove(_root, { customerIds }: { customerIds: string[] }, { user }: { user: IUserDocument }) { + async customersRemove(_root, { customerIds }: { customerIds: string[] }, { user }: IContext) { for (const customerId of customerIds) { // Removing every customer and modules associated with const customer = await Customers.findOne({ _id: customerId }); diff --git a/src/data/resolvers/mutations/deals.ts b/src/data/resolvers/mutations/deals.ts index 148b39097..5a260f9c5 100644 --- a/src/data/resolvers/mutations/deals.ts +++ b/src/data/resolvers/mutations/deals.ts @@ -2,8 +2,8 @@ import { Deals } from '../../../db/models'; import { IOrderInput } from '../../../db/models/definitions/boards'; import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { IDeal } from '../../../db/models/definitions/deals'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; import { itemsChange, sendNotifications } from '../boardUtils'; import { checkUserIds } from './notifications'; @@ -16,8 +16,9 @@ const dealMutations = { /** * Create new deal */ - async dealsAdd(_root, doc: IDeal, { user }: { user: IUserDocument }) { + async dealsAdd(_root, doc: IDeal, { user }: IContext) { doc.initialStageId = doc.stageId; + const deal = await Deals.createDeal({ ...doc, modifiedBy: user._id, @@ -48,7 +49,7 @@ const dealMutations = { /** * Edit deal */ - async dealsEdit(_root, { _id, ...doc }: IDealsEdit, { user }) { + async dealsEdit(_root, { _id, ...doc }: IDealsEdit, { user }: IContext) { const oldDeal = await Deals.findOne({ _id }); if (!oldDeal) { @@ -94,7 +95,7 @@ const dealMutations = { async dealsChange( _root, { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const deal = await Deals.findOne({ _id }); @@ -132,7 +133,7 @@ const dealMutations = { /** * Remove deal */ - async dealsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async dealsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const deal = await Deals.findOne({ _id }); if (!deal) { @@ -165,7 +166,7 @@ const dealMutations = { /** * Watch deal */ - async dealsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + async dealsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { const deal = await Deals.findOne({ _id }); if (!deal) { diff --git a/src/data/resolvers/mutations/emailTemplates.ts b/src/data/resolvers/mutations/emailTemplates.ts index 5b22a6a22..d98d8eaee 100644 --- a/src/data/resolvers/mutations/emailTemplates.ts +++ b/src/data/resolvers/mutations/emailTemplates.ts @@ -1,7 +1,7 @@ import { EmailTemplates } from '../../../db/models'; import { IEmailTemplate } from '../../../db/models/definitions/emailTemplates'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IEmailTemplatesEdit extends IEmailTemplate { @@ -12,8 +12,8 @@ const emailTemplateMutations = { /** * Create new email template */ - async emailTemplatesAdd(_root, doc: IEmailTemplate, { user }: { user: IUserDocument }) { - const template = await EmailTemplates.create(doc); + async emailTemplatesAdd(_root, doc: IEmailTemplate, { user, docModifier }: IContext) { + const template = await EmailTemplates.create(docModifier(doc)); if (template) { await putCreateLog( @@ -33,9 +33,9 @@ const emailTemplateMutations = { /** * Update email template */ - async emailTemplatesEdit(_root, { _id, ...fields }: IEmailTemplatesEdit, { user }: { user: IUserDocument }) { + async emailTemplatesEdit(_root, { _id, ...fields }: IEmailTemplatesEdit, { user, docModifier }: IContext) { const template = await EmailTemplates.findOne({ _id }); - const updated = await EmailTemplates.updateEmailTemplate(_id, fields); + const updated = await EmailTemplates.updateEmailTemplate(_id, docModifier(fields)); if (template) { await putUpdateLog( @@ -55,7 +55,7 @@ const emailTemplateMutations = { /** * Delete email template */ - async emailTemplatesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async emailTemplatesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const template = await EmailTemplates.findOne({ _id }); const removed = await EmailTemplates.removeEmailTemplate(_id); diff --git a/src/data/resolvers/mutations/engages.ts b/src/data/resolvers/mutations/engages.ts index ddf3badb5..51b72a1d8 100644 --- a/src/data/resolvers/mutations/engages.ts +++ b/src/data/resolvers/mutations/engages.ts @@ -1,9 +1,9 @@ import { EngageMessages } from '../../../db/models'; import { IEngageMessage } from '../../../db/models/definitions/engages'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { debugExternalApi } from '../../../debuggers'; import { MESSAGE_KINDS } from '../../constants'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { fetchCronsApi, putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; import { send } from './engageUtils'; @@ -15,8 +15,8 @@ const engageMutations = { /** * Create new message */ - async engageMessageAdd(_root, doc: IEngageMessage, { user }: { user: IUserDocument }) { - const engageMessage = await EngageMessages.createEngageMessage(doc); + async engageMessageAdd(_root, doc: IEngageMessage, { user, docModifier }: IContext) { + const engageMessage = await EngageMessages.createEngageMessage(docModifier(doc)); await send(engageMessage); @@ -38,9 +38,9 @@ const engageMutations = { /** * Edit message */ - async engageMessageEdit(_root, { _id, ...doc }: IEngageMessageEdit, { user }: { user: IUserDocument }) { + async engageMessageEdit(_root, { _id, ...doc }: IEngageMessageEdit, { user, docModifier }: IContext) { const engageMessage = await EngageMessages.findOne({ _id }); - const updated = await EngageMessages.updateEngageMessage(_id, doc); + const updated = await EngageMessages.updateEngageMessage(_id, docModifier(doc)); try { await fetchCronsApi({ path: '/update-or-remove-schedule', method: 'POST', body: { _id, update: 'true' } }); @@ -66,7 +66,7 @@ const engageMutations = { /** * Remove message */ - async engageMessageRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async engageMessageRemove(_root, { _id }: { _id: string }, { user }: IContext) { const engageMessage = await EngageMessages.findOne({ _id }); try { diff --git a/src/data/resolvers/mutations/fields.ts b/src/data/resolvers/mutations/fields.ts index dcb2c3c67..73bd98577 100644 --- a/src/data/resolvers/mutations/fields.ts +++ b/src/data/resolvers/mutations/fields.ts @@ -1,8 +1,8 @@ import { Fields, FieldsGroups } from '../../../db/models'; import { IField, IFieldGroup } from '../../../db/models/definitions/fields'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { IOrderInput } from '../../../db/models/Fields'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; interface IFieldsEdit extends IField { _id: string; @@ -16,14 +16,14 @@ const fieldMutations = { /** * Adds field object */ - fieldsAdd(_root, args: IField, { user }: { user: IUserDocument }) { + fieldsAdd(_root, args: IField, { user }: IContext) { return Fields.createField({ ...args, lastUpdatedUserId: user._id }); }, /** * Updates field object */ - fieldsEdit(_root, { _id, ...doc }: IFieldsEdit, { user }: { user: IUserDocument }) { + fieldsEdit(_root, { _id, ...doc }: IFieldsEdit, { user }: IContext) { return Fields.updateField(_id, { ...doc, lastUpdatedUserId: user._id }); }, @@ -44,11 +44,7 @@ const fieldMutations = { /** * Update field's visible */ - fieldsUpdateVisible( - _root, - { _id, isVisible }: { _id: string; isVisible: boolean }, - { user }: { user: IUserDocument }, - ) { + fieldsUpdateVisible(_root, { _id, isVisible }: { _id: string; isVisible: boolean }, { user }: IContext) { return Fields.updateFieldsVisible(_id, isVisible, user._id); }, }; @@ -57,14 +53,14 @@ const fieldsGroupsMutations = { /** * Create a new group for fields */ - fieldsGroupsAdd(_root, doc: IFieldGroup, { user }: { user: IUserDocument }) { + fieldsGroupsAdd(_root, doc: IFieldGroup, { user }: IContext) { return FieldsGroups.createGroup({ ...doc, lastUpdatedUserId: user._id }); }, /** * Update group for fields */ - fieldsGroupsEdit(_root, { _id, ...doc }: IFieldsGroupsEdit, { user }: { user: IUserDocument }) { + fieldsGroupsEdit(_root, { _id, ...doc }: IFieldsGroupsEdit, { user }: IContext) { return FieldsGroups.updateGroup(_id, { ...doc, lastUpdatedUserId: user._id, @@ -81,11 +77,7 @@ const fieldsGroupsMutations = { /** * Update field group's visible */ - fieldsGroupsUpdateVisible( - _root, - { _id, isVisible }: { _id: string; isVisible: boolean }, - { user }: { user: IUserDocument }, - ) { + fieldsGroupsUpdateVisible(_root, { _id, isVisible }: { _id: string; isVisible: boolean }, { user }: IContext) { return FieldsGroups.updateGroupVisible(_id, isVisible, user._id); }, }; diff --git a/src/data/resolvers/mutations/forms.ts b/src/data/resolvers/mutations/forms.ts index aaf003495..b599e3283 100644 --- a/src/data/resolvers/mutations/forms.ts +++ b/src/data/resolvers/mutations/forms.ts @@ -1,7 +1,7 @@ import { Forms } from '../../../db/models'; import { IForm } from '../../../db/models/definitions/forms'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; interface IFormsEdit extends IForm { _id: string; @@ -11,15 +11,15 @@ const formMutations = { /** * Create a new form */ - formsAdd(_root, doc: IForm, { user }: { user: IUserDocument }) { - return Forms.createForm(doc, user._id); + formsAdd(_root, doc: IForm, { user, docModifier }: IContext) { + return Forms.createForm(docModifier(doc), user._id); }, /** * Update form data */ - formsEdit(_root, { _id, ...doc }: IFormsEdit) { - return Forms.updateForm(_id, doc); + formsEdit(_root, { _id, ...doc }: IFormsEdit, { docModifier }: IContext) { + return Forms.updateForm(_id, docModifier(doc)); }, }; diff --git a/src/data/resolvers/mutations/importHistory.ts b/src/data/resolvers/mutations/importHistory.ts index ec664bbd9..a33d8fbe3 100644 --- a/src/data/resolvers/mutations/importHistory.ts +++ b/src/data/resolvers/mutations/importHistory.ts @@ -1,6 +1,6 @@ import { ImportHistory } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { fetchWorkersApi, putDeleteLog } from '../../utils'; const importHistoryMutations = { @@ -8,7 +8,7 @@ const importHistoryMutations = { * Removes a history * @param {string} param1._id ImportHistory id */ - async importHistoriesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async importHistoriesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const importHistory = await ImportHistory.findOne({ _id }); if (!importHistory) { diff --git a/src/data/resolvers/mutations/integrations.ts b/src/data/resolvers/mutations/integrations.ts index 17fa01999..1ce278792 100644 --- a/src/data/resolvers/mutations/integrations.ts +++ b/src/data/resolvers/mutations/integrations.ts @@ -1,9 +1,9 @@ import { Integrations } from '../../../db/models'; import { IIntegration, IMessengerData, IUiOptions } from '../../../db/models/definitions/integrations'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { IExternalIntegrationParams, IMessengerIntegration } from '../../../db/models/Integrations'; import { IntegrationsAPI } from '../../dataSources'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IEditMessengerIntegration extends IMessengerIntegration { @@ -20,7 +20,7 @@ const integrationMutations = { /** * Create a new messenger integration */ - async integrationsCreateMessengerIntegration(_root, doc: IMessengerIntegration, { user }: { user: IUserDocument }) { + async integrationsCreateMessengerIntegration(_root, doc: IMessengerIntegration, { user }: IContext) { const integration = await Integrations.createMessengerIntegration(doc); if (integration) { @@ -41,11 +41,7 @@ const integrationMutations = { /** * Update messenger integration */ - async integrationsEditMessengerIntegration( - _root, - { _id, ...fields }: IEditMessengerIntegration, - { user }: { user: IUserDocument }, - ) { + async integrationsEditMessengerIntegration(_root, { _id, ...fields }: IEditMessengerIntegration, { user }: IContext) { const integration = await Integrations.findOne({ _id }); const updated = await Integrations.updateMessengerIntegration(_id, fields); @@ -115,7 +111,7 @@ const integrationMutations = { /** * Delete an integration */ - async integrationsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async integrationsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const integration = await Integrations.findOne({ _id }); if (integration) { diff --git a/src/data/resolvers/mutations/internalNotes.ts b/src/data/resolvers/mutations/internalNotes.ts index 242979b5d..3dd9520a1 100644 --- a/src/data/resolvers/mutations/internalNotes.ts +++ b/src/data/resolvers/mutations/internalNotes.ts @@ -1,8 +1,8 @@ import { Companies, Customers, Deals, InternalNotes, Pipelines, Stages, Tasks, Tickets } from '../../../db/models'; import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { IInternalNote } from '../../../db/models/definitions/internalNotes'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import utils, { ISendNotification, putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IInternalNotesEdit extends IInternalNote { @@ -13,7 +13,7 @@ const internalNoteMutations = { /** * Adds internalNote object and also adds an activity log */ - async internalNotesAdd(_root, args: IInternalNote, { user }: { user: IUserDocument }) { + async internalNotesAdd(_root, args: IInternalNote, { user }: IContext) { const notifDoc: ISendNotification = { title: `${args.contentType.toUpperCase()} updated`, createdUser: user, @@ -105,7 +105,7 @@ const internalNoteMutations = { /** * Updates internalNote object */ - async internalNotesEdit(_root, { _id, ...doc }: IInternalNotesEdit, { user }: { user: IUserDocument }) { + async internalNotesEdit(_root, { _id, ...doc }: IInternalNotesEdit, { user }: IContext) { const internalNote = await InternalNotes.findOne({ _id }); const updated = await InternalNotes.updateInternalNote(_id, doc); @@ -127,7 +127,7 @@ const internalNoteMutations = { /** * Remove a channel */ - async internalNotesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async internalNotesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const internalNote = await InternalNotes.findOne({ _id }); const removed = await InternalNotes.removeInternalNote(_id); diff --git a/src/data/resolvers/mutations/knowledgeBase.ts b/src/data/resolvers/mutations/knowledgeBase.ts index 46bb270a8..11743a74b 100644 --- a/src/data/resolvers/mutations/knowledgeBase.ts +++ b/src/data/resolvers/mutations/knowledgeBase.ts @@ -1,17 +1,16 @@ import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; - import { ITopic } from '../../../db/models/definitions/knowledgebase'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { IArticleCreate, ICategoryCreate } from '../../../db/models/KnowledgeBase'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; const knowledgeBaseMutations = { /** * Create topic document */ - async knowledgeBaseTopicsAdd(_root, { doc }: { doc: ITopic }, { user }: { user: IUserDocument }) { - const topic = await KnowledgeBaseTopics.createDoc(doc, user._id); + async knowledgeBaseTopicsAdd(_root, { doc }: { doc: ITopic }, { user, docModifier }: IContext) { + const topic = await KnowledgeBaseTopics.createDoc(docModifier(doc), user._id); if (topic) { await putCreateLog( @@ -31,9 +30,9 @@ const knowledgeBaseMutations = { /** * Update topic document */ - async knowledgeBaseTopicsEdit(_root, { _id, doc }: { _id: string; doc: ITopic }, { user }: { user: IUserDocument }) { + async knowledgeBaseTopicsEdit(_root, { _id, doc }: { _id: string; doc: ITopic }, { user, docModifier }: IContext) { const topic = await KnowledgeBaseTopics.findOne({ _id }); - const updated = await KnowledgeBaseTopics.updateDoc(_id, doc, user._id); + const updated = await KnowledgeBaseTopics.updateDoc(_id, docModifier(doc), user._id); if (topic) { await putUpdateLog( @@ -53,7 +52,7 @@ const knowledgeBaseMutations = { /** * Remove topic document */ - async knowledgeBaseTopicsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async knowledgeBaseTopicsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const topic = await KnowledgeBaseTopics.findOne({ _id }); const removed = await KnowledgeBaseTopics.removeDoc(_id); @@ -74,7 +73,7 @@ const knowledgeBaseMutations = { /** * Create category document */ - async knowledgeBaseCategoriesAdd(_root, { doc }: { doc: ICategoryCreate }, { user }: { user: IUserDocument }) { + async knowledgeBaseCategoriesAdd(_root, { doc }: { doc: ICategoryCreate }, { user }: IContext) { const kbCategory = await KnowledgeBaseCategories.createDoc(doc, user._id); await putCreateLog( @@ -93,11 +92,7 @@ const knowledgeBaseMutations = { /** * Update category document */ - async knowledgeBaseCategoriesEdit( - _root, - { _id, doc }: { _id: string; doc: ICategoryCreate }, - { user }: { user: IUserDocument }, - ) { + async knowledgeBaseCategoriesEdit(_root, { _id, doc }: { _id: string; doc: ICategoryCreate }, { user }: IContext) { const kbCategory = await KnowledgeBaseCategories.findOne({ _id }); const updated = await KnowledgeBaseCategories.updateDoc(_id, doc, user._id); @@ -119,7 +114,7 @@ const knowledgeBaseMutations = { /** * Remove category document */ - async knowledgeBaseCategoriesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async knowledgeBaseCategoriesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const kbCategory = await KnowledgeBaseCategories.findOne({ _id }); const removed = await KnowledgeBaseCategories.removeDoc(_id); @@ -140,7 +135,7 @@ const knowledgeBaseMutations = { /** * Create article document */ - async knowledgeBaseArticlesAdd(_root, { doc }: { doc: IArticleCreate }, { user }: { user: IUserDocument }) { + async knowledgeBaseArticlesAdd(_root, { doc }: { doc: IArticleCreate }, { user }: IContext) { const kbArticle = await KnowledgeBaseArticles.createDoc(doc, user._id); await putCreateLog( @@ -159,11 +154,7 @@ const knowledgeBaseMutations = { /** * Update article document */ - async knowledgeBaseArticlesEdit( - _root, - { _id, doc }: { _id: string; doc: IArticleCreate }, - { user }: { user: IUserDocument }, - ) { + async knowledgeBaseArticlesEdit(_root, { _id, doc }: { _id: string; doc: IArticleCreate }, { user }: IContext) { const kbArticle = await KnowledgeBaseArticles.findOne({ _id }); const updated = await KnowledgeBaseArticles.updateDoc(_id, doc, user._id); @@ -185,7 +176,7 @@ const knowledgeBaseMutations = { /** * Remove article document */ - async knowledgeBaseArticlesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async knowledgeBaseArticlesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const kbArticle = await KnowledgeBaseArticles.findOne({ _id }); const removed = await KnowledgeBaseArticles.removeDoc(_id); diff --git a/src/data/resolvers/mutations/messengerApps.ts b/src/data/resolvers/mutations/messengerApps.ts index 964e641eb..9c13463a2 100644 --- a/src/data/resolvers/mutations/messengerApps.ts +++ b/src/data/resolvers/mutations/messengerApps.ts @@ -1,6 +1,6 @@ import { Forms, MessengerApps } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog } from '../../utils'; const messengerAppMutations = { @@ -10,17 +10,20 @@ const messengerAppMutations = { * @param {string} params.integrationId Integration * @param {string} params.topicId Topic */ - async messengerAppsAddKnowledgebase(_root, params, { user }: { user: IUserDocument }) { + async messengerAppsAddKnowledgebase(_root, params, { user, docModifier }: IContext) { const { name, integrationId, topicId } = params; - const kb = await MessengerApps.createApp({ - name, - kind: 'knowledgebase', - showInInbox: false, - credentials: { - integrationId, - topicId, - }, - }); + + const kb = await MessengerApps.createApp( + docModifier({ + name, + kind: 'knowledgebase', + showInInbox: false, + credentials: { + integrationId, + topicId, + }, + }), + ); if (kb) { await putCreateLog( @@ -43,7 +46,7 @@ const messengerAppMutations = { * @param {string} params.integrationId Integration * @param {string} params.formId Form */ - async messengerAppsAddLead(_root, params, { user }: { user: IUserDocument }) { + async messengerAppsAddLead(_root, params, { user, docModifier }: IContext) { const { name, integrationId, formId } = params; const form = await Forms.findOne({ _id: formId }); @@ -51,15 +54,17 @@ const messengerAppMutations = { throw new Error('Form not found'); } - const lead = await MessengerApps.createApp({ - name, - kind: 'lead', - showInInbox: false, - credentials: { - integrationId, - formCode: form.code || '', - }, - }); + const lead = await MessengerApps.createApp( + docModifier({ + name, + kind: 'lead', + showInInbox: false, + credentials: { + integrationId, + formCode: form.code || '', + }, + }), + ); if (lead) { await putCreateLog( @@ -79,7 +84,7 @@ const messengerAppMutations = { /* * Remove app */ - async messengerAppsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async messengerAppsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const messengerApp = await MessengerApps.findOne({ _id }); const removed = await MessengerApps.deleteOne({ _id }); diff --git a/src/data/resolvers/mutations/notifications.ts b/src/data/resolvers/mutations/notifications.ts index 55ec61a11..e08a39de4 100644 --- a/src/data/resolvers/mutations/notifications.ts +++ b/src/data/resolvers/mutations/notifications.ts @@ -1,8 +1,8 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; import { IConfig } from '../../../db/models/definitions/notifications'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { graphqlPubsub } from '../../../pubsub'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; /** * Check user ids whether its added or removed from array of ids @@ -19,14 +19,14 @@ const notificationMutations = { /** * Save notification configuration */ - notificationsSaveConfig(_root, doc: IConfig, { user }: { user: IUserDocument }) { + notificationsSaveConfig(_root, doc: IConfig, { user }: IContext) { return NotificationConfigurations.createOrUpdateConfiguration(doc, user); }, /** * Marks notification as read */ - notificationsMarkAsRead(_root, { _ids }: { _ids: string[] }, { user }: { user: IUserDocument }) { + notificationsMarkAsRead(_root, { _ids }: { _ids: string[] }, { user }: IContext) { // notify subscription graphqlPubsub.publish('notificationsChanged', ''); diff --git a/src/data/resolvers/mutations/permissions.ts b/src/data/resolvers/mutations/permissions.ts index 78df0a1a3..ed27ce642 100644 --- a/src/data/resolvers/mutations/permissions.ts +++ b/src/data/resolvers/mutations/permissions.ts @@ -1,8 +1,8 @@ import { Permissions, Users, UsersGroups } from '../../../db/models'; import { IPermissionParams, IUserGroup } from '../../../db/models/definitions/permissions'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { resetPermissionsCache } from '../../permissions/utils'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; const permissionMutations = { @@ -14,7 +14,7 @@ const permissionMutations = { * @param {Boolean} doc.allowed * @return {Promise} newly created permission object */ - async permissionsAdd(_root, doc: IPermissionParams, { user }: { user: IUserDocument }) { + async permissionsAdd(_root, doc: IPermissionParams, { user }: IContext) { const result = await Permissions.createPermission(doc); if (result && result.length > 0) { @@ -59,7 +59,7 @@ const permissionMutations = { * @param {[String]} ids * @return {Promise} */ - async permissionsRemove(_root, { ids }: { ids: string[] }, { user }: { user: IUserDocument }) { + async permissionsRemove(_root, { ids }: { ids: string[] }, { user }: IContext) { const permissions = await Permissions.find({ _id: { $in: ids } }); const result = await Permissions.removePermission(ids); @@ -107,11 +107,7 @@ const usersGroupMutations = { * @param {String} doc.description * @return {Promise} newly created group object */ - async usersGroupsAdd( - _root, - { memberIds, ...doc }: IUserGroup & { memberIds?: string[] }, - { user }: { user: IUserDocument }, - ) { + async usersGroupsAdd(_root, { memberIds, ...doc }: IUserGroup & { memberIds?: string[] }, { user }: IContext) { const result = await UsersGroups.createGroup(doc, memberIds); if (result) { @@ -140,7 +136,7 @@ const usersGroupMutations = { async usersGroupsEdit( _root, { _id, memberIds, ...doc }: { _id: string; memberIds?: string[] } & IUserGroup, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const group = await UsersGroups.findOne({ _id }); const result = await UsersGroups.updateGroup(_id, doc, memberIds); @@ -167,7 +163,7 @@ const usersGroupMutations = { * @param {String} _id * @return {Promise} */ - async usersGroupsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async usersGroupsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const group = await UsersGroups.findOne({ _id }); const result = await UsersGroups.removeGroup(_id); diff --git a/src/data/resolvers/mutations/products.ts b/src/data/resolvers/mutations/products.ts index 94167d3d0..18d52d452 100644 --- a/src/data/resolvers/mutations/products.ts +++ b/src/data/resolvers/mutations/products.ts @@ -1,7 +1,7 @@ import { Products } from '../../../db/models'; import { IProduct } from '../../../db/models/definitions/deals'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IProductsEdit extends IProduct { @@ -13,8 +13,8 @@ const productMutations = { * Creates a new product * @param {Object} doc Product document */ - async productsAdd(_root, doc: IProduct, { user }: { user: IUserDocument }) { - const product = await Products.createProduct(doc); + async productsAdd(_root, doc: IProduct, { user, docModifier }: IContext) { + const product = await Products.createProduct(docModifier(doc)); if (product) { await putCreateLog( @@ -36,9 +36,9 @@ const productMutations = { * @param {string} param2._id Product id * @param {Object} param2.doc Product info */ - async productsEdit(_root, { _id, ...doc }: IProductsEdit, { user }: { user: IUserDocument }) { + async productsEdit(_root, { _id, ...doc }: IProductsEdit, { user, docModifier }: IContext) { const product = await Products.findOne({ _id }); - const updated = await Products.updateProduct(_id, doc); + const updated = await Products.updateProduct(_id, docModifier(doc)); if (product) { await putUpdateLog( @@ -59,7 +59,7 @@ const productMutations = { * Removes a product * @param {string} param1._id Product id */ - async productsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async productsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const product = await Products.findOne({ _id }); const removed = await Products.removeProduct(_id); diff --git a/src/data/resolvers/mutations/responseTemplates.ts b/src/data/resolvers/mutations/responseTemplates.ts index cac7b02cd..bc99dc921 100644 --- a/src/data/resolvers/mutations/responseTemplates.ts +++ b/src/data/resolvers/mutations/responseTemplates.ts @@ -1,7 +1,7 @@ import { ResponseTemplates } from '../../../db/models'; import { IResponseTemplate } from '../../../db/models/definitions/responseTemplates'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IResponseTemplatesEdit extends IResponseTemplate { @@ -12,8 +12,8 @@ const responseTemplateMutations = { /** * Create new response template */ - async responseTemplatesAdd(_root, doc: IResponseTemplate, { user }: { user: IUserDocument }) { - const template = await ResponseTemplates.create(doc); + async responseTemplatesAdd(_root, doc: IResponseTemplate, { user, docModifier }: IContext) { + const template = await ResponseTemplates.create(docModifier(doc)); if (template) { await putCreateLog( @@ -33,9 +33,9 @@ const responseTemplateMutations = { /** * Update response template */ - async responseTemplatesEdit(_root, { _id, ...fields }: IResponseTemplatesEdit, { user }: { user: IUserDocument }) { + async responseTemplatesEdit(_root, { _id, ...fields }: IResponseTemplatesEdit, { user, docModifier }: IContext) { const template = await ResponseTemplates.findOne({ _id }); - const updated = await ResponseTemplates.updateResponseTemplate(_id, fields); + const updated = await ResponseTemplates.updateResponseTemplate(_id, docModifier(fields)); if (template) { await putUpdateLog( @@ -55,7 +55,7 @@ const responseTemplateMutations = { /** * Delete response template */ - async responseTemplatesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async responseTemplatesRemove(_root, { _id }: { _id: string }, { user }: IContext) { const template = await ResponseTemplates.findOne({ _id }); const removed = await ResponseTemplates.removeResponseTemplate(_id); diff --git a/src/data/resolvers/mutations/scripts.ts b/src/data/resolvers/mutations/scripts.ts index c1f0a8b2b..a60414e64 100644 --- a/src/data/resolvers/mutations/scripts.ts +++ b/src/data/resolvers/mutations/scripts.ts @@ -1,7 +1,7 @@ import { Scripts } from '../../../db/models'; import { IScript } from '../../../db/models/definitions/scripts'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface IScriptsEdit extends IScript { @@ -12,8 +12,8 @@ const scriptMutations = { /** * Create new script */ - async scriptsAdd(_root, doc: IScript, { user }: { user: IUserDocument }) { - const script = await Scripts.createScript(doc); + async scriptsAdd(_root, doc: IScript, { user, docModifier }: IContext) { + const script = await Scripts.createScript(docModifier(doc)); if (script) { await putCreateLog( @@ -33,9 +33,9 @@ const scriptMutations = { /** * Update script */ - async scriptsEdit(_root, { _id, ...fields }: IScriptsEdit, { user }: { user: IUserDocument }) { + async scriptsEdit(_root, { _id, ...fields }: IScriptsEdit, { user, docModifier }: IContext) { const script = await Scripts.findOne({ _id }); - const updated = await Scripts.updateScript(_id, fields); + const updated = await Scripts.updateScript(_id, docModifier(fields)); if (script) { await putUpdateLog( @@ -55,7 +55,7 @@ const scriptMutations = { /** * Delete script */ - async scriptsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async scriptsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const script = await Scripts.findOne({ _id }); const removed = await Scripts.removeScript(_id); diff --git a/src/data/resolvers/mutations/segments.ts b/src/data/resolvers/mutations/segments.ts index 5adf23665..e5960b855 100644 --- a/src/data/resolvers/mutations/segments.ts +++ b/src/data/resolvers/mutations/segments.ts @@ -1,7 +1,7 @@ import { Segments } from '../../../db/models'; import { ISegment } from '../../../db/models/definitions/segments'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; interface ISegmentsEdit extends ISegment { @@ -12,8 +12,8 @@ const segmentMutations = { /** * Create new segment */ - async segmentsAdd(_root, doc: ISegment, { user }: { user: IUserDocument }) { - const segment = await Segments.createSegment(doc); + async segmentsAdd(_root, doc: ISegment, { user, docModifier }: IContext) { + const segment = await Segments.createSegment(docModifier(doc)); if (segment) { await putCreateLog( @@ -33,9 +33,9 @@ const segmentMutations = { /** * Update segment */ - async segmentsEdit(_root, { _id, ...doc }: ISegmentsEdit, { user }: { user: IUserDocument }) { + async segmentsEdit(_root, { _id, ...doc }: ISegmentsEdit, { user, docModifier }: IContext) { const segment = await Segments.findOne({ _id }); - const updated = await Segments.updateSegment(_id, doc); + const updated = await Segments.updateSegment(_id, docModifier(doc)); if (segment) { await putUpdateLog( @@ -55,7 +55,7 @@ const segmentMutations = { /** * Delete segment */ - async segmentsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async segmentsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const segment = await Segments.findOne({ _id }); const removed = await Segments.removeSegment(_id); diff --git a/src/data/resolvers/mutations/tags.ts b/src/data/resolvers/mutations/tags.ts index f84a824cc..9a3b3d333 100644 --- a/src/data/resolvers/mutations/tags.ts +++ b/src/data/resolvers/mutations/tags.ts @@ -1,7 +1,7 @@ import { Tags } from '../../../db/models'; import { ITag } from '../../../db/models/definitions/tags'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; import { publishConversationsChanged } from './conversations'; @@ -13,8 +13,8 @@ const tagMutations = { /** * Create new tag */ - async tagsAdd(_root, doc: ITag, { user }: { user: IUserDocument }) { - const tag = await Tags.createTag(doc); + async tagsAdd(_root, doc: ITag, { user, docModifier }: IContext) { + const tag = await Tags.createTag(docModifier(doc)); if (tag) { await putCreateLog( @@ -34,9 +34,9 @@ const tagMutations = { /** * Edit tag */ - async tagsEdit(_root, { _id, ...doc }: ITagsEdit, { user }: { user: IUserDocument }) { + async tagsEdit(_root, { _id, ...doc }: ITagsEdit, { user, docModifier }: IContext) { const tag = await Tags.findOne({ _id }); - const updated = await Tags.updateTag(_id, doc); + const updated = await Tags.updateTag(_id, docModifier(doc)); if (tag) { await putUpdateLog( @@ -56,7 +56,7 @@ const tagMutations = { /** * Remove tag */ - async tagsRemove(_root, { ids }: { ids: string[] }, { user }: { user: IUserDocument }) { + async tagsRemove(_root, { ids }: { ids: string[] }, { user }: IContext) { const tags = await Tags.find({ _id: { $in: ids } }); const removed = await Tags.removeTag(ids); diff --git a/src/data/resolvers/mutations/tasks.ts b/src/data/resolvers/mutations/tasks.ts index 3a60b5780..4035ea756 100644 --- a/src/data/resolvers/mutations/tasks.ts +++ b/src/data/resolvers/mutations/tasks.ts @@ -2,8 +2,8 @@ import { Tasks } from '../../../db/models'; import { IOrderInput } from '../../../db/models/definitions/boards'; import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { ITask } from '../../../db/models/definitions/tasks'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { itemsChange, sendNotifications } from '../boardUtils'; import { checkUserIds } from './notifications'; @@ -15,7 +15,7 @@ const taskMutations = { /** * Create new task */ - async tasksAdd(_root, doc: ITask, { user }: { user: IUserDocument }) { + async tasksAdd(_root, doc: ITask, { user }: IContext) { const task = await Tasks.createTask({ ...doc, modifiedBy: user._id, @@ -36,7 +36,7 @@ const taskMutations = { /** * Edit task */ - async tasksEdit(_root, { _id, ...doc }: ITasksEdit, { user }) { + async tasksEdit(_root, { _id, ...doc }: ITasksEdit, { user }: IContext) { const oldTask = await Tasks.findOne({ _id }); if (!oldTask) { @@ -69,7 +69,7 @@ const taskMutations = { async tasksChange( _root, { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const task = await Tasks.updateTask(_id, { modifiedAt: new Date(), @@ -101,7 +101,7 @@ const taskMutations = { /** * Remove task */ - async tasksRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async tasksRemove(_root, { _id }: { _id: string }, { user }: IContext) { const task = await Tasks.findOne({ _id }); if (!task) { @@ -123,7 +123,7 @@ const taskMutations = { /** * Watch task */ - async tasksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + async tasksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { const task = await Tasks.findOne({ _id }); if (!task) { diff --git a/src/data/resolvers/mutations/tickets.ts b/src/data/resolvers/mutations/tickets.ts index 73108e149..672822d3f 100644 --- a/src/data/resolvers/mutations/tickets.ts +++ b/src/data/resolvers/mutations/tickets.ts @@ -2,8 +2,8 @@ import { Tickets } from '../../../db/models'; import { IOrderInput } from '../../../db/models/definitions/boards'; import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { ITicket } from '../../../db/models/definitions/tickets'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { itemsChange, sendNotifications } from '../boardUtils'; import { checkUserIds } from './notifications'; @@ -15,7 +15,7 @@ const ticketMutations = { /** * Create new ticket */ - async ticketsAdd(_root, doc: ITicket, { user }: { user: IUserDocument }) { + async ticketsAdd(_root, doc: ITicket, { user }: IContext) { const ticket = await Tickets.createTicket({ ...doc, modifiedBy: user._id, @@ -36,7 +36,7 @@ const ticketMutations = { /** * Edit ticket */ - async ticketsEdit(_root, { _id, ...doc }: ITicketsEdit, { user }) { + async ticketsEdit(_root, { _id, ...doc }: ITicketsEdit, { user }: IContext) { const oldTicket = await Tickets.findOne({ _id }); if (!oldTicket) { @@ -69,7 +69,7 @@ const ticketMutations = { async ticketsChange( _root, { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const ticket = await Tickets.updateTicket(_id, { modifiedAt: new Date(), @@ -101,7 +101,7 @@ const ticketMutations = { /** * Remove ticket */ - async ticketsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async ticketsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const ticket = await Tickets.findOne({ _id }); if (!ticket) { @@ -123,7 +123,7 @@ const ticketMutations = { /** * Watch ticket */ - async ticketsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + async ticketsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { const ticket = await Tickets.findOne({ _id }); if (!ticket) { diff --git a/src/data/resolvers/mutations/users.ts b/src/data/resolvers/mutations/users.ts index 90d91c3fd..52cd85219 100644 --- a/src/data/resolvers/mutations/users.ts +++ b/src/data/resolvers/mutations/users.ts @@ -1,12 +1,12 @@ import { Channels, Users } from '../../../db/models'; -import { IDetail, IEmailSignature, ILink, IUser, IUserDocument } from '../../../db/models/definitions/users'; +import { IDetail, IEmailSignature, ILink, IUser } from '../../../db/models/definitions/users'; import { resetPermissionsCache } from '../../permissions/utils'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import utils, { authCookieOptions, getEnv } from '../../utils'; interface IUsersEdit extends IUser { channelIds?: string[]; - groupIds?: string[]; _id: string; } @@ -32,7 +32,7 @@ const userMutations = { /* * Login */ - async login(_root, args: { email: string; password: string; deviceToken?: string }, { res }) { + async login(_root, args: { email: string; password: string; deviceToken?: string }, { res }: IContext) { const response = await Users.login(args); const { token } = response; @@ -82,11 +82,7 @@ const userMutations = { /* * Change user password */ - usersChangePassword( - _root, - args: { currentPassword: string; newPassword: string }, - { user }: { user: IUserDocument }, - ) { + usersChangePassword(_root, args: { currentPassword: string; newPassword: string }, { user }: IContext) { return Users.changePassword({ _id: user._id, ...args }); }, @@ -94,7 +90,7 @@ const userMutations = { * Update user */ async usersEdit(_root, args: IUsersEdit) { - const { _id, username, email, channelIds = [], groupIds = [], details, links } = args; + const { _id, username, email, channelIds = [], groupIds = [], brandIds = [], details, links } = args; const updatedUser = await Users.updateUser(_id, { username, @@ -102,6 +98,7 @@ const userMutations = { details, links, groupIds, + brandIds, }); // add new user to channels @@ -130,7 +127,7 @@ const userMutations = { details: IDetail; links: ILink; }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const userOnDb = await Users.findOne({ _id: user._id }); @@ -151,7 +148,7 @@ const userMutations = { /* * Set Active or inactive user */ - async usersSetActiveStatus(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async usersSetActiveStatus(_root, { _id }: { _id: string }, { user }: IContext) { if (user._id === _id) { throw new Error('You can not delete yourself'); } @@ -186,7 +183,7 @@ const userMutations = { /* * User has seen onboard */ - async usersSeenOnBoard(_root, {}, { user }: { user: IUserDocument }) { + async usersSeenOnBoard(_root, {}, { user }: IContext) { return Users.updateOnBoardSeen({ _id: user._id }); }, @@ -209,15 +206,11 @@ const userMutations = { return Users.confirmInvitation({ token, password, passwordConfirmation, fullName, username }); }, - usersConfigEmailSignatures( - _root, - { signatures }: { signatures: IEmailSignature[] }, - { user }: { user: IUserDocument }, - ) { + usersConfigEmailSignatures(_root, { signatures }: { signatures: IEmailSignature[] }, { user }: IContext) { return Users.configEmailSignatures(user._id, signatures); }, - usersConfigGetNotificationByEmail(_root, { isAllowed }: { isAllowed: boolean }, { user }: { user: IUserDocument }) { + usersConfigGetNotificationByEmail(_root, { isAllowed }: { isAllowed: boolean }, { user }: IContext) { return Users.configGetNotificationByEmail(user._id, isAllowed); }, }; diff --git a/src/data/resolvers/pipeline.ts b/src/data/resolvers/pipeline.ts index d75521586..7cf396097 100644 --- a/src/data/resolvers/pipeline.ts +++ b/src/data/resolvers/pipeline.ts @@ -1,7 +1,7 @@ import { Users } from '../../db/models'; import { IPipelineDocument } from '../../db/models/definitions/boards'; import { PIPELINE_VISIBLITIES } from '../../db/models/definitions/constants'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; export default { members(pipeline: IPipelineDocument, {}) { @@ -12,7 +12,7 @@ export default { return []; }, - isWatched(pipeline: IPipelineDocument, _args, { user }: { user: IUserDocument }) { + isWatched(pipeline: IPipelineDocument, _args, { user }: IContext) { const watchedUserIds = pipeline.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { diff --git a/src/data/resolvers/queries/activityLogs.ts b/src/data/resolvers/queries/activityLogs.ts index 1b7e87a1d..76dde1c4d 100644 --- a/src/data/resolvers/queries/activityLogs.ts +++ b/src/data/resolvers/queries/activityLogs.ts @@ -1,28 +1,28 @@ -import { ActivityLogs } from '../../../db/models'; -import { IActivityLog } from '../../../db/models/definitions/activityLogs'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -const activityLogQueries = { - /** - * Get activity log list - */ - activityLogs(_root, doc: IActivityLog) { - const { contentType, contentId, activityType, limit } = doc; - - const query = { 'contentType.type': contentType, 'contentType.id': contentId }; - - if (activityType) { - query['activity.type'] = activityType; - } - - const sort = { createdAt: -1 }; - - return ActivityLogs.find(query) - .sort(sort) - .limit(limit); - }, -}; - -moduleRequireLogin(activityLogQueries); - -export default activityLogQueries; +import { ActivityLogs } from '../../../db/models'; +import { IActivityLog } from '../../../db/models/definitions/activityLogs'; +import { moduleRequireLogin } from '../../permissions/wrappers'; + +const activityLogQueries = { + /** + * Get activity log list + */ + activityLogs(_root, doc: IActivityLog) { + const { contentType, contentId, activityType, limit } = doc; + + const query = { 'contentType.type': contentType, 'contentType.id': contentId }; + + if (activityType) { + query['activity.type'] = activityType; + } + + const sort = { createdAt: -1 }; + + return ActivityLogs.find(query) + .sort(sort) + .limit(limit); + }, +}; + +moduleRequireLogin(activityLogQueries); + +export default activityLogQueries; diff --git a/src/data/resolvers/queries/boardUtils.ts b/src/data/resolvers/queries/boardUtils.ts index 4ae97ca4d..01a43a4c3 100644 --- a/src/data/resolvers/queries/boardUtils.ts +++ b/src/data/resolvers/queries/boardUtils.ts @@ -1,189 +1,189 @@ -import * as moment from 'moment'; -import { Stages } from '../../../db/models'; -import { getNextMonth, getToday } from '../../utils'; - -export const contains = (values: string[] = [], empty = false) => { - if (empty) { - return []; - } - - return { $in: values }; -}; - -export const generateCommonFilters = async (args: any) => { - const { - $and, - date, - pipelineId, - stageId, - search, - overdue, - nextMonth, - nextDay, - nextWeek, - noCloseDate, - assignedUserIds, - customerIds, - companyIds, - order, - probability, - initialStageId, - } = args; - - const assignedToNoOne = value => { - return value.length === 1 && value[0].length === 0; - }; - - const filter: any = {}; - - if (assignedUserIds) { - // Filter by assigned to no one - const notAssigned = assignedToNoOne(assignedUserIds); - - filter.assignedUserIds = notAssigned ? contains([], true) : contains(assignedUserIds); - } - - if ($and) { - filter.$and = $and; - } - - if (customerIds) { - filter.customerIds = contains(customerIds); - } - - if (companyIds) { - filter.companyIds = contains(companyIds); - } - - if (order) { - filter.order = order; - } - - if (probability) { - filter.probability = probability; - } - - if (initialStageId) { - filter.initialStageId = initialStageId; - } - - if (nextDay) { - const tommorrow = moment().add(1, 'days'); - - filter.closeDate = { - $gte: new Date(tommorrow.startOf('day').format('YYYY-MM-DD')), - $lte: new Date(tommorrow.endOf('day').format('YYYY-MM-DD')), - }; - } - - if (nextWeek) { - const monday = moment() - .day(1 + 7) - .format('YYYY-MM-DD'); - const nextSunday = moment() - .day(7 + 7) - .format('YYYY-MM-DD'); - - filter.closeDate = { - $gte: new Date(monday), - $lte: new Date(nextSunday), - }; - } - - if (nextMonth) { - const now = new Date(); - const { start, end } = getNextMonth(now); - - filter.closeDate = { - $gte: new Date(start), - $lte: new Date(end), - }; - } - - if (noCloseDate) { - filter.closeDate = { $exists: false }; - } - - if (overdue) { - const now = new Date(); - const today = getToday(now); - - filter.closeDate = { $lt: today }; - } - - if (search) { - filter.$or = [ - { name: new RegExp(`.*${search || ''}.*`, 'i') }, - { description: new RegExp(`.*${search || ''}.*`, 'i') }, - ]; - } - - if (stageId) { - filter.stageId = stageId; - } - - // Calendar monthly date - if (date) { - const stageIds = await Stages.find({ pipelineId }).distinct('_id'); - - filter.closeDate = dateSelector(date); - filter.stageId = { $in: stageIds }; - } - - return filter; -}; - -export const generateDealCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { productIds } = extraParams || args; - - if (productIds) { - filter['productsData.productId'] = contains(productIds); - } - - return filter; -}; - -export const generateTicketCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { priority, source } = extraParams || args; - - if (priority) { - filter.priority = contains(priority); - } - - if (source) { - filter.source = contains(source); - } - - return filter; -}; - -export const generateTaskCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { priority } = extraParams || args; - - if (priority) { - filter.priority = contains(priority); - } - - return filter; -}; - -interface IDate { - month: number; - year: number; -} - -export const dateSelector = (date: IDate) => { - const { year, month } = date; - const currentDate = new Date(); - - const start = currentDate.setFullYear(year, month, 1); - const end = currentDate.setFullYear(year, month + 1, 0); - - return { - $gte: new Date(start), - $lte: new Date(end), - }; -}; +import * as moment from 'moment'; +import { Stages } from '../../../db/models'; +import { getNextMonth, getToday } from '../../utils'; + +export const contains = (values: string[] = [], empty = false) => { + if (empty) { + return []; + } + + return { $in: values }; +}; + +export const generateCommonFilters = async (args: any) => { + const { + $and, + date, + pipelineId, + stageId, + search, + overdue, + nextMonth, + nextDay, + nextWeek, + noCloseDate, + assignedUserIds, + customerIds, + companyIds, + order, + probability, + initialStageId, + } = args; + + const assignedToNoOne = value => { + return value.length === 1 && value[0].length === 0; + }; + + const filter: any = {}; + + if (assignedUserIds) { + // Filter by assigned to no one + const notAssigned = assignedToNoOne(assignedUserIds); + + filter.assignedUserIds = notAssigned ? contains([], true) : contains(assignedUserIds); + } + + if ($and) { + filter.$and = $and; + } + + if (customerIds) { + filter.customerIds = contains(customerIds); + } + + if (companyIds) { + filter.companyIds = contains(companyIds); + } + + if (order) { + filter.order = order; + } + + if (probability) { + filter.probability = probability; + } + + if (initialStageId) { + filter.initialStageId = initialStageId; + } + + if (nextDay) { + const tommorrow = moment().add(1, 'days'); + + filter.closeDate = { + $gte: new Date(tommorrow.startOf('day').format('YYYY-MM-DD')), + $lte: new Date(tommorrow.endOf('day').format('YYYY-MM-DD')), + }; + } + + if (nextWeek) { + const monday = moment() + .day(1 + 7) + .format('YYYY-MM-DD'); + const nextSunday = moment() + .day(7 + 7) + .format('YYYY-MM-DD'); + + filter.closeDate = { + $gte: new Date(monday), + $lte: new Date(nextSunday), + }; + } + + if (nextMonth) { + const now = new Date(); + const { start, end } = getNextMonth(now); + + filter.closeDate = { + $gte: new Date(start), + $lte: new Date(end), + }; + } + + if (noCloseDate) { + filter.closeDate = { $exists: false }; + } + + if (overdue) { + const now = new Date(); + const today = getToday(now); + + filter.closeDate = { $lt: today }; + } + + if (search) { + filter.$or = [ + { name: new RegExp(`.*${search || ''}.*`, 'i') }, + { description: new RegExp(`.*${search || ''}.*`, 'i') }, + ]; + } + + if (stageId) { + filter.stageId = stageId; + } + + // Calendar monthly date + if (date) { + const stageIds = await Stages.find({ pipelineId }).distinct('_id'); + + filter.closeDate = dateSelector(date); + filter.stageId = { $in: stageIds }; + } + + return filter; +}; + +export const generateDealCommonFilters = async (args: any, extraParams?: any) => { + const filter = await generateCommonFilters(args); + const { productIds } = extraParams || args; + + if (productIds) { + filter['productsData.productId'] = contains(productIds); + } + + return filter; +}; + +export const generateTicketCommonFilters = async (args: any, extraParams?: any) => { + const filter = await generateCommonFilters(args); + const { priority, source } = extraParams || args; + + if (priority) { + filter.priority = contains(priority); + } + + if (source) { + filter.source = contains(source); + } + + return filter; +}; + +export const generateTaskCommonFilters = async (args: any, extraParams?: any) => { + const filter = await generateCommonFilters(args); + const { priority } = extraParams || args; + + if (priority) { + filter.priority = contains(priority); + } + + return filter; +}; + +interface IDate { + month: number; + year: number; +} + +export const dateSelector = (date: IDate) => { + const { year, month } = date; + const currentDate = new Date(); + + const start = currentDate.setFullYear(year, month, 1); + const end = currentDate.setFullYear(year, month + 1, 0); + + return { + $gte: new Date(start), + $lte: new Date(end), + }; +}; diff --git a/src/data/resolvers/queries/boards.ts b/src/data/resolvers/queries/boards.ts index 4fc3fe75c..0bdedf3f7 100644 --- a/src/data/resolvers/queries/boards.ts +++ b/src/data/resolvers/queries/boards.ts @@ -1,81 +1,82 @@ -import { Boards, Pipelines, Stages } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -export interface IDate { - month: number; - year: number; -} - -export interface IListParams { - pipelineId?: string; - stageId: string; - skip?: number; - date?: IDate; - search?: string; - customerIds?: [string]; - companyIds?: [string]; - assignedUserIds?: [string]; -} - -const boardQueries = { - /** - * Boards list - */ - boards(_root, { type }: { type: string }) { - return Boards.find({ type }).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Board detail - */ - boardDetail(_root, { _id }: { _id: string }) { - return Boards.findOne({ _id }); - }, - - /** - * Get last board - */ - boardGetLast(_root, { type }: { type: string }) { - return Boards.findOne({ type }).sort({ createdAt: -1 }); - }, - - /** - * Pipelines list - */ - pipelines(_root, { boardId }: { boardId: string; type: string }) { - return Pipelines.find({ boardId }).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Pipeline detail - */ - pipelineDetail(_root, { _id }: { _id: string }) { - return Pipelines.findOne({ _id }); - }, - - /** - * Stages list - */ - stages(_root, { pipelineId, isNotLost }: { pipelineId: string; isNotLost: boolean }) { - const filter: any = {}; - - filter.pipelineId = pipelineId; - - if (isNotLost) { - filter.probability = { $ne: 'Lost' }; - } - - return Stages.find(filter).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Stage detail - */ - stageDetail(_root, { _id }: { _id: string }) { - return Stages.findOne({ _id }); - }, -}; - -moduleRequireLogin(boardQueries); - -export default boardQueries; +import { Boards, Pipelines, Stages } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +export interface IDate { + month: number; + year: number; +} + +export interface IListParams { + pipelineId?: string; + stageId: string; + skip?: number; + date?: IDate; + search?: string; + customerIds?: [string]; + companyIds?: [string]; + assignedUserIds?: [string]; +} + +const boardQueries = { + /** + * Boards list + */ + boards(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Boards.find({ ...commonQuerySelector, type }).sort({ order: 1, createdAt: -1 }); + }, + + /** + * Board detail + */ + boardDetail(_root, { _id }: { _id: string }, { commonQuerySelector }: IContext) { + return Boards.findOne({ ...commonQuerySelector, _id }); + }, + + /** + * Get last board + */ + boardGetLast(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Boards.findOne({ ...commonQuerySelector, type }).sort({ createdAt: -1 }); + }, + + /** + * Pipelines list + */ + pipelines(_root, { boardId }: { boardId: string; type: string }) { + return Pipelines.find({ boardId }).sort({ order: 1, createdAt: -1 }); + }, + + /** + * Pipeline detail + */ + pipelineDetail(_root, { _id }: { _id: string }) { + return Pipelines.findOne({ _id }); + }, + + /** + * Stages list + */ + stages(_root, { pipelineId, isNotLost }: { pipelineId: string; isNotLost: boolean }) { + const filter: any = {}; + + filter.pipelineId = pipelineId; + + if (isNotLost) { + filter.probability = { $ne: 'Lost' }; + } + + return Stages.find(filter).sort({ order: 1, createdAt: -1 }); + }, + + /** + * Stage detail + */ + stageDetail(_root, { _id }: { _id: string }) { + return Stages.findOne({ _id }); + }, +}; + +moduleRequireLogin(boardQueries); + +export default boardQueries; diff --git a/src/data/resolvers/queries/brands.ts b/src/data/resolvers/queries/brands.ts index 2cb9b70c1..d48a798df 100644 --- a/src/data/resolvers/queries/brands.ts +++ b/src/data/resolvers/queries/brands.ts @@ -1,42 +1,43 @@ -import { Brands } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const brandQueries = { - /** - * Brands list - */ - brands(_root, args: { page: number; perPage: number }) { - const brands = paginate(Brands.find({}), args); - return brands.sort({ createdAt: -1 }); - }, - - /** - * Get one brand - */ - brandDetail(_root, { _id }: { _id: string }) { - return Brands.findOne({ _id }); - }, - - /** - * Get all brands count. We will use it in pager - */ - brandsTotalCount() { - return Brands.find({}).countDocuments(); - }, - - /** - * Get last brand - */ - brandsGetLast() { - return Brands.findOne({}).sort({ createdAt: -1 }); - }, -}; - -requireLogin(brandQueries, 'brandsTotalCount'); -requireLogin(brandQueries, 'brandsGetLast'); -requireLogin(brandQueries, 'brandDetail'); - -checkPermission(brandQueries, 'brands', 'showBrands', []); - -export default brandQueries; +import { Brands } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +const brandQueries = { + /** + * Brands list + */ + brands(_root, args: { page: number; perPage: number }, { brandIdSelector }: IContext) { + const brands = paginate(Brands.find(brandIdSelector), args); + return brands.sort({ createdAt: -1 }); + }, + + /** + * Get one brand + */ + brandDetail(_root, { _id }: { _id: string }) { + return Brands.findOne({ _id }); + }, + + /** + * Get all brands count. We will use it in pager + */ + brandsTotalCount(_root, _args, { brandIdSelector }: IContext) { + return Brands.find(brandIdSelector).countDocuments(); + }, + + /** + * Get last brand + */ + brandsGetLast() { + return Brands.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(brandQueries, 'brandsTotalCount'); +requireLogin(brandQueries, 'brandsGetLast'); +requireLogin(brandQueries, 'brandDetail'); + +checkPermission(brandQueries, 'brands', 'showBrands', []); + +export default brandQueries; diff --git a/src/data/resolvers/queries/channels.ts b/src/data/resolvers/queries/channels.ts index 52011f800..393a48301 100644 --- a/src/data/resolvers/queries/channels.ts +++ b/src/data/resolvers/queries/channels.ts @@ -1,58 +1,58 @@ -import { Channels } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface IIn { - $in: string[]; -} - -interface IChannelQuery { - memberIds?: IIn; -} - -const channelQueries = { - /** - * Channels list - */ - channels(_root, { memberIds, ...queryParams }: { page: number; perPage: number; memberIds: string[] }) { - const query: IChannelQuery = {}; - const sort = { createdAt: -1 }; - - if (memberIds) { - query.memberIds = { $in: memberIds }; - } - - const channels = paginate(Channels.find(query), queryParams); - - return channels.sort(sort); - }, - - /** - * Get one channel - */ - channelDetail(_root, { _id }: { _id: string }) { - return Channels.findOne({ _id }); - }, - - /** - * Get all channels count. We will use it in pager - */ - channelsTotalCount() { - return Channels.find({}).countDocuments(); - }, - - /** - * Get last channel - */ - channelsGetLast() { - return Channels.findOne({}).sort({ createdAt: -1 }); - }, -}; - -requireLogin(channelQueries, 'channelsGetLast'); -requireLogin(channelQueries, 'channelsTotalCount'); -requireLogin(channelQueries, 'channelDetail'); - -checkPermission(channelQueries, 'channels', 'showChannels', []); - -export default channelQueries; +import { Channels } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { paginate } from '../../utils'; + +interface IIn { + $in: string[]; +} + +interface IChannelQuery { + memberIds?: IIn; +} + +const channelQueries = { + /** + * Channels list + */ + channels(_root, { memberIds, ...queryParams }: { page: number; perPage: number; memberIds: string[] }) { + const query: IChannelQuery = {}; + const sort = { createdAt: -1 }; + + if (memberIds) { + query.memberIds = { $in: memberIds }; + } + + const channels = paginate(Channels.find(query), queryParams); + + return channels.sort(sort); + }, + + /** + * Get one channel + */ + channelDetail(_root, { _id }: { _id: string }) { + return Channels.findOne({ _id }); + }, + + /** + * Get all channels count. We will use it in pager + */ + channelsTotalCount() { + return Channels.find({}).countDocuments(); + }, + + /** + * Get last channel + */ + channelsGetLast() { + return Channels.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(channelQueries, 'channelsGetLast'); +requireLogin(channelQueries, 'channelsTotalCount'); +requireLogin(channelQueries, 'channelDetail'); + +checkPermission(channelQueries, 'channels', 'showChannels', []); + +export default channelQueries; diff --git a/src/data/resolvers/queries/companies.ts b/src/data/resolvers/queries/companies.ts index c2c919c60..86f4e3324 100644 --- a/src/data/resolvers/queries/companies.ts +++ b/src/data/resolvers/queries/companies.ts @@ -1,157 +1,170 @@ -import { Brands, Companies, Segments, Tags } from '../../../db/models'; -import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; -import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES } from '../../constants'; -import { brandFilter, filter, IListArgs, sortBuilder } from '../../modules/coc/companies'; -import QueryBuilder from '../../modules/segments/queryBuilder'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface ICountArgs extends IListArgs { - only?: string; - byFakeSegment: any; -} - -interface ICountBy { - [index: string]: number; -} - -const count = async (query: any, args: ICountArgs) => { - const selector = await filter(args); - - const findQuery = { ...selector, ...query }; - - return Companies.find(findQuery).countDocuments(); -}; - -const countBySegment = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by segments ========= - const segments = await Segments.find({ - contentType: ACTIVITY_CONTENT_TYPES.COMPANY, - }); - - for (const s of segments) { - counts[s._id] = await count(await QueryBuilder.segments(s), args); - } - - return counts; -}; - -const countByTags = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by tag ========= - const tags = await Tags.find({ type: TAG_TYPES.COMPANY }); - - for (const tag of tags) { - counts[tag._id] = await count({ tagIds: tag._id }, args); - } - - return counts; -}; - -const countByBrands = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by brand ========= - const brands = await Brands.find({}); - - for (const brand of brands) { - counts[brand._id] = await count(await brandFilter(brand._id), args); - } - - return counts; -}; - -const companyQueries = { - /** - * Companies list - */ - async companies(_root, params: IListArgs) { - const selector = await filter(params); - const sort = sortBuilder(params); - - return paginate(Companies.find(selector), params).sort(sort); - }, - - /** - * Companies for only main list - */ - async companiesMain(_root, params: IListArgs) { - const selector = await filter(params); - const sort = sortBuilder(params); - - const list = await paginate(Companies.find(selector).sort(sort), params); - const totalCount = await Companies.find(selector).countDocuments(); - - return { list, totalCount }; - }, - - /** - * Group company counts by segments - */ - async companyCounts(_root, args: ICountArgs) { - const counts = { - bySegment: {}, - byFakeSegment: 0, - byTag: {}, - byBrand: {}, - byLeadStatus: {}, - byLifecycleState: {}, - }; - - const { only } = args; - - switch (only) { - case 'byTag': - counts.byTag = await countByTags(args); - break; - case 'bySegment': - counts.bySegment = await countBySegment(args); - break; - case 'byBrand': - counts.byBrand = await countByBrands(args); - break; - case 'byLeadStatus': - { - // Count companies by lead status ====== - for (const status of COC_LEAD_STATUS_TYPES) { - counts.byLeadStatus[status] = await count({ leadStatus: status }, args); - } - } - break; - case 'byLifecycleState': - { - // Count companies by life cycle state ======= - for (const state of COC_LIFECYCLE_STATE_TYPES) { - counts.byLifecycleState[state] = await count({ lifecycleState: state }, args); - } - } - break; - } - - // Count companies by fake segment - if (args.byFakeSegment) { - counts.byFakeSegment = await count(await QueryBuilder.segments(args.byFakeSegment), args); - } - - return counts; - }, - - /** - * Get one company - */ - companyDetail(_root, { _id }: { _id: string }) { - return Companies.findOne({ _id }); - }, -}; - -requireLogin(companyQueries, 'companiesMain'); -requireLogin(companyQueries, 'companyCounts'); -requireLogin(companyQueries, 'companyDetail'); - -checkPermission(companyQueries, 'companies', 'showCompanies', []); -checkPermission(companyQueries, 'companiesMain', 'showCompanies', { list: [], totalCount: 0 }); - -export default companyQueries; +import { Brands, Companies, Segments, Tags } from '../../../db/models'; +import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; +import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES } from '../../constants'; +import { brandFilter, filter, IListArgs, sortBuilder } from '../../modules/coc/companies'; +import QueryBuilder from '../../modules/segments/queryBuilder'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +interface ICountArgs extends IListArgs { + only?: string; + byFakeSegment: any; +} + +interface ICountBy { + [index: string]: number; +} + +const count = async (query: any, args: ICountArgs) => { + const selector = await filter(args); + + const findQuery = { ...selector, ...query }; + + return Companies.find(findQuery).countDocuments(); +}; + +const countBySegment = async (commonSelector, args: ICountArgs): Promise => { + const counts = {}; + + // Count companies by segments ========= + const segments = await Segments.find({ + contentType: ACTIVITY_CONTENT_TYPES.COMPANY, + }); + + for (const s of segments) { + try { + counts[s._id] = await count({ ...commonSelector, ...(await QueryBuilder.segments(s)) }, args); + } catch (e) { + // catch mongo error + if (e.name === 'CastError') { + counts[s._id] = 0; + } else { + throw new Error(e); + } + } + } + + return counts; +}; + +const countByTags = async (commonSelector, args: ICountArgs): Promise => { + const counts = {}; + + // Count companies by tag ========= + const tags = await Tags.find({ type: TAG_TYPES.COMPANY }); + + for (const tag of tags) { + counts[tag._id] = await count({ ...commonSelector, tagIds: tag._id }, args); + } + + return counts; +}; + +const countByBrands = async (commonSelector, args: ICountArgs): Promise => { + const counts = {}; + + // Count companies by brand ========= + const brands = await Brands.find({}); + + for (const brand of brands) { + counts[brand._id] = await count({ ...commonSelector, ...(await brandFilter(brand._id)) }, args); + } + + return counts; +}; + +const companyQueries = { + /** + * Companies list + */ + async companies(_root, params: IListArgs, { commonQuerySelector }: IContext) { + const selector = { ...commonQuerySelector, ...(await filter(params)) }; + const sort = sortBuilder(params); + + return paginate(Companies.find(selector), params).sort(sort); + }, + + /** + * Companies for only main list + */ + async companiesMain(_root, params: IListArgs, { commonQuerySelector }: IContext) { + const selector = { ...commonQuerySelector, ...(await filter(params)) }; + const sort = sortBuilder(params); + + const list = await paginate(Companies.find(selector).sort(sort), params); + const totalCount = await Companies.find(selector).countDocuments(); + + return { list, totalCount }; + }, + + /** + * Group company counts by segments + */ + async companyCounts(_root, args: ICountArgs, { commonQuerySelector }: IContext) { + const counts = { + bySegment: {}, + byFakeSegment: 0, + byTag: {}, + byBrand: {}, + byLeadStatus: {}, + byLifecycleState: {}, + }; + + const { only } = args; + + switch (only) { + case 'byTag': + counts.byTag = await countByTags(commonQuerySelector, args); + break; + case 'bySegment': + counts.bySegment = await countBySegment(commonQuerySelector, args); + break; + case 'byBrand': + counts.byBrand = await countByBrands(commonQuerySelector, args); + break; + case 'byLeadStatus': + { + // Count companies by lead status ====== + for (const status of COC_LEAD_STATUS_TYPES) { + counts.byLeadStatus[status] = await count({ ...commonQuerySelector, leadStatus: status }, args); + } + } + break; + case 'byLifecycleState': + { + // Count companies by life cycle state ======= + for (const state of COC_LIFECYCLE_STATE_TYPES) { + counts.byLifecycleState[state] = await count({ ...commonQuerySelector, lifecycleState: state }, args); + } + } + break; + } + + // Count companies by fake segment + if (args.byFakeSegment) { + counts.byFakeSegment = await count( + { ...commonQuerySelector, ...(await QueryBuilder.segments(args.byFakeSegment)) }, + args, + ); + } + + return counts; + }, + + /** + * Get one company + */ + companyDetail(_root, { _id }: { _id: string }) { + return Companies.findOne({ _id }); + }, +}; + +requireLogin(companyQueries, 'companiesMain'); +requireLogin(companyQueries, 'companyCounts'); +requireLogin(companyQueries, 'companyDetail'); + +checkPermission(companyQueries, 'companies', 'showCompanies', []); +checkPermission(companyQueries, 'companiesMain', 'showCompanies', { list: [], totalCount: 0 }); + +export default companyQueries; diff --git a/src/data/resolvers/queries/configs.ts b/src/data/resolvers/queries/configs.ts index 03ed9b352..fdaa9290f 100644 --- a/src/data/resolvers/queries/configs.ts +++ b/src/data/resolvers/queries/configs.ts @@ -1,66 +1,72 @@ -import { Configs } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; -import { getEnv, sendRequest } from '../../utils'; - -const configQueries = { - /** - * Config object - */ - configsDetail(_root, { code }: { code: string }) { - return Configs.findOne({ code }); - }, - - async configsVersions(_root) { - const erxesDomain = getEnv({ name: 'MAIN_APP_DOMAIN' }); - const domain = getEnv({ name: 'DOMAIN' }); - const widgetsApiDomain = getEnv({ name: 'WIDGETS_API_DOMAIN' }); - const widgetsDomain = getEnv({ name: 'WIDGETS_DOMAIN' }); - - let erxesVersion; - let apiVersion; - let widgetApiVersion; - let widgetVersion; - - try { - erxesVersion = await sendRequest({ url: `${erxesDomain}/version.json`, method: 'GET' }); - } catch (e) { - erxesVersion = {}; - } - - try { - apiVersion = await sendRequest({ url: `${domain}/static/version.json`, method: 'GET' }); - } catch (e) { - apiVersion = {}; - } - - try { - widgetApiVersion = await sendRequest({ url: `${widgetsApiDomain}/static/version.json`, method: 'GET' }); - } catch (e) { - widgetApiVersion = {}; - } - - try { - widgetVersion = await sendRequest({ url: `${widgetsDomain}/build/version.json`, method: 'GET' }); - } catch (e) { - widgetVersion = {}; - } - - return { - erxesVersion, - apiVersion, - widgetApiVersion, - widgetVersion, - }; - }, - - /** - * Config for engage - */ - engagesConfigDetail(_root, {}, { dataSources: { EngagesAPI } }) { - return EngagesAPI.engagesConfigDetail(); - }, -}; - -moduleRequireLogin(configQueries); - -export default configQueries; +import { Configs } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { getEnv, sendRequest } from '../../utils'; + +const configQueries = { + /** + * Config object + */ + configsDetail(_root, { code }: { code: string }) { + return Configs.findOne({ code }); + }, + + async configsVersions(_root) { + const erxesDomain = getEnv({ name: 'MAIN_APP_DOMAIN' }); + const domain = getEnv({ name: 'DOMAIN' }); + const widgetsApiDomain = getEnv({ name: 'WIDGETS_API_DOMAIN' }); + const widgetsDomain = getEnv({ name: 'WIDGETS_DOMAIN' }); + + let erxesVersion; + let apiVersion; + let widgetApiVersion; + let widgetVersion; + + try { + erxesVersion = await sendRequest({ url: `${erxesDomain}/version.json`, method: 'GET' }); + } catch (e) { + erxesVersion = {}; + } + + try { + apiVersion = await sendRequest({ url: `${domain}/static/version.json`, method: 'GET' }); + } catch (e) { + apiVersion = {}; + } + + try { + widgetApiVersion = await sendRequest({ url: `${widgetsApiDomain}/static/version.json`, method: 'GET' }); + } catch (e) { + widgetApiVersion = {}; + } + + try { + widgetVersion = await sendRequest({ url: `${widgetsDomain}/build/version.json`, method: 'GET' }); + } catch (e) { + widgetVersion = {}; + } + + return { + erxesVersion, + apiVersion, + widgetApiVersion, + widgetVersion, + }; + }, + + configsGetEnv(_root) { + return { + USE_BRAND_RESTRICTIONS: process.env.USE_BRAND_RESTRICTIONS, + }; + }, + + /** + * Config for engage + */ + engagesConfigDetail(_root, {}, { dataSources: { EngagesAPI } }) { + return EngagesAPI.engagesConfigDetail(); + }, +}; + +moduleRequireLogin(configQueries); + +export default configQueries; diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index 7dd8642eb..fee8fe5bf 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -1,299 +1,299 @@ -import * as _ from 'underscore'; -import { Channels, Integrations } from '../../../db/models'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { fixDate } from '../../utils'; - -interface IIn { - $in: string[]; -} - -interface IExists { - $exists: boolean; -} - -export interface IListArgs { - limit?: number; - channelId?: string; - status?: string; - unassigned?: string; - brandId?: string; - tag?: string; - integrationType?: string; - participating?: string; - starred?: string; - ids?: string[]; - startDate?: string; - endDate?: string; - only?: string; -} - -interface IUserArgs { - _id: string; - starredConversationIds?: string[]; -} - -interface IIntersectIntegrationIds { - integrationId: IIn; -} - -interface IUnassignedFilter { - assignedUserId: IExists; -} - -interface IDateFilter { - createdAt: { - $gte: Date; - $lte: Date; - }; -} - -export default class Builder { - public params: IListArgs; - public user: IUserArgs; - public queries: any; - public unassignedQuery?: IUnassignedFilter; - - constructor(params: IListArgs, user: IUserArgs) { - this.params = params; - this.user = user; - } - - public defaultFilters(): { [index: string]: {} } { - let statusFilter = this.statusFilter([CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN]); - - if (this.params.status === 'closed') { - statusFilter = this.statusFilter([CONVERSATION_STATUSES.CLOSED]); - } - - return { - ...statusFilter, - // exclude engage messages if customer did not reply - $or: [ - { - userId: { $exists: true }, - messageCount: { $gt: 1 }, - }, - { - userId: { $exists: false }, - }, - ], - }; - } - - public intersectIntegrationIds(...queries: any[]): { integrationId: IIn } { - // filter only queries with $in field - const withIn = queries.filter(q => q.integrationId && q.integrationId.$in && q.integrationId.$in.length > 0); - - // [{$in: ['id1', 'id2']}, {$in: ['id3', 'id1', 'id4']}] - const $ins = _.pluck(withIn, 'integrationId'); - - // [['id1', 'id2'], ['id3', 'id1', 'id4']] - const nestedIntegrationIds = _.pluck($ins, '$in'); - - // ['id1'] - const integrationids: any = _.intersection(...nestedIntegrationIds); - - return { - integrationId: { $in: integrationids }, - }; - } - - /* - * find integrationIds from channel && brand - */ - public async integrationsFilter(): Promise { - const channelFilter = { - memberIds: this.user._id, - }; - - // find all posssible integrations - let availIntegrationIds: any = []; - - const channels = await Channels.find(channelFilter); - - channels.forEach(channel => { - availIntegrationIds = _.union(availIntegrationIds, channel.integrationIds || ''); - }); - - const nestedIntegrationIds: any = [{ integrationId: { $in: availIntegrationIds } }]; - - // filter by channel - if (this.params.channelId) { - const channelQuery = await this.channelFilter(this.params.channelId); - nestedIntegrationIds.push(channelQuery); - } - - // filter by brand - if (this.params.brandId) { - const brandQuery = await this.brandFilter(this.params.brandId); - nestedIntegrationIds.push(brandQuery); - } - - return this.intersectIntegrationIds(...nestedIntegrationIds); - } - - // filter by channel - public async channelFilter(channelId: string): Promise<{ integrationId: IIn }> { - const channel = await Channels.findOne({ _id: channelId }); - if (channel && channel.integrationIds) { - return { - integrationId: { $in: channel.integrationIds }, - }; - } else { - return { integrationId: { $in: [] } }; - } - } - - // filter by brand - public async brandFilter(brandId: string): Promise<{ integrationId: IIn }> { - const integrations = await Integrations.find({ brandId }); - const integrationIds = _.pluck(integrations, '_id'); - - return { - integrationId: { $in: integrationIds }, - }; - } - - // filter all unassigned - public unassignedFilter(): IUnassignedFilter { - this.unassignedQuery = { - assignedUserId: { $exists: false }, - }; - - return this.unassignedQuery; - } - - // filter by participating - public participatingFilter(): { participatedUserIds: IIn } { - return { - participatedUserIds: { $in: [this.user._id] }, - }; - } - - // filter by starred - public starredFilter(): { _id: IIn | { $in: any[] } } { - let ids: any = []; - - if (this.user) { - ids = this.user.starredConversationIds || []; - } - - return { - _id: { $in: ids }, - }; - } - - public statusFilter(statusChoices: string[]): { status: IIn } { - return { - status: { $in: statusChoices }, - }; - } - - // filter by integration type - public async integrationTypeFilter(integrationType: string): Promise<{ $and: IIntersectIntegrationIds[] }> { - const integrations = await Integrations.find({ kind: integrationType }); - - return { - $and: [ - // add channel && brand filter - this.queries.integrations, - - // filter by integration type - { integrationId: { $in: _.pluck(integrations, '_id') } }, - ], - }; - } - - // filter by tag - public tagFilter(tagId: string): { tagIds: string[] } { - return { - tagIds: [tagId], - }; - } - - public dateFilter(startDate: string, endDate: string): IDateFilter { - return { - createdAt: { - $gte: fixDate(startDate), - $lte: fixDate(endDate), - }, - }; - } - - /* - * prepare all queries. do not do any action - */ - public async buildAllQueries(): Promise { - this.queries = { - default: this.defaultFilters(), - starred: {}, - status: {}, - unassigned: {}, - tag: {}, - channel: {}, - integrationType: {}, - - // find it using channel && brand - integrations: {}, - - participating: {}, - createdAt: {}, - }; - - // filter by channel - if (this.params.channelId) { - this.queries.channel = await this.channelFilter(this.params.channelId); - } - - // filter by channelId & brandId - this.queries.integrations = await this.integrationsFilter(); - - // unassigned - if (this.params.unassigned) { - this.queries.unassigned = this.unassignedFilter(); - } - - // participating - if (this.params.participating) { - this.queries.participating = this.participatingFilter(); - } - - // starred - if (this.params.starred) { - this.queries.starred = this.starredFilter(); - } - - // filter by status - if (this.params.status) { - this.queries.status = this.statusFilter([this.params.status]); - } - - // filter by tag - if (this.params.tag) { - this.queries.tag = this.tagFilter(this.params.tag); - } - - // filter by integration type - if (this.params.integrationType) { - this.queries.integrationType = await this.integrationTypeFilter(this.params.integrationType); - } - - if (this.params.startDate && this.params.endDate) { - this.queries.createdAt = this.dateFilter(this.params.startDate, this.params.endDate); - } - } - - public mainQuery(): any { - return { - ...this.queries.default, - ...this.queries.integrations, - ...this.queries.integrationType, - ...this.queries.unassigned, - ...this.queries.participating, - ...this.queries.status, - ...this.queries.starred, - ...this.queries.tag, - ...this.queries.createdAt, - }; - } -} +import * as _ from 'underscore'; +import { Channels, Integrations } from '../../../db/models'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { fixDate } from '../../utils'; + +interface IIn { + $in: string[]; +} + +interface IExists { + $exists: boolean; +} + +export interface IListArgs { + limit?: number; + channelId?: string; + status?: string; + unassigned?: string; + brandId?: string; + tag?: string; + integrationType?: string; + participating?: string; + starred?: string; + ids?: string[]; + startDate?: string; + endDate?: string; + only?: string; +} + +interface IUserArgs { + _id: string; + starredConversationIds?: string[]; +} + +interface IIntersectIntegrationIds { + integrationId: IIn; +} + +interface IUnassignedFilter { + assignedUserId: IExists; +} + +interface IDateFilter { + createdAt: { + $gte: Date; + $lte: Date; + }; +} + +export default class Builder { + public params: IListArgs; + public user: IUserArgs; + public queries: any; + public unassignedQuery?: IUnassignedFilter; + + constructor(params: IListArgs, user: IUserArgs) { + this.params = params; + this.user = user; + } + + public defaultFilters(): { [index: string]: {} } { + let statusFilter = this.statusFilter([CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN]); + + if (this.params.status === 'closed') { + statusFilter = this.statusFilter([CONVERSATION_STATUSES.CLOSED]); + } + + return { + ...statusFilter, + // exclude engage messages if customer did not reply + $or: [ + { + userId: { $exists: true }, + messageCount: { $gt: 1 }, + }, + { + userId: { $exists: false }, + }, + ], + }; + } + + public intersectIntegrationIds(...queries: any[]): { integrationId: IIn } { + // filter only queries with $in field + const withIn = queries.filter(q => q.integrationId && q.integrationId.$in && q.integrationId.$in.length > 0); + + // [{$in: ['id1', 'id2']}, {$in: ['id3', 'id1', 'id4']}] + const $ins = _.pluck(withIn, 'integrationId'); + + // [['id1', 'id2'], ['id3', 'id1', 'id4']] + const nestedIntegrationIds = _.pluck($ins, '$in'); + + // ['id1'] + const integrationids: any = _.intersection(...nestedIntegrationIds); + + return { + integrationId: { $in: integrationids }, + }; + } + + /* + * find integrationIds from channel && brand + */ + public async integrationsFilter(): Promise { + const channelFilter = { + memberIds: this.user._id, + }; + + // find all posssible integrations + let availIntegrationIds: any = []; + + const channels = await Channels.find(channelFilter); + + channels.forEach(channel => { + availIntegrationIds = _.union(availIntegrationIds, channel.integrationIds || ''); + }); + + const nestedIntegrationIds: any = [{ integrationId: { $in: availIntegrationIds } }]; + + // filter by channel + if (this.params.channelId) { + const channelQuery = await this.channelFilter(this.params.channelId); + nestedIntegrationIds.push(channelQuery); + } + + // filter by brand + if (this.params.brandId) { + const brandQuery = await this.brandFilter(this.params.brandId); + nestedIntegrationIds.push(brandQuery); + } + + return this.intersectIntegrationIds(...nestedIntegrationIds); + } + + // filter by channel + public async channelFilter(channelId: string): Promise<{ integrationId: IIn }> { + const channel = await Channels.findOne({ _id: channelId }); + if (channel && channel.integrationIds) { + return { + integrationId: { $in: channel.integrationIds }, + }; + } else { + return { integrationId: { $in: [] } }; + } + } + + // filter by brand + public async brandFilter(brandId: string): Promise<{ integrationId: IIn }> { + const integrations = await Integrations.find({ brandId }); + const integrationIds = _.pluck(integrations, '_id'); + + return { + integrationId: { $in: integrationIds }, + }; + } + + // filter all unassigned + public unassignedFilter(): IUnassignedFilter { + this.unassignedQuery = { + assignedUserId: { $exists: false }, + }; + + return this.unassignedQuery; + } + + // filter by participating + public participatingFilter(): { participatedUserIds: IIn } { + return { + participatedUserIds: { $in: [this.user._id] }, + }; + } + + // filter by starred + public starredFilter(): { _id: IIn | { $in: any[] } } { + let ids: any = []; + + if (this.user) { + ids = this.user.starredConversationIds || []; + } + + return { + _id: { $in: ids }, + }; + } + + public statusFilter(statusChoices: string[]): { status: IIn } { + return { + status: { $in: statusChoices }, + }; + } + + // filter by integration type + public async integrationTypeFilter(integrationType: string): Promise<{ $and: IIntersectIntegrationIds[] }> { + const integrations = await Integrations.find({ kind: integrationType }); + + return { + $and: [ + // add channel && brand filter + this.queries.integrations, + + // filter by integration type + { integrationId: { $in: _.pluck(integrations, '_id') } }, + ], + }; + } + + // filter by tag + public tagFilter(tagId: string): { tagIds: string[] } { + return { + tagIds: [tagId], + }; + } + + public dateFilter(startDate: string, endDate: string): IDateFilter { + return { + createdAt: { + $gte: fixDate(startDate), + $lte: fixDate(endDate), + }, + }; + } + + /* + * prepare all queries. do not do any action + */ + public async buildAllQueries(): Promise { + this.queries = { + default: this.defaultFilters(), + starred: {}, + status: {}, + unassigned: {}, + tag: {}, + channel: {}, + integrationType: {}, + + // find it using channel && brand + integrations: {}, + + participating: {}, + createdAt: {}, + }; + + // filter by channel + if (this.params.channelId) { + this.queries.channel = await this.channelFilter(this.params.channelId); + } + + // filter by channelId & brandId + this.queries.integrations = await this.integrationsFilter(); + + // unassigned + if (this.params.unassigned) { + this.queries.unassigned = this.unassignedFilter(); + } + + // participating + if (this.params.participating) { + this.queries.participating = this.participatingFilter(); + } + + // starred + if (this.params.starred) { + this.queries.starred = this.starredFilter(); + } + + // filter by status + if (this.params.status) { + this.queries.status = this.statusFilter([this.params.status]); + } + + // filter by tag + if (this.params.tag) { + this.queries.tag = this.tagFilter(this.params.tag); + } + + // filter by integration type + if (this.params.integrationType) { + this.queries.integrationType = await this.integrationTypeFilter(this.params.integrationType); + } + + if (this.params.startDate && this.params.endDate) { + this.queries.createdAt = this.dateFilter(this.params.startDate, this.params.endDate); + } + } + + public mainQuery(): any { + return { + ...this.queries.default, + ...this.queries.integrations, + ...this.queries.integrationType, + ...this.queries.unassigned, + ...this.queries.participating, + ...this.queries.status, + ...this.queries.starred, + ...this.queries.tag, + ...this.queries.createdAt, + }; + } +} diff --git a/src/data/resolvers/queries/conversations.ts b/src/data/resolvers/queries/conversations.ts index 32fdd97ce..b111e944d 100644 --- a/src/data/resolvers/queries/conversations.ts +++ b/src/data/resolvers/queries/conversations.ts @@ -1,284 +1,284 @@ -import { Brands, Channels, ConversationMessages, Conversations, Tags } from '../../../db/models'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { INTEGRATION_KIND_CHOICES } from '../../constants'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import QueryBuilder, { IListArgs } from './conversationQueryBuilder'; - -interface ICountBy { - [index: string]: number; -} - -interface IConversationRes { - [index: string]: number | ICountBy; -} - -// count helper -const count = async (query: any): Promise => { - const result = await Conversations.find(query).countDocuments(); - - return Number(result); -}; - -const countByChannels = async (qb: any): Promise => { - const byChannels: ICountBy = {}; - const channels = await Channels.find(); - - for (const channel of channels) { - byChannels[channel._id] = await count({ - ...qb.mainQuery(), - ...(await qb.channelFilter(channel._id)), - }); - } - - return byChannels; -}; - -const countByIntegrationTypes = async (qb: any): Promise => { - const byIntegrationTypes: ICountBy = {}; - - for (const intT of INTEGRATION_KIND_CHOICES.ALL) { - byIntegrationTypes[intT] = await count({ - ...qb.mainQuery(), - ...(await qb.integrationTypeFilter(intT)), - }); - } - - return byIntegrationTypes; -}; - -const countByTags = async (qb: any): Promise => { - const byTags: ICountBy = {}; - const queries = qb.queries; - const tags = await Tags.find(); - - for (const tag of tags) { - byTags[tag._id] = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.tagFilter(tag._id), - }); - } - - return byTags; -}; - -const countByBrands = async (qb: any): Promise => { - const byBrands: ICountBy = {}; - const brands = await Brands.find(); - - for (const brand of brands) { - byBrands[brand._id] = await count({ - ...qb.mainQuery(), - ...qb.intersectIntegrationIds(qb.queries.channel, await qb.brandFilter(brand._id)), - }); - } - - return byBrands; -}; - -const conversationQueries = { - /** - * Conversations list - */ - async conversations(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // filter by ids of conversations - if (params && params.ids) { - return Conversations.find({ _id: { $in: params.ids } }).sort({ - createdAt: -1, - }); - } - - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.find(qb.mainQuery()) - .sort({ updatedAt: -1 }) - .limit(params.limit || 0); - }, - - /** - * Get conversation messages - */ - async conversationMessages( - _root, - { - conversationId, - skip, - limit, - }: { - conversationId: string; - skip: number; - limit: number; - }, - ) { - const query = { conversationId }; - - if (limit) { - const messages = await ConversationMessages.find(query) - .sort({ createdAt: -1 }) - .skip(skip || 0) - .limit(limit); - - return messages.reverse(); - } - - return ConversationMessages.find(query) - .sort({ createdAt: 1 }) - .limit(50); - }, - - /** - * Get all conversation messages count. We will use it in pager - */ - async conversationMessagesTotalCount(_root, { conversationId }: { conversationId: string }) { - return ConversationMessages.countDocuments({ conversationId }); - }, - - /** - * Group conversation counts by brands, channels, integrations, status - */ - async conversationCounts(_root, params: IListArgs, { user }: { user: IUserDocument }) { - const { only } = params; - - const response: IConversationRes = {}; - - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - const queries = qb.queries; - - switch (only) { - case 'byChannels': - response.byChannels = await countByChannels(qb); - break; - - case 'byIntegrationTypes': - response.byIntegrationTypes = await countByIntegrationTypes(qb); - break; - - case 'byBrands': - response.byBrands = await countByBrands(qb); - break; - - case 'byTags': - response.byTags = await countByTags(qb); - break; - } - - // unassigned count - response.unassigned = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.unassignedFilter(), - }); - - // participating count - response.participating = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.participatingFilter(), - }); - - // starred count - response.starred = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.starredFilter(), - }); - - // resolved count - response.resolved = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.statusFilter(['closed']), - }); - - return response; - }, - - /** - * Get one conversation - */ - conversationDetail(_root, { _id }: { _id: string }) { - return Conversations.findOne({ _id }); - }, - - /** - * Get all conversations count. We will use it in pager - */ - async conversationsTotalCount(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.find(qb.mainQuery()).countDocuments(); - }, - - /** - * Get last conversation - */ - async conversationsGetLast(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.findOne(qb.mainQuery()).sort({ updatedAt: -1 }); - }, - - /** - * Get all unread conversations for logged in user - */ - async conversationsTotalUnreadCount(_root, _args, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder({}, { _id: user._id }); - - // get all possible integration ids - const integrationsFilter = await qb.integrationsFilter(); - - return Conversations.find({ - ...integrationsFilter, - status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, - readUserIds: { $ne: user._id }, - - // exclude engage messages if customer did not reply - $or: [ - { - userId: { $exists: true }, - messageCount: { $gt: 1 }, - }, - { - userId: { $exists: false }, - }, - ], - }).countDocuments(); - }, -}; - -moduleRequireLogin(conversationQueries); - -checkPermission(conversationQueries, 'conversations', 'showConversations', []); - -export default conversationQueries; +import { Brands, Channels, ConversationMessages, Conversations, Tags } from '../../../db/models'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { INTEGRATION_KIND_CHOICES } from '../../constants'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import QueryBuilder, { IListArgs } from './conversationQueryBuilder'; + +interface ICountBy { + [index: string]: number; +} + +interface IConversationRes { + [index: string]: number | ICountBy; +} + +// count helper +const count = async (query: any): Promise => { + const result = await Conversations.find(query).countDocuments(); + + return Number(result); +}; + +const countByChannels = async (qb: any): Promise => { + const byChannels: ICountBy = {}; + const channels = await Channels.find(); + + for (const channel of channels) { + byChannels[channel._id] = await count({ + ...qb.mainQuery(), + ...(await qb.channelFilter(channel._id)), + }); + } + + return byChannels; +}; + +const countByIntegrationTypes = async (qb: any): Promise => { + const byIntegrationTypes: ICountBy = {}; + + for (const intT of INTEGRATION_KIND_CHOICES.ALL) { + byIntegrationTypes[intT] = await count({ + ...qb.mainQuery(), + ...(await qb.integrationTypeFilter(intT)), + }); + } + + return byIntegrationTypes; +}; + +const countByTags = async (qb: any): Promise => { + const byTags: ICountBy = {}; + const queries = qb.queries; + const tags = await Tags.find(); + + for (const tag of tags) { + byTags[tag._id] = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.tagFilter(tag._id), + }); + } + + return byTags; +}; + +const countByBrands = async (qb: any): Promise => { + const byBrands: ICountBy = {}; + const brands = await Brands.find(); + + for (const brand of brands) { + byBrands[brand._id] = await count({ + ...qb.mainQuery(), + ...qb.intersectIntegrationIds(qb.queries.channel, await qb.brandFilter(brand._id)), + }); + } + + return byBrands; +}; + +const conversationQueries = { + /** + * Conversations list + */ + async conversations(_root, params: IListArgs, { user }: IContext) { + // filter by ids of conversations + if (params && params.ids) { + return Conversations.find({ _id: { $in: params.ids } }).sort({ + createdAt: -1, + }); + } + + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.find(qb.mainQuery()) + .sort({ updatedAt: -1 }) + .limit(params.limit || 0); + }, + + /** + * Get conversation messages + */ + async conversationMessages( + _root, + { + conversationId, + skip, + limit, + }: { + conversationId: string; + skip: number; + limit: number; + }, + ) { + const query = { conversationId }; + + if (limit) { + const messages = await ConversationMessages.find(query) + .sort({ createdAt: -1 }) + .skip(skip || 0) + .limit(limit); + + return messages.reverse(); + } + + return ConversationMessages.find(query) + .sort({ createdAt: 1 }) + .limit(50); + }, + + /** + * Get all conversation messages count. We will use it in pager + */ + async conversationMessagesTotalCount(_root, { conversationId }: { conversationId: string }) { + return ConversationMessages.countDocuments({ conversationId }); + }, + + /** + * Group conversation counts by brands, channels, integrations, status + */ + async conversationCounts(_root, params: IListArgs, { user }: IContext) { + const { only } = params; + + const response: IConversationRes = {}; + + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + const queries = qb.queries; + + switch (only) { + case 'byChannels': + response.byChannels = await countByChannels(qb); + break; + + case 'byIntegrationTypes': + response.byIntegrationTypes = await countByIntegrationTypes(qb); + break; + + case 'byBrands': + response.byBrands = await countByBrands(qb); + break; + + case 'byTags': + response.byTags = await countByTags(qb); + break; + } + + // unassigned count + response.unassigned = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.unassignedFilter(), + }); + + // participating count + response.participating = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.participatingFilter(), + }); + + // starred count + response.starred = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.starredFilter(), + }); + + // resolved count + response.resolved = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.statusFilter(['closed']), + }); + + return response; + }, + + /** + * Get one conversation + */ + conversationDetail(_root, { _id }: { _id: string }) { + return Conversations.findOne({ _id }); + }, + + /** + * Get all conversations count. We will use it in pager + */ + async conversationsTotalCount(_root, params: IListArgs, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.find(qb.mainQuery()).countDocuments(); + }, + + /** + * Get last conversation + */ + async conversationsGetLast(_root, params: IListArgs, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.findOne(qb.mainQuery()).sort({ updatedAt: -1 }); + }, + + /** + * Get all unread conversations for logged in user + */ + async conversationsTotalUnreadCount(_root, _args, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder({}, { _id: user._id }); + + // get all possible integration ids + const integrationsFilter = await qb.integrationsFilter(); + + return Conversations.find({ + ...integrationsFilter, + status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, + readUserIds: { $ne: user._id }, + + // exclude engage messages if customer did not reply + $or: [ + { + userId: { $exists: true }, + messageCount: { $gt: 1 }, + }, + { + userId: { $exists: false }, + }, + ], + }).countDocuments(); + }, +}; + +moduleRequireLogin(conversationQueries); + +checkPermission(conversationQueries, 'conversations', 'showConversations', []); + +export default conversationQueries; diff --git a/src/data/resolvers/queries/customers.ts b/src/data/resolvers/queries/customers.ts index fc2761cac..23e119ac1 100644 --- a/src/data/resolvers/queries/customers.ts +++ b/src/data/resolvers/queries/customers.ts @@ -1,282 +1,289 @@ -import { Brands, Customers, Forms, Integrations, Segments, Tags } from '../../../db/models'; -import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; -import { ISegment } from '../../../db/models/definitions/segments'; -import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES, INTEGRATION_KIND_CHOICES } from '../../constants'; -import { Builder as BuildQuery, IListArgs, sortBuilder } from '../../modules/coc/customers'; -import QueryBuilder from '../../modules/segments/queryBuilder'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface ICountBy { - [index: string]: number; -} - -interface ICountParams extends IListArgs { - only: string; -} - -const count = (query, mainQuery) => { - const findQuery = { $and: [mainQuery, query] }; - - return Customers.find(findQuery).countDocuments(); -}; - -const countBySegment = async (qb: any, mainQuery: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by segments - const segments = await Segments.find({ - contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, - }); - - // Count customers by segment - for (const s of segments) { - try { - counts[s._id] = await count(await qb.segmentFilter(s._id), mainQuery); - } catch (e) { - // catch mongo error - if (e.name === 'CastError') { - counts[s._id] = 0; - } else { - throw new Error(e); - } - } - } - - return counts; -}; - -const countByBrand = async (qb: any, mainQuery: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by brand - const brands = await Brands.find({}); - - for (const brand of brands) { - counts[brand._id] = await count(await qb.brandFilter(brand._id), mainQuery); - } - - return counts; -}; - -const countByField = async ( - mainQuery: any, - field: string, - values: [string], - multi: boolean = false, -): Promise => { - const counts: ICountBy = {}; - values.map(row => (counts[row] = 0)); - - const matchObj: any = {}; - matchObj[field] = { $in: values }; - let pipelines: any = [ - { $match: mainQuery }, - { $match: matchObj }, - { $group: { _id: `$${field}`, count: { $sum: 1 } } }, - ]; - if (multi) { - pipelines = [pipelines[0], { $unwind: `$${field}` }, pipelines[1], pipelines[2]]; - } - const countData = await Customers.aggregate(pipelines); - - await countData.forEach(element => { - counts[element._id] += element.count; - }); - - return counts; -}; - -const countByIntegration = async (mainQuery: any): Promise => { - const counts: ICountBy = {}; - const integrations = await Integrations.find({ kind: { $in: INTEGRATION_KIND_CHOICES.ALL } }).select({ - _id: 1, - name: 1, - kind: 1, - }); - const rawIntegrationIds: any = []; - const integrationMap = {}; - integrations.forEach(element => { - rawIntegrationIds.push(element._id); - integrationMap[element._id] = element.kind; - counts[element.kind || ''] = 0; - }); - - const query = { integrationId: { $in: rawIntegrationIds } }; - const findQuery = { $and: [mainQuery, query] }; - const countData = await countByField(findQuery, 'integrationId', rawIntegrationIds); - await Object.keys(countData).forEach(key => { - const integrationName = integrationMap[key]; - counts[integrationName] += countData[key]; - }); - - return counts; -}; - -const countByTag = async (mainQuery: any): Promise => { - // Count customers by tag - const tags = await Tags.find({ type: TAG_TYPES.CUSTOMER }).select('_id'); - const tagRawIds: any = []; - tags.forEach(element => { - tagRawIds.push(element._id); - }); - return countByField(mainQuery, 'tagIds', tagRawIds, true); -}; - -const countByLeadStatus = async (mainQuery: any): Promise => { - const statuses: any = []; - COC_LEAD_STATUS_TYPES.forEach(row => { - statuses.push(row); - }); - return countByField(mainQuery, 'leadStatus', statuses); -}; - -const countByLifecycleStatus = async (mainQuery: any): Promise => { - const stateTypes: any = []; - COC_LIFECYCLE_STATE_TYPES.forEach(row => { - stateTypes.push(row); - }); - return countByField(mainQuery, 'lifecycleState', stateTypes); -}; - -const countByForm = async (qb: any, mainQuery: any, params: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by submitted form - const forms = await Forms.find({}); - - for (const form of forms) { - counts[form._id] = await count(await qb.formFilter(form._id, params.startDate, params.endDate), mainQuery); - } - - return counts; -}; - -const customerQueries = { - /** - * Customers list - */ - async customers(_root, params: IListArgs) { - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - const sort = sortBuilder(params); - - return paginate(Customers.find(qb.mainQuery()).sort(sort), params); - }, - - /** - * Customers for only main list - */ - async customersMain(_root, params: IListArgs) { - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - const sort = sortBuilder(params); - - const list = await paginate(Customers.find(qb.mainQuery()).sort(sort), params); - const totalCount = await Customers.find(qb.mainQuery()).countDocuments(); - - return { list, totalCount }; - }, - - /** - * Group customer counts by brands, segments, integrations, tags - */ - async customerCounts(_root, params: ICountParams) { - const { only } = params; - - const counts = { - bySegment: {}, - byBrand: {}, - byIntegrationType: {}, - byTag: {}, - byFakeSegment: 0, - byForm: {}, - byLeadStatus: {}, - byLifecycleState: {}, - }; - - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - let mainQuery = qb.mainQuery(); - - // if passed at least one filter other than perPage - // then find all filtered customers then add subsequent filter to it - if (Object.keys(params).length > 1) { - const customers = await Customers.find(qb.mainQuery(), { _id: 1 }); - const customerIds = customers.map(customer => customer._id); - - mainQuery = { _id: { $in: customerIds } }; - } - - switch (only) { - case 'bySegment': - counts.bySegment = await countBySegment(qb, mainQuery); - break; - - case 'byBrand': - counts.byBrand = await countByBrand(qb, mainQuery); - break; - - case 'byTag': - counts.byTag = await countByTag(mainQuery); - break; - - case 'byForm': - counts.byForm = await countByForm(qb, mainQuery, params); - break; - case 'byLeadStatus': - counts.byLeadStatus = await countByLeadStatus(mainQuery); - break; - - case 'byLifecycleState': - counts.byLifecycleState = await countByLifecycleStatus(mainQuery); - break; - - case 'byIntegrationType': - counts.byIntegrationType = await countByIntegration(mainQuery); - break; - } - - // Count customers by fake segment - if (params.byFakeSegment) { - counts.byFakeSegment = await count(await QueryBuilder.segments(params.byFakeSegment), mainQuery); - } - - return counts; - }, - - /** - * Publishes customers list for the preview - * when creating/editing a customer segment - */ - async customerListForSegmentPreview(_root, { segment, limit }: { segment: ISegment; limit: number }) { - const headSegment = await Segments.findOne({ _id: segment.subOf }); - - const query = await QueryBuilder.segments(segment, headSegment); - const sort = { 'messengerData.lastSeenAt': -1 }; - - return Customers.find(query) - .sort(sort) - .limit(limit); - }, - - /** - * Get one customer - */ - customerDetail(_root, { _id }: { _id: string }) { - return Customers.findOne({ _id }); - }, -}; - -moduleRequireLogin(customerQueries); - -checkPermission(customerQueries, 'customers', 'showCustomers', []); -checkPermission(customerQueries, 'customersMain', 'showCustomers', { list: [], totalCount: 0 }); - -export default customerQueries; +import { Brands, Customers, Forms, Integrations, Segments, Tags } from '../../../db/models'; +import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; +import { ISegment } from '../../../db/models/definitions/segments'; +import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES, INTEGRATION_KIND_CHOICES } from '../../constants'; +import { Builder as BuildQuery, IListArgs, sortBuilder } from '../../modules/coc/customers'; +import QueryBuilder from '../../modules/segments/queryBuilder'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +interface ICountBy { + [index: string]: number; +} + +interface ICountParams extends IListArgs { + only: string; +} + +const count = (query, mainQuery) => { + const findQuery = { $and: [mainQuery, query] }; + + return Customers.find(findQuery).countDocuments(); +}; + +const countBySegment = async (qb: any, mainQuery: any): Promise => { + const counts: ICountBy = {}; + + // Count customers by segments + const segments = await Segments.find({ + contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, + }); + + // Count customers by segment + for (const s of segments) { + try { + counts[s._id] = await count(await qb.segmentFilter(s._id), mainQuery); + } catch (e) { + // catch mongo error + if (e.name === 'CastError') { + counts[s._id] = 0; + } else { + throw new Error(e); + } + } + } + + return counts; +}; + +const countByBrand = async (qb: any, mainQuery: any): Promise => { + const counts: ICountBy = {}; + + // Count customers by brand + const brands = await Brands.find({}); + + for (const brand of brands) { + counts[brand._id] = await count(await qb.brandFilter(brand._id), mainQuery); + } + + return counts; +}; + +const countByField = async ( + mainQuery: any, + field: string, + values: [string], + multi: boolean = false, +): Promise => { + const counts: ICountBy = {}; + values.map(row => (counts[row] = 0)); + + const matchObj: any = {}; + matchObj[field] = { $in: values }; + let pipelines: any = [ + { $match: mainQuery }, + { $match: matchObj }, + { $group: { _id: `$${field}`, count: { $sum: 1 } } }, + ]; + if (multi) { + pipelines = [pipelines[0], { $unwind: `$${field}` }, pipelines[1], pipelines[2]]; + } + const countData = await Customers.aggregate(pipelines); + + await countData.forEach(element => { + counts[element._id] += element.count; + }); + + return counts; +}; + +const countByIntegration = async (mainQuery: any): Promise => { + const counts: ICountBy = {}; + + const integrations = await Integrations.find({ kind: { $in: INTEGRATION_KIND_CHOICES.ALL } }).select({ + _id: 1, + name: 1, + kind: 1, + }); + + const rawIntegrationIds: any = []; + const integrationMap = {}; + + integrations.forEach(element => { + rawIntegrationIds.push(element._id); + integrationMap[element._id] = element.kind; + counts[element.kind || ''] = 0; + }); + + const query = { integrationId: { $in: rawIntegrationIds } }; + const findQuery = { $and: [mainQuery, query] }; + const countData = await countByField(findQuery, 'integrationId', rawIntegrationIds); + + await Object.keys(countData).forEach(key => { + const integrationName = integrationMap[key]; + counts[integrationName] += countData[key]; + }); + + return counts; +}; + +const countByTag = async (mainQuery: any): Promise => { + // Count customers by tag + const tags = await Tags.find({ type: TAG_TYPES.CUSTOMER }).select('_id'); + const tagRawIds: any = []; + tags.forEach(element => { + tagRawIds.push(element._id); + }); + return countByField(mainQuery, 'tagIds', tagRawIds, true); +}; + +const countByLeadStatus = async (mainQuery: any): Promise => { + const statuses: any = []; + COC_LEAD_STATUS_TYPES.forEach(row => { + statuses.push(row); + }); + return countByField(mainQuery, 'leadStatus', statuses); +}; + +const countByLifecycleStatus = async (mainQuery: any): Promise => { + const stateTypes: any = []; + COC_LIFECYCLE_STATE_TYPES.forEach(row => { + stateTypes.push(row); + }); + return countByField(mainQuery, 'lifecycleState', stateTypes); +}; + +const countByForm = async (qb: any, mainQuery: any, params: any): Promise => { + const counts: ICountBy = {}; + + // Count customers by submitted form + const forms = await Forms.find({}); + + for (const form of forms) { + counts[form._id] = await count(await qb.formFilter(form._id, params.startDate, params.endDate), mainQuery); + } + + return counts; +}; + +const customerQueries = { + /** + * Customers list + */ + async customers(_root, params: IListArgs, { commonQuerySelector }: IContext) { + const qb = new BuildQuery(params); + + await qb.buildAllQueries(); + + const sort = sortBuilder(params); + + return paginate(Customers.find({ ...commonQuerySelector, ...qb.mainQuery() }).sort(sort), params); + }, + + /** + * Customers for only main list + */ + async customersMain(_root, params: IListArgs, { commonQuerySelector }: IContext) { + const qb = new BuildQuery(params); + + await qb.buildAllQueries(); + + const sort = sortBuilder(params); + + const selector = { ...commonQuerySelector, ...qb.mainQuery() }; + + const list = await paginate(Customers.find(selector).sort(sort), params); + const totalCount = await Customers.find(selector).countDocuments(); + + return { list, totalCount }; + }, + + /** + * Group customer counts by brands, segments, integrations, tags + */ + async customerCounts(_root, params: ICountParams, { commonQuerySelector }: IContext) { + const { only } = params; + + const counts = { + bySegment: {}, + byBrand: {}, + byIntegrationType: {}, + byTag: {}, + byFakeSegment: 0, + byForm: {}, + byLeadStatus: {}, + byLifecycleState: {}, + }; + + const qb = new BuildQuery(params); + + await qb.buildAllQueries(); + + let mainQuery = { ...commonQuerySelector, ...qb.mainQuery() }; + + // if passed at least one filter other than perPage + // then find all filtered customers then add subsequent filter to it + if (Object.keys(params).length > 1) { + const customers = await Customers.find(mainQuery, { _id: 1 }); + const customerIds = customers.map(customer => customer._id); + + mainQuery = { _id: { $in: customerIds } }; + } + + switch (only) { + case 'bySegment': + counts.bySegment = await countBySegment(qb, mainQuery); + break; + + case 'byBrand': + counts.byBrand = await countByBrand(qb, mainQuery); + break; + + case 'byTag': + counts.byTag = await countByTag(mainQuery); + break; + + case 'byForm': + counts.byForm = await countByForm(qb, mainQuery, params); + break; + case 'byLeadStatus': + counts.byLeadStatus = await countByLeadStatus(mainQuery); + break; + + case 'byLifecycleState': + counts.byLifecycleState = await countByLifecycleStatus(mainQuery); + break; + + case 'byIntegrationType': + counts.byIntegrationType = await countByIntegration(mainQuery); + break; + } + + // Count customers by fake segment + if (params.byFakeSegment) { + counts.byFakeSegment = await count(await QueryBuilder.segments(params.byFakeSegment), mainQuery); + } + + return counts; + }, + + /** + * Publishes customers list for the preview + * when creating/editing a customer segment + */ + async customerListForSegmentPreview(_root, { segment, limit }: { segment: ISegment; limit: number }) { + const headSegment = await Segments.findOne({ _id: segment.subOf }); + + const query = await QueryBuilder.segments(segment, headSegment); + const sort = { 'messengerData.lastSeenAt': -1 }; + + return Customers.find(query) + .sort(sort) + .limit(limit); + }, + + /** + * Get one customer + */ + customerDetail(_root, { _id }: { _id: string }) { + return Customers.findOne({ _id }); + }, +}; + +moduleRequireLogin(customerQueries); + +checkPermission(customerQueries, 'customers', 'showCustomers', []); +checkPermission(customerQueries, 'customersMain', 'showCustomers', { list: [], totalCount: 0 }); + +export default customerQueries; diff --git a/src/data/resolvers/queries/dealInsights.ts b/src/data/resolvers/queries/dealInsights.ts index fc69b1852..aafb9cbb0 100644 --- a/src/data/resolvers/queries/dealInsights.ts +++ b/src/data/resolvers/queries/dealInsights.ts @@ -1,5 +1,4 @@ import { Deals } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { INSIGHT_TYPES } from '../../constants'; import { getDateFieldAsStr } from '../../modules/insights/aggregationUtils'; import { IDealListArgs } from '../../modules/insights/types'; @@ -13,12 +12,13 @@ import { getTimezone, } from '../../modules/insights/utils'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const dealInsightQueries = { /** * Counts deals by each hours in each days. */ - async dealInsightsPunchCard(_root, args: IDealListArgs, { user }: { user: IUserDocument }) { + async dealInsightsPunchCard(_root, args: IDealListArgs, { user }: IContext) { const selector = await getDealSelector(args); return generatePunchData(Deals, selector, user); @@ -57,7 +57,7 @@ const dealInsightQueries = { /** * Calculates won or lost deals for each team members. */ - async dealInsightsByTeamMember(_root, args: IDealListArgs, { user }: { user: IUserDocument }) { + async dealInsightsByTeamMember(_root, args: IDealListArgs, { user }: IContext) { const dealMatch = await getDealSelector(args); const insightAggregateData = await Deals.aggregate([ diff --git a/src/data/resolvers/queries/deals.ts b/src/data/resolvers/queries/deals.ts index 31d985fb3..6c0230829 100644 --- a/src/data/resolvers/queries/deals.ts +++ b/src/data/resolvers/queries/deals.ts @@ -1,116 +1,116 @@ -import { Deals } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateDealCommonFilters } from './boardUtils'; - -interface IDealListParams extends IListParams { - productIds?: [string]; -} - -const dealQueries = { - /** - * Deals list - */ - async deals(_root, args: IDealListParams) { - const filter = await generateDealCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Deals.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Deal total amounts - */ - async dealsTotalAmounts(_root, args: IDealListParams) { - const filter = await generateDealCommonFilters(args); - - const dealCount = await Deals.find(filter).countDocuments(); - const amountList = await Deals.aggregate([ - { - $match: filter, - }, - { - $lookup: { - from: 'stages', - let: { letStageId: '$stageId' }, - pipeline: [ - { - $match: { - $expr: { - $eq: ['$_id', '$$letStageId'], - }, - }, - }, - { - $project: { - probability: { - $cond: { - if: { - $or: [{ $eq: ['$probability', 'Won'] }, { $eq: ['$probability', 'Lost'] }], - }, - then: '$probability', - else: 'In progress', - }, - }, - }, - }, - ], - as: 'stageProbability', - }, - }, - { - $unwind: '$productsData', - }, - { - $unwind: '$stageProbability', - }, - { - $project: { - amount: '$productsData.amount', - currency: '$productsData.currency', - type: '$stageProbability.probability', - }, - }, - { - $group: { - _id: { currency: '$currency', type: '$type' }, - - amount: { $sum: '$amount' }, - }, - }, - { - $group: { - _id: '$_id.type', - currencies: { - $push: { amount: '$amount', name: '$_id.currency' }, - }, - }, - }, - { - $sort: { _id: -1 }, - }, - ]); - - const totalForType = amountList.map(type => { - return { _id: Math.random(), name: type._id, currencies: type.currencies }; - }); - - return { _id: Math.random(), dealCount, totalForType }; - }, - - /** - * Deal detail - */ - dealDetail(_root, { _id }: { _id: string }) { - return Deals.findOne({ _id }); - }, -}; - -moduleRequireLogin(dealQueries); - -checkPermission(dealQueries, 'deals', 'showDeals', []); - -export default dealQueries; +import { Deals } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IListParams } from './boards'; +import { generateDealCommonFilters } from './boardUtils'; + +interface IDealListParams extends IListParams { + productIds?: [string]; +} + +const dealQueries = { + /** + * Deals list + */ + async deals(_root, args: IDealListParams) { + const filter = await generateDealCommonFilters(args); + const sort = { order: 1, createdAt: -1 }; + + return Deals.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Deal total amounts + */ + async dealsTotalAmounts(_root, args: IDealListParams) { + const filter = await generateDealCommonFilters(args); + + const dealCount = await Deals.find(filter).countDocuments(); + const amountList = await Deals.aggregate([ + { + $match: filter, + }, + { + $lookup: { + from: 'stages', + let: { letStageId: '$stageId' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$_id', '$$letStageId'], + }, + }, + }, + { + $project: { + probability: { + $cond: { + if: { + $or: [{ $eq: ['$probability', 'Won'] }, { $eq: ['$probability', 'Lost'] }], + }, + then: '$probability', + else: 'In progress', + }, + }, + }, + }, + ], + as: 'stageProbability', + }, + }, + { + $unwind: '$productsData', + }, + { + $unwind: '$stageProbability', + }, + { + $project: { + amount: '$productsData.amount', + currency: '$productsData.currency', + type: '$stageProbability.probability', + }, + }, + { + $group: { + _id: { currency: '$currency', type: '$type' }, + + amount: { $sum: '$amount' }, + }, + }, + { + $group: { + _id: '$_id.type', + currencies: { + $push: { amount: '$amount', name: '$_id.currency' }, + }, + }, + }, + { + $sort: { _id: -1 }, + }, + ]); + + const totalForType = amountList.map(type => { + return { _id: Math.random(), name: type._id, currencies: type.currencies }; + }); + + return { _id: Math.random(), dealCount, totalForType }; + }, + + /** + * Deal detail + */ + dealDetail(_root, { _id }: { _id: string }) { + return Deals.findOne({ _id }); + }, +}; + +moduleRequireLogin(dealQueries); + +checkPermission(dealQueries, 'deals', 'showDeals', []); + +export default dealQueries; diff --git a/src/data/resolvers/queries/emailTemplates.ts b/src/data/resolvers/queries/emailTemplates.ts index 4a4a5ae9c..fc91e71aa 100644 --- a/src/data/resolvers/queries/emailTemplates.ts +++ b/src/data/resolvers/queries/emailTemplates.ts @@ -1,24 +1,25 @@ -import { EmailTemplates } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const emailTemplateQueries = { - /** - * Email templates list - */ - emailTemplates(_root, args: { page: number; perPage: number }) { - return paginate(EmailTemplates.find({}), args); - }, - - /** - * Get all email templates count. We will use it in pager - */ - emailTemplatesTotalCount() { - return EmailTemplates.find({}).countDocuments(); - }, -}; - -requireLogin(emailTemplateQueries, 'emailTemplatesTotalCount'); -checkPermission(emailTemplateQueries, 'emailTemplates', 'showEmailTemplates', []); - -export default emailTemplateQueries; +import { EmailTemplates } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +const emailTemplateQueries = { + /** + * Email templates list + */ + emailTemplates(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + return paginate(EmailTemplates.find(commonQuerySelector), args); + }, + + /** + * Get all email templates count. We will use it in pager + */ + emailTemplatesTotalCount() { + return EmailTemplates.find({}).countDocuments(); + }, +}; + +requireLogin(emailTemplateQueries, 'emailTemplatesTotalCount'); +checkPermission(emailTemplateQueries, 'emailTemplates', 'showEmailTemplates', []); + +export default emailTemplateQueries; diff --git a/src/data/resolvers/queries/engages.ts b/src/data/resolvers/queries/engages.ts index d3b6430b9..40753ac3d 100644 --- a/src/data/resolvers/queries/engages.ts +++ b/src/data/resolvers/queries/engages.ts @@ -1,210 +1,221 @@ -import { EngageMessages, Tags } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface IListArgs { - kind?: string; - status?: string; - tag?: string; - ids?: string[]; - brandIds?: string[]; - segmentIds?: string[]; - tagIds?: string[]; - page?: number; - perPage?: number; -} - -interface IQuery { - kind?: string; -} - -interface IStatusQueryBuilder { - [index: string]: boolean | string; -} - -interface ICountsByStatus { - [index: string]: number; -} - -interface ICountsByTag { - [index: string]: number; -} - -// basic count helper -const count = async (selector: {}): Promise => { - const res = await EngageMessages.find(selector).countDocuments(); - return Number(res); -}; - -// Tag query builder -const tagQueryBuilder = (tagId: string) => ({ tagIds: tagId }); - -// status query builder -const statusQueryBuilder = (status: string, user?: IUserDocument): IStatusQueryBuilder => { - if (status === 'live') { - return { isLive: true }; - } - - if (status === 'draft') { - return { isDraft: true }; - } - - if (status === 'paused') { - return { isLive: false }; - } - - if (status === 'yours' && user) { - return { fromUserId: user._id }; - } - - return {}; -}; - -// count for each kind -const countsByKind = async () => ({ - all: await count({}), - auto: await count({ kind: 'auto' }), - visitorAuto: await count({ kind: 'visitorAuto' }), - manual: await count({ kind: 'manual' }), -}); - -// count for each status type -const countsByStatus = async ({ kind, user }: { kind: string; user: IUserDocument }): Promise => { - const query: IQuery = {}; - - if (kind) { - query.kind = kind; - } - - return { - live: await count({ ...query, ...statusQueryBuilder('live') }), - draft: await count({ ...query, ...statusQueryBuilder('draft') }), - paused: await count({ ...query, ...statusQueryBuilder('paused') }), - yours: await count({ ...query, ...statusQueryBuilder('yours', user) }), - }; -}; - -// cout for each tag -const countsByTag = async ({ - kind, - status, - user, -}: { - kind: string; - status: string; - user: IUserDocument; -}): Promise => { - let query: any = {}; - - if (kind) { - query.kind = kind; - } - - if (status) { - query = { ...query, ...statusQueryBuilder(status, user) }; - } - - const tags = await Tags.find({ type: 'engageMessage' }); - - // const response: {[name: string]: number} = {}; - const response: ICountsByTag[] = []; - - for (const tag of tags) { - response[tag._id] = await count({ ...query, ...tagQueryBuilder(tag._id) }); - } - - return response; -}; - -/* - * List filter - */ -const listQuery = ({ segmentIds, brandIds, tagIds, kind, status, tag, ids }: IListArgs, user: IUserDocument): any => { - if (ids) { - return EngageMessages.find({ _id: { $in: ids } }); - } - - if (segmentIds) { - return EngageMessages.find({ segmentIds: { $in: segmentIds } }); - } - - if (brandIds) { - return EngageMessages.find({ brandIds: { $in: brandIds } }); - } - - if (tagIds) { - return EngageMessages.find({ tagIds: { $in: tagIds } }); - } - - let query: any = {}; - - // filter by kind - if (kind) { - query.kind = kind; - } - - // filter by status - if (status) { - query = { ...query, ...statusQueryBuilder(status, user) }; - } - - // filter by tag - if (tag) { - query = { ...query, ...tagQueryBuilder(tag) }; - } - - return query; -}; - -const engageQueries = { - /** - * Group engage messages counts by kind, status, tag - */ - engageMessageCounts( - _root, - { name, kind, status }: { name: string; kind: string; status: string }, - { user }: { user: IUserDocument }, - ) { - if (name === 'kind') { - return countsByKind(); - } - - if (name === 'status') { - return countsByStatus({ kind, user }); - } - - if (name === 'tag') { - return countsByTag({ kind, status, user }); - } - }, - - /** - * Engage messages list - */ - engageMessages(_root, args: IListArgs, { user }: { user: IUserDocument }) { - return paginate(EngageMessages.find(listQuery(args, user)), args); - }, - - /** - * Get one message - */ - engageMessageDetail(_root, { _id }: { _id: string }) { - return EngageMessages.findOne({ _id }); - }, - - /** - * Get all messages count. We will use it in pager - */ - engageMessagesTotalCount(_root, args: IListArgs, { user }: { user: IUserDocument }) { - return EngageMessages.find(listQuery(args, user)).countDocuments(); - }, -}; - -requireLogin(engageQueries, 'engageMessagesTotalCount'); -requireLogin(engageQueries, 'engageMessageCounts'); -requireLogin(engageQueries, 'engageMessageDetail'); - -checkPermission(engageQueries, 'engageMessages', 'showEngagesMessages', []); - -export default engageQueries; +import { EngageMessages, Tags } from '../../../db/models'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +interface IListArgs { + kind?: string; + status?: string; + tag?: string; + ids?: string[]; + brandIds?: string[]; + segmentIds?: string[]; + tagIds?: string[]; + page?: number; + perPage?: number; +} + +interface IQuery { + kind?: string; +} + +interface IStatusQueryBuilder { + [index: string]: boolean | string; +} + +interface ICountsByStatus { + [index: string]: number; +} + +interface ICountsByTag { + [index: string]: number; +} + +// basic count helper +const count = async (selector: {}): Promise => { + const res = await EngageMessages.find(selector).countDocuments(); + return Number(res); +}; + +// Tag query builder +const tagQueryBuilder = (tagId: string) => ({ tagIds: tagId }); + +// status query builder +const statusQueryBuilder = (status: string, user?: IUserDocument): IStatusQueryBuilder => { + if (status === 'live') { + return { isLive: true }; + } + + if (status === 'draft') { + return { isDraft: true }; + } + + if (status === 'paused') { + return { isLive: false }; + } + + if (status === 'yours' && user) { + return { fromUserId: user._id }; + } + + return {}; +}; + +// count for each kind +const countsByKind = async commonSelector => ({ + all: await count(commonSelector), + auto: await count({ ...commonSelector, kind: 'auto' }), + visitorAuto: await count({ ...commonSelector, kind: 'visitorAuto' }), + manual: await count({ ...commonSelector, kind: 'manual' }), +}); + +// count for each status type +const countsByStatus = async ( + commonSelector, + { kind, user }: { kind: string; user: IUserDocument }, +): Promise => { + const query: IQuery = commonSelector; + + if (kind) { + query.kind = kind; + } + + return { + live: await count({ ...query, ...statusQueryBuilder('live') }), + draft: await count({ ...query, ...statusQueryBuilder('draft') }), + paused: await count({ ...query, ...statusQueryBuilder('paused') }), + yours: await count({ ...query, ...statusQueryBuilder('yours', user) }), + }; +}; + +// cout for each tag +const countsByTag = async ( + commonSelector, + { + kind, + status, + user, + }: { + kind: string; + status: string; + user: IUserDocument; + }, +): Promise => { + let query: any = commonSelector; + + if (kind) { + query.kind = kind; + } + + if (status) { + query = { ...query, ...statusQueryBuilder(status, user) }; + } + + const tags = await Tags.find({ type: 'engageMessage' }); + + // const response: {[name: string]: number} = {}; + const response: ICountsByTag[] = []; + + for (const tag of tags) { + response[tag._id] = await count({ ...query, ...tagQueryBuilder(tag._id) }); + } + + return response; +}; + +/* + * List filter + */ +const listQuery = ( + commonSelector, + { segmentIds, brandIds, tagIds, kind, status, tag, ids }: IListArgs, + user: IUserDocument, +): any => { + if (ids) { + return EngageMessages.find({ ...commonSelector, _id: { $in: ids } }); + } + + if (segmentIds) { + return EngageMessages.find({ ...commonSelector, segmentIds: { $in: segmentIds } }); + } + + if (brandIds) { + return EngageMessages.find({ ...commonSelector, brandIds: { $in: brandIds } }); + } + + if (tagIds) { + return EngageMessages.find({ ...commonSelector, tagIds: { $in: tagIds } }); + } + + let query = commonSelector; + + // filter by kind + if (kind) { + query.kind = kind; + } + + // filter by status + if (status) { + query = { ...query, ...statusQueryBuilder(status, user) }; + } + + // filter by tag + if (tag) { + query = { ...query, ...tagQueryBuilder(tag) }; + } + + return query; +}; + +const engageQueries = { + /** + * Group engage messages counts by kind, status, tag + */ + engageMessageCounts( + _root, + { name, kind, status }: { name: string; kind: string; status: string }, + { user, commonQuerySelector }: IContext, + ) { + if (name === 'kind') { + return countsByKind(commonQuerySelector); + } + + if (name === 'status') { + return countsByStatus(commonQuerySelector, { kind, user }); + } + + if (name === 'tag') { + return countsByTag(commonQuerySelector, { kind, status, user }); + } + }, + + /** + * Engage messages list + */ + engageMessages(_root, args: IListArgs, { user, commonQuerySelector }: IContext) { + return paginate(EngageMessages.find(listQuery(commonQuerySelector, args, user)), args); + }, + + /** + * Get one message + */ + engageMessageDetail(_root, { _id }: { _id: string }) { + return EngageMessages.findOne({ _id }); + }, + + /** + * Get all messages count. We will use it in pager + */ + engageMessagesTotalCount(_root, args: IListArgs, { user, commonQuerySelector }: IContext) { + return EngageMessages.find(listQuery(commonQuerySelector, args, user)).countDocuments(); + }, +}; + +requireLogin(engageQueries, 'engageMessagesTotalCount'); +requireLogin(engageQueries, 'engageMessageCounts'); +requireLogin(engageQueries, 'engageMessageDetail'); + +checkPermission(engageQueries, 'engageMessages', 'showEngagesMessages', []); + +export default engageQueries; diff --git a/src/data/resolvers/queries/fields.ts b/src/data/resolvers/queries/fields.ts index b0f8b59df..3ebf58757 100644 --- a/src/data/resolvers/queries/fields.ts +++ b/src/data/resolvers/queries/fields.ts @@ -1,183 +1,183 @@ -import { FIELD_CONTENT_TYPES, FIELDS_GROUPS_CONTENT_TYPES, INTEGRATION_KIND_CHOICES } from '../../../data/constants'; -import { Brands, Companies, Customers, Fields, FieldsGroups, Integrations } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; - -interface IFieldsQuery { - contentType: string; - contentTypeId?: string; -} - -interface IfieldsDefaultColmns { - [index: number]: { name: string; label: string; order: number } | {}; -} - -const fieldQueries = { - /** - * Fields list - */ - fields(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { - const query: IFieldsQuery = { contentType }; - - if (contentTypeId) { - query.contentTypeId = contentTypeId; - } - - return Fields.find(query).sort({ order: 1 }); - }, - - /** - * Generates all field choices base on given kind. - * For example if kind is customer - * then it will generate customer related fields - * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] - */ - async fieldsCombinedByContentType(_root, { contentType }: { contentType: string }) { - /* - * Generates fields using given schema - */ - const generateFieldsFromSchema = (queSchema: any, namePrefix: string) => { - const queFields: any = []; - - // field definations - const paths = queSchema.paths; - - queSchema.eachPath(name => { - const label = paths[name].options.label; - - // add to fields list - if (label) { - queFields.push({ - _id: Math.random(), - name: `${namePrefix}${name}`, - label, - }); - } - }); - - return queFields; - }; - - let schema: any = Companies.schema; - let fields: Array<{ _id: number; name: string; label?: string; brandName?: string; brandId?: string }> = []; - - if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { - const messengerIntegrations = await Integrations.find({ kind: INTEGRATION_KIND_CHOICES.MESSENGER }); - - // generate messengerData.customData fields - for (const integration of messengerIntegrations) { - const brand = await Brands.findOne({ _id: integration.brandId }); - - const lastCustomers = await Customers.find({ - integrationId: integration._id, - $and: [ - { 'messengerData.customData': { $exists: true } }, - { 'messengerData.customData': { $ne: null } }, - { 'messengerData.customData': { $ne: {} } }, - ], - }) - .sort({ createdAt: -1 }) - .limit(1); - - if (brand && integration && lastCustomers.length > 0) { - const [lastCustomer] = lastCustomers; - - if (lastCustomer.messengerData) { - const customDataFields = Object.keys(lastCustomer.messengerData.customData || {}); - - for (const customDataField of customDataFields) { - fields.push({ - _id: Math.random(), - name: `messengerData.customData.${customDataField}`, - label: customDataField, - brandName: brand.name, - brandId: brand._id, - }); - } - } - } - } - - schema = Customers.schema; - } - - // generate list using customer or company schema - fields = [...fields, ...generateFieldsFromSchema(schema, '')]; - - schema.eachPath(name => { - const path = schema.paths[name]; - - // extend fields list using sub schema fields - if (path.schema) { - fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; - } - }); - - const customFields = await Fields.find({ contentType }); - - // extend fields list using custom fields - for (const customField of customFields) { - const group = await FieldsGroups.findOne({ _id: customField.groupId }); - - if (group && group.isVisible && customField.isVisible) { - fields.push({ - _id: Math.random(), - name: `customFieldsData.${customField._id}`, - label: customField.text, - }); - } - } - - return fields; - }, - - /** - * Default list columns config - */ - fieldsDefaultColumnsConfig(_root, { contentType }: { contentType: string }): IfieldsDefaultColmns { - if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { - return [ - { name: 'firstName', label: 'First name', order: 1 }, - { name: 'lastName', label: 'Last name', order: 1 }, - { name: 'primaryEmail', label: 'Primary email', order: 2 }, - { name: 'primaryPhone', label: 'Primary phone', order: 3 }, - ]; - } - - if (contentType === FIELD_CONTENT_TYPES.COMPANY) { - return [ - { name: 'primaryName', label: 'Primary Name', order: 1 }, - { name: 'size', label: 'Size', order: 2 }, - { name: 'links.website', label: 'Website', order: 3 }, - { name: 'industry', label: 'Industry', order: 4 }, - { name: 'plan', label: 'Plan', order: 5 }, - { name: 'lastSeenAt', label: 'Last seen at', order: 6 }, - { name: 'sessionCount', label: 'Session count', order: 7 }, - ]; - } - - return []; - }, -}; - -requireLogin(fieldQueries, 'fieldsCombinedByContentType'); -requireLogin(fieldQueries, 'fieldsDefaultColumnsConfig'); - -checkPermission(fieldQueries, 'fields', 'showFields', []); - -const fieldsGroupQueries = { - /** - * Fields group list - */ - fieldsGroups(_root, { contentType }: { contentType: string }) { - const query: any = {}; - - // querying by content type - query.contentType = contentType || FIELDS_GROUPS_CONTENT_TYPES.CUSTOMER; - - return FieldsGroups.find(query).sort({ order: 1 }); - }, -}; - -checkPermission(fieldsGroupQueries, 'fieldsGroups', 'showFieldsGroups', []); - -export { fieldQueries, fieldsGroupQueries }; +import { FIELD_CONTENT_TYPES, FIELDS_GROUPS_CONTENT_TYPES, INTEGRATION_KIND_CHOICES } from '../../../data/constants'; +import { Brands, Companies, Customers, Fields, FieldsGroups, Integrations } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; + +interface IFieldsQuery { + contentType: string; + contentTypeId?: string; +} + +interface IfieldsDefaultColmns { + [index: number]: { name: string; label: string; order: number } | {}; +} + +const fieldQueries = { + /** + * Fields list + */ + fields(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { + const query: IFieldsQuery = { contentType }; + + if (contentTypeId) { + query.contentTypeId = contentTypeId; + } + + return Fields.find(query).sort({ order: 1 }); + }, + + /** + * Generates all field choices base on given kind. + * For example if kind is customer + * then it will generate customer related fields + * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] + */ + async fieldsCombinedByContentType(_root, { contentType }: { contentType: string }) { + /* + * Generates fields using given schema + */ + const generateFieldsFromSchema = (queSchema: any, namePrefix: string) => { + const queFields: any = []; + + // field definations + const paths = queSchema.paths; + + queSchema.eachPath(name => { + const label = paths[name].options.label; + + // add to fields list + if (label) { + queFields.push({ + _id: Math.random(), + name: `${namePrefix}${name}`, + label, + }); + } + }); + + return queFields; + }; + + let schema: any = Companies.schema; + let fields: Array<{ _id: number; name: string; label?: string; brandName?: string; brandId?: string }> = []; + + if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { + const messengerIntegrations = await Integrations.find({ kind: INTEGRATION_KIND_CHOICES.MESSENGER }); + + // generate messengerData.customData fields + for (const integration of messengerIntegrations) { + const brand = await Brands.findOne({ _id: integration.brandId }); + + const lastCustomers = await Customers.find({ + integrationId: integration._id, + $and: [ + { 'messengerData.customData': { $exists: true } }, + { 'messengerData.customData': { $ne: null } }, + { 'messengerData.customData': { $ne: {} } }, + ], + }) + .sort({ createdAt: -1 }) + .limit(1); + + if (brand && integration && lastCustomers.length > 0) { + const [lastCustomer] = lastCustomers; + + if (lastCustomer.messengerData) { + const customDataFields = Object.keys(lastCustomer.messengerData.customData || {}); + + for (const customDataField of customDataFields) { + fields.push({ + _id: Math.random(), + name: `messengerData.customData.${customDataField}`, + label: customDataField, + brandName: brand.name, + brandId: brand._id, + }); + } + } + } + } + + schema = Customers.schema; + } + + // generate list using customer or company schema + fields = [...fields, ...generateFieldsFromSchema(schema, '')]; + + schema.eachPath(name => { + const path = schema.paths[name]; + + // extend fields list using sub schema fields + if (path.schema) { + fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; + } + }); + + const customFields = await Fields.find({ contentType }); + + // extend fields list using custom fields + for (const customField of customFields) { + const group = await FieldsGroups.findOne({ _id: customField.groupId }); + + if (group && group.isVisible && customField.isVisible) { + fields.push({ + _id: Math.random(), + name: `customFieldsData.${customField._id}`, + label: customField.text, + }); + } + } + + return fields; + }, + + /** + * Default list columns config + */ + fieldsDefaultColumnsConfig(_root, { contentType }: { contentType: string }): IfieldsDefaultColmns { + if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { + return [ + { name: 'firstName', label: 'First name', order: 1 }, + { name: 'lastName', label: 'Last name', order: 1 }, + { name: 'primaryEmail', label: 'Primary email', order: 2 }, + { name: 'primaryPhone', label: 'Primary phone', order: 3 }, + ]; + } + + if (contentType === FIELD_CONTENT_TYPES.COMPANY) { + return [ + { name: 'primaryName', label: 'Primary Name', order: 1 }, + { name: 'size', label: 'Size', order: 2 }, + { name: 'links.website', label: 'Website', order: 3 }, + { name: 'industry', label: 'Industry', order: 4 }, + { name: 'plan', label: 'Plan', order: 5 }, + { name: 'lastSeenAt', label: 'Last seen at', order: 6 }, + { name: 'sessionCount', label: 'Session count', order: 7 }, + ]; + } + + return []; + }, +}; + +requireLogin(fieldQueries, 'fieldsCombinedByContentType'); +requireLogin(fieldQueries, 'fieldsDefaultColumnsConfig'); + +checkPermission(fieldQueries, 'fields', 'showFields', []); + +const fieldsGroupQueries = { + /** + * Fields group list + */ + fieldsGroups(_root, { contentType }: { contentType: string }) { + const query: any = {}; + + // querying by content type + query.contentType = contentType || FIELDS_GROUPS_CONTENT_TYPES.CUSTOMER; + + return FieldsGroups.find(query).sort({ order: 1 }); + }, +}; + +checkPermission(fieldsGroupQueries, 'fieldsGroups', 'showFieldsGroups', []); + +export { fieldQueries, fieldsGroupQueries }; diff --git a/src/data/resolvers/queries/forms.ts b/src/data/resolvers/queries/forms.ts index 434bbd477..06c82b554 100644 --- a/src/data/resolvers/queries/forms.ts +++ b/src/data/resolvers/queries/forms.ts @@ -1,23 +1,24 @@ -import { Forms } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; - -const formQueries = { - /** - * Forms list - */ - forms() { - return Forms.find({}).sort({ title: 1 }); - }, - - /** - * Get one form - */ - formDetail(_root, { _id }: { _id: string }) { - return Forms.findOne({ _id }); - }, -}; - -requireLogin(formQueries, 'formDetail'); -checkPermission(formQueries, 'forms', 'showForms', []); - -export default formQueries; +import { Forms } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +const formQueries = { + /** + * Forms list + */ + forms(_root, _args, { commonQuerySelector }: IContext) { + return Forms.find(commonQuerySelector).sort({ title: 1 }); + }, + + /** + * Get one form + */ + formDetail(_root, { _id }: { _id: string }) { + return Forms.findOne({ _id }); + }, +}; + +requireLogin(formQueries, 'formDetail'); +checkPermission(formQueries, 'forms', 'showForms', []); + +export default formQueries; diff --git a/src/data/resolvers/queries/importHistory.ts b/src/data/resolvers/queries/importHistory.ts index b5b526338..63f811f96 100644 --- a/src/data/resolvers/queries/importHistory.ts +++ b/src/data/resolvers/queries/importHistory.ts @@ -1,31 +1,31 @@ -import { ImportHistory } from '../../../db/models'; -import { checkPermission } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const importHistoryQueries = { - /** - * Import history list - */ - importHistories(_root, { type, ...args }: { page: number; perPage: number; type: string }) { - const list = paginate(ImportHistory.find({ contentType: type }), args).sort({ date: -1 }); - const count = ImportHistory.find({ contentType: type }).countDocuments(); - - return { list, count }; - }, - - async importHistoryDetail(_root, { _id }: { _id: string }) { - const importHistory = await ImportHistory.findOne({ _id }); - - if (!importHistory) { - throw new Error('Import history not found'); - } - - importHistory.errorMsgs = (importHistory.errorMsgs || []).slice(0, 100); - - return importHistory; - }, -}; - -checkPermission(importHistoryQueries, 'importHistories', 'importHistories', []); - -export default importHistoryQueries; +import { ImportHistory } from '../../../db/models'; +import { checkPermission } from '../../permissions/wrappers'; +import { paginate } from '../../utils'; + +const importHistoryQueries = { + /** + * Import history list + */ + importHistories(_root, { type, ...args }: { page: number; perPage: number; type: string }) { + const list = paginate(ImportHistory.find({ contentType: type }), args).sort({ date: -1 }); + const count = ImportHistory.find({ contentType: type }).countDocuments(); + + return { list, count }; + }, + + async importHistoryDetail(_root, { _id }: { _id: string }) { + const importHistory = await ImportHistory.findOne({ _id }); + + if (!importHistory) { + throw new Error('Import history not found'); + } + + importHistory.errorMsgs = (importHistory.errorMsgs || []).slice(0, 100); + + return importHistory; + }, +}; + +checkPermission(importHistoryQueries, 'importHistories', 'importHistories', []); + +export default importHistoryQueries; diff --git a/src/data/resolvers/queries/insights.ts b/src/data/resolvers/queries/insights.ts index c1b531c4f..bed853346 100644 --- a/src/data/resolvers/queries/insights.ts +++ b/src/data/resolvers/queries/insights.ts @@ -1,702 +1,702 @@ -import { ConversationMessages, Conversations, Integrations, Tags } from '../../../db/models'; -import { TAG_TYPES } from '../../../db/models/definitions/constants'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { INTEGRATION_KIND_CHOICES } from '../../constants'; -import { getDateFieldAsStr, getDurationField } from '../../modules/insights/aggregationUtils'; -import { IListArgs, IPieChartData } from '../../modules/insights/types'; -import { - fixChartData, - fixDates, - generateChartDataByCollection, - generateChartDataBySelector, - generatePunchData, - generateResponseData, - getConversationReportLookup, - getConversationSelector, - getConversationSelectorByMsg, - getConversationSelectorToMsg, - getFilterSelector, - getMessageSelector, - getSummaryData, - getSummaryDates, - getTimezone, - noConversationSelector, -} from '../../modules/insights/utils'; -import { moduleCheckPermission, moduleRequireLogin } from '../../permissions/wrappers'; - -const insightQueries = { - /** - * Builds insights charting data contains - * count of conversations in various integrations kinds. - */ - async insightsIntegrations(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const conversationSelector = await getConversationSelector(filterSelector); - - const integrations: IPieChartData[] = []; - - // count conversations by each integration kind - for (const kind of INTEGRATION_KIND_CHOICES.ALL) { - const integrationIds = await Integrations.find({ - ...filterSelector.integration, - kind, - }).select('_id'); - - // find conversation counts of given integrations - const value = await Conversations.countDocuments({ - ...conversationSelector, - integrationId: { $in: integrationIds }, - }); - - if (value > 0) { - integrations.push({ id: kind, label: kind, value }); - } - } - - return integrations; - }, - - /** - * Builds insights charting data contains - * count of conversations in various integrations tags. - */ - async insightsTags(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const conversationSelector = { - createdAt: filterSelector.createdAt, - ...noConversationSelector, - }; - - const tagDatas: IPieChartData[] = []; - - const tags = await Tags.find({ type: TAG_TYPES.CONVERSATION }).select('name'); - - const integrationIdsByTag = await Integrations.find(filterSelector.integration).select('_id'); - - const rawIntegrationIdsByTag = integrationIdsByTag.map(row => row._id); - - const tagData = await Conversations.aggregate([ - { - $match: { - ...conversationSelector, - integrationId: { $in: rawIntegrationIdsByTag }, - }, - }, - { - $unwind: '$tagIds', - }, - { - $group: { - _id: '$tagIds', - count: { $sum: 1 }, - }, - }, - ]); - - const tagDictionaryData = {}; - - tagData.forEach(row => { - tagDictionaryData[row._id] = row.count; - }); - - // count conversations by each tag - for (const tag of tags) { - // find conversation counts of given tag - const value = tagDictionaryData[tag._id]; - if (tag._id in tagDictionaryData) { - tagDatas.push({ id: tag.name, label: tag.name, value }); - } - } - - return tagDatas; - }, - - /** - * Counts conversations by each hours in each days. - */ - async insightsPunchCard(_root, args: IListArgs, { user }: { user: IUserDocument }) { - const messageSelector = await getMessageSelector({ args }); - - return generatePunchData(ConversationMessages, messageSelector, user); - }, - - /** - * Sends combined charting data for trends. - */ - async insightsTrend(_root, args: IListArgs) { - const messageSelector = await getMessageSelector({ args }); - - return generateChartDataBySelector({ selector: messageSelector }); - }, - - /** - * Sends summary datas. - */ - async insightsSummaryData(_root, args: IListArgs) { - const selector = await getMessageSelector({ - args, - createdAt: getSummaryDates(args.endDate), - }); - - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - return getSummaryData({ - start, - end, - collection: ConversationMessages, - selector, - }); - }, - - /** - * Sends combined charting data for trends and summaries. - */ - async insightsConversation(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const selector = await getConversationSelector(filterSelector); - - const conversations = await Conversations.find(selector); - - const insightData: any = { - summary: [], - trend: await generateChartDataByCollection(conversations), - }; - - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - insightData.summary = await getSummaryData({ - start, - end, - collection: Conversations, - selector, - }); - - return insightData; - }, - - /** - * Calculates average first response time for each team members. - */ - async insightsFirstResponse(_root, args: IListArgs) { - const { startDate, endDate } = args; - const filterSelector = getFilterSelector(args); - const { start, end } = fixDates(startDate, endDate); - - const conversationSelector = { - firstRespondedUserId: { $exists: true }, - firstRespondedDate: { $exists: true }, - messageCount: { $gt: 1 }, - createdAt: { $gte: start, $lte: end }, - }; - - const insightData = { teamMembers: [], trend: [] }; - - // Variable that holds all responded conversation messages - const firstResponseData: any = []; - - // Variables holds every user's response time. - const responseUserData: any = {}; - - let allResponseTime = 0; - - const selector = await getConversationSelector(filterSelector, conversationSelector); - - const conversations = await Conversations.find(selector); - - if (conversations.length < 1) { - return insightData; - } - - const summaries = [0, 0, 0, 0]; - - // Processes total first response time for each users. - for (const conversation of conversations) { - const { firstRespondedUserId, firstRespondedDate, createdAt } = conversation; - - let responseTime = 0; - - // checking wheter or not this is actual conversation - if (firstRespondedDate && firstRespondedUserId) { - responseTime = createdAt.getTime() - firstRespondedDate.getTime(); - responseTime = Math.abs(responseTime / 1000); - - const userId = firstRespondedUserId; - - // collecting each user's respond information - firstResponseData.push({ - createdAt: firstRespondedDate, - userId, - responseTime, - }); - - allResponseTime += responseTime; - - // Builds every users's response time and conversation message count. - if (responseUserData[userId]) { - responseUserData[userId].responseTime = responseTime + responseUserData[userId].responseTime; - responseUserData[userId].count = responseUserData[userId].count + 1; - } else { - responseUserData[userId] = { - responseTime, - count: 1, - summaries: [0, 0, 0, 0], - }; - } - - const minute = Math.floor(responseTime / 60); - const index = minute < 3 ? minute : 3; - - summaries[index] = summaries[index] + 1; - responseUserData[userId].summaries[index] = responseUserData[userId].summaries[index] + 1; - } - } - - const doc = await generateResponseData(firstResponseData, responseUserData, allResponseTime); - - return { ...doc, summaries }; - }, - - /** - * Calculates average response close time for each team members. - */ - async insightsResponseClose(_root, args: IListArgs, { user }: { user: IUserDocument }) { - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - const conversationSelector = { - createdAt: { $gte: start, $lte: end }, - closedAt: { $exists: true }, - closedUserId: { $exists: true }, - }; - - const conversationMatch = await getConversationSelector(getFilterSelector(args), { ...conversationSelector }); - - const insightAggregateData = await Conversations.aggregate([ - { - $match: conversationMatch, - }, - { - $project: { - responseTime: getDurationField({ startField: '$closedAt', endField: '$createdAt' }), - date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), - closedUserId: 1, - }, - }, - { - $group: { - _id: { - closedUserId: '$closedUserId', - date: '$date', - }, - totalResponseTime: { $sum: '$responseTime' }, - avgResponseTime: { $avg: '$responseTime' }, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - closedUserId: '$_id.closedUserId', - date: '$_id.date', - totalResponseTime: 1, - avgResponseTime: 1, - count: 1, - }, - }, - { - $lookup: { - from: 'users', - localField: 'closedUserId', - foreignField: '_id', - as: 'userDoc', - }, - }, - { - $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, - }, - { - $group: { - _id: '$closedUserId', - responseTime: { $sum: '$totalResponseTime' }, - avgResponseTime: { $avg: '$avgResponseTime' }, - count: { $sum: '$count' }, - fullName: { $first: '$fullName' }, - avatar: { $first: '$avatar' }, - chartDatas: { - $push: { - date: '$date', - count: '$count', - }, - }, - }, - }, - ]); - - // Variables holds every user's response time. - const teamMembers: any = []; - const responseUserData: any = {}; - - let allResponseTime = 0; - let totalCount = 0; - const aggregatedTrend = {}; - - for (const userData of insightAggregateData) { - // responseUserData - responseUserData[userData._id] = { - responseTime: userData.responseTime, - count: userData.count, - avgResponseTime: userData.avgResponseTime, - fullName: userData.fullName, - avatar: userData.avatar, - }; - // team members gather - const fixedChartData = await fixChartData(userData.chartDatas, 'date', 'count'); - userData.chartDatas.forEach(row => { - if (row.date in aggregatedTrend) { - aggregatedTrend[row.date] += row.count; - } else { - aggregatedTrend[row.date] = row.count; - } - }); - - teamMembers.push({ - data: { - fullName: userData.fullName, - avatar: userData.avatar, - graph: fixedChartData, - }, - }); - // calculate allResponseTime to render average responseTime - allResponseTime += userData.responseTime; - totalCount += userData.count; - } - - if (insightAggregateData.length < 1) { - return { teamMembers: [], trend: [] }; - } - - const trend = await fixChartData( - Object.keys(aggregatedTrend) - .sort() - .map(key => { - return { date: key, count: aggregatedTrend[key] }; - }), - 'date', - 'count', - ); - const time = Math.floor(allResponseTime / totalCount); - - return { trend, teamMembers, time }; - }, - - /** - * Calculates average ConversationMessages frequency time for second - */ - async insightsConversationSummary(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateData = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { internal: false }, { userId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.customerId': { $exists: true }, - }, - }, - lookupHelper.diffSecondCalc, - { - $lookup: { - from: 'users', - localField: 'userId', - foreignField: '_id', - as: 'userDoc', - }, - }, - { - $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, - }, - { - $group: { - _id: { - date: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } }, - user: '$userId', - }, - userId: { $first: '$userId' }, - fullName: { $first: '$fullName' }, - avatar: { $first: '$avatar' }, - date: { $first: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } } }, - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - if (insightAggregateData.length === 0) { - return { - avg: [{ title: 'Average all operator response time', count: 0 }], - trend: [], - teamMembers: [], - }; - } - - const averageTotal = - insightAggregateData.reduce((preVal, currVal) => ({ - avgSecond: preVal.avgSecond + currVal.avgSecond, - })).avgSecond / insightAggregateData.length; - - const summaryChart = await fixChartData(insightAggregateData, 'date', 'avgSecond'); - - const userIds = await insightAggregateData.map(item => item.userId).filter((v, i, a) => a.indexOf(v) === i); - - const perUserChart: object[] = []; - - for (const userId of userIds) { - const perData = insightAggregateData.filter(item => item.userId === userId); - const perChart = await fixChartData(perData, 'date', 'avgSecond'); - - perUserChart.push({ - avatar: perData[0].avatar, - fullName: perData[0].fullName, - graph: perChart, - }); - } - - return { - avg: [{ title: 'Average all operator response time', count: averageTotal }], - trend: summaryChart, - teamMembers: perUserChart, - }; - }, - - /** - * Calculates average ConversationMessages spec CustomerAvg - */ - async insightsConversationCustomerAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateCustomer = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { internal: false }, { customerId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.userId': { $exists: true }, - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Average all customer response time', - count: insightAggregateCustomer.length ? insightAggregateCustomer[0].avgSecond : 0, - }, - ]; - }, - - /** - * Calculates average ConversationMessages spec InternalMsgsAvg - */ - async insightsConversationInternalAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - lookupHelper.lookupPrevMsg.$lookup.pipeline[1].$project.sizeMentionedIds = { $size: '$mentionedUserIds' }; - - const insightAggregateInternal = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { userId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.sizeMentionedIds': { $gt: 0 }, - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Average internal response time', - count: insightAggregateInternal.length ? insightAggregateInternal[0].avgSecond : 0, - }, - ]; - }, - - /** - * Calculates average ConversationMessages spec Overall - */ - async insightsConversationOverallAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateAllAvg = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - $or: [ - { $and: [{ userId: { $exists: true } }, { 'prevMsg.customerId': { $exists: true } }] }, - { $and: [{ customerId: { $exists: true } }, { 'prevMsg.userId': { $exists: true } }] }, - ], - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - // all duration average - const conversationMatch = await getConversationSelectorToMsg(integrationIds, brandIds); - - const insightAggregateDurationAvg = await Conversations.aggregate([ - { - $match: { - $and: [conversationMatch, messageSelector], - }, - }, - { - $lookup: { - from: 'conversation_messages', - let: { checkConversation: '$_id' }, - pipeline: [ - { - $match: { - $expr: { $eq: ['$conversationId', '$$checkConversation'] }, - }, - }, - { - $project: { createdAt: 1 }, - }, - ], - as: 'allMsgs', - }, - }, - { - $addFields: { - firstMsg: { $slice: ['$allMsgs', 1] }, - lastMsg: { $slice: ['$allMsgs', -1] }, - }, - }, - { - $project: { - _id: 1, - createdAt: 1, - firstMsg: 1, - lastMsg: 1, - }, - }, - { $unwind: '$firstMsg' }, - { $unwind: '$lastMsg' }, - { - $addFields: { - diffSec: { - $divide: [{ $subtract: ['$lastMsg.createdAt', '$firstMsg.createdAt'] }, 1000], - }, - }, - }, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Overall average', - count: insightAggregateAllAvg.length ? insightAggregateAllAvg[0].avgSecond : 0, - }, - { - title: 'Summary duration average', - count: insightAggregateDurationAvg.length ? insightAggregateDurationAvg[0].avgSecond : 0, - }, - ]; - }, -}; - -moduleRequireLogin(insightQueries); - -moduleCheckPermission(insightQueries, 'showInsights'); - -export default insightQueries; +import { ConversationMessages, Conversations, Integrations, Tags } from '../../../db/models'; +import { TAG_TYPES } from '../../../db/models/definitions/constants'; +import { INTEGRATION_KIND_CHOICES } from '../../constants'; +import { getDateFieldAsStr, getDurationField } from '../../modules/insights/aggregationUtils'; +import { IListArgs, IPieChartData } from '../../modules/insights/types'; +import { + fixChartData, + fixDates, + generateChartDataByCollection, + generateChartDataBySelector, + generatePunchData, + generateResponseData, + getConversationReportLookup, + getConversationSelector, + getConversationSelectorByMsg, + getConversationSelectorToMsg, + getFilterSelector, + getMessageSelector, + getSummaryData, + getSummaryDates, + getTimezone, + noConversationSelector, +} from '../../modules/insights/utils'; +import { moduleCheckPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +const insightQueries = { + /** + * Builds insights charting data contains + * count of conversations in various integrations kinds. + */ + async insightsIntegrations(_root, args: IListArgs) { + const filterSelector = getFilterSelector(args); + + const conversationSelector = await getConversationSelector(filterSelector); + + const integrations: IPieChartData[] = []; + + // count conversations by each integration kind + for (const kind of INTEGRATION_KIND_CHOICES.ALL) { + const integrationIds = await Integrations.find({ + ...filterSelector.integration, + kind, + }).select('_id'); + + // find conversation counts of given integrations + const value = await Conversations.countDocuments({ + ...conversationSelector, + integrationId: { $in: integrationIds }, + }); + + if (value > 0) { + integrations.push({ id: kind, label: kind, value }); + } + } + + return integrations; + }, + + /** + * Builds insights charting data contains + * count of conversations in various integrations tags. + */ + async insightsTags(_root, args: IListArgs) { + const filterSelector = getFilterSelector(args); + + const conversationSelector = { + createdAt: filterSelector.createdAt, + ...noConversationSelector, + }; + + const tagDatas: IPieChartData[] = []; + + const tags = await Tags.find({ type: TAG_TYPES.CONVERSATION }).select('name'); + + const integrationIdsByTag = await Integrations.find(filterSelector.integration).select('_id'); + + const rawIntegrationIdsByTag = integrationIdsByTag.map(row => row._id); + + const tagData = await Conversations.aggregate([ + { + $match: { + ...conversationSelector, + integrationId: { $in: rawIntegrationIdsByTag }, + }, + }, + { + $unwind: '$tagIds', + }, + { + $group: { + _id: '$tagIds', + count: { $sum: 1 }, + }, + }, + ]); + + const tagDictionaryData = {}; + + tagData.forEach(row => { + tagDictionaryData[row._id] = row.count; + }); + + // count conversations by each tag + for (const tag of tags) { + // find conversation counts of given tag + const value = tagDictionaryData[tag._id]; + if (tag._id in tagDictionaryData) { + tagDatas.push({ id: tag.name, label: tag.name, value }); + } + } + + return tagDatas; + }, + + /** + * Counts conversations by each hours in each days. + */ + async insightsPunchCard(_root, args: IListArgs, { user }: IContext) { + const messageSelector = await getMessageSelector({ args }); + + return generatePunchData(ConversationMessages, messageSelector, user); + }, + + /** + * Sends combined charting data for trends. + */ + async insightsTrend(_root, args: IListArgs) { + const messageSelector = await getMessageSelector({ args }); + + return generateChartDataBySelector({ selector: messageSelector }); + }, + + /** + * Sends summary datas. + */ + async insightsSummaryData(_root, args: IListArgs) { + const selector = await getMessageSelector({ + args, + createdAt: getSummaryDates(args.endDate), + }); + + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + return getSummaryData({ + start, + end, + collection: ConversationMessages, + selector, + }); + }, + + /** + * Sends combined charting data for trends and summaries. + */ + async insightsConversation(_root, args: IListArgs) { + const filterSelector = getFilterSelector(args); + + const selector = await getConversationSelector(filterSelector); + + const conversations = await Conversations.find(selector); + + const insightData: any = { + summary: [], + trend: await generateChartDataByCollection(conversations), + }; + + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + insightData.summary = await getSummaryData({ + start, + end, + collection: Conversations, + selector, + }); + + return insightData; + }, + + /** + * Calculates average first response time for each team members. + */ + async insightsFirstResponse(_root, args: IListArgs) { + const { startDate, endDate } = args; + const filterSelector = getFilterSelector(args); + const { start, end } = fixDates(startDate, endDate); + + const conversationSelector = { + firstRespondedUserId: { $exists: true }, + firstRespondedDate: { $exists: true }, + messageCount: { $gt: 1 }, + createdAt: { $gte: start, $lte: end }, + }; + + const insightData = { teamMembers: [], trend: [] }; + + // Variable that holds all responded conversation messages + const firstResponseData: any = []; + + // Variables holds every user's response time. + const responseUserData: any = {}; + + let allResponseTime = 0; + + const selector = await getConversationSelector(filterSelector, conversationSelector); + + const conversations = await Conversations.find(selector); + + if (conversations.length < 1) { + return insightData; + } + + const summaries = [0, 0, 0, 0]; + + // Processes total first response time for each users. + for (const conversation of conversations) { + const { firstRespondedUserId, firstRespondedDate, createdAt } = conversation; + + let responseTime = 0; + + // checking wheter or not this is actual conversation + if (firstRespondedDate && firstRespondedUserId) { + responseTime = createdAt.getTime() - firstRespondedDate.getTime(); + responseTime = Math.abs(responseTime / 1000); + + const userId = firstRespondedUserId; + + // collecting each user's respond information + firstResponseData.push({ + createdAt: firstRespondedDate, + userId, + responseTime, + }); + + allResponseTime += responseTime; + + // Builds every users's response time and conversation message count. + if (responseUserData[userId]) { + responseUserData[userId].responseTime = responseTime + responseUserData[userId].responseTime; + responseUserData[userId].count = responseUserData[userId].count + 1; + } else { + responseUserData[userId] = { + responseTime, + count: 1, + summaries: [0, 0, 0, 0], + }; + } + + const minute = Math.floor(responseTime / 60); + const index = minute < 3 ? minute : 3; + + summaries[index] = summaries[index] + 1; + responseUserData[userId].summaries[index] = responseUserData[userId].summaries[index] + 1; + } + } + + const doc = await generateResponseData(firstResponseData, responseUserData, allResponseTime); + + return { ...doc, summaries }; + }, + + /** + * Calculates average response close time for each team members. + */ + async insightsResponseClose(_root, args: IListArgs, { user }: IContext) { + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + const conversationSelector = { + createdAt: { $gte: start, $lte: end }, + closedAt: { $exists: true }, + closedUserId: { $exists: true }, + }; + + const conversationMatch = await getConversationSelector(getFilterSelector(args), { ...conversationSelector }); + + const insightAggregateData = await Conversations.aggregate([ + { + $match: conversationMatch, + }, + { + $project: { + responseTime: getDurationField({ startField: '$closedAt', endField: '$createdAt' }), + date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), + closedUserId: 1, + }, + }, + { + $group: { + _id: { + closedUserId: '$closedUserId', + date: '$date', + }, + totalResponseTime: { $sum: '$responseTime' }, + avgResponseTime: { $avg: '$responseTime' }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + closedUserId: '$_id.closedUserId', + date: '$_id.date', + totalResponseTime: 1, + avgResponseTime: 1, + count: 1, + }, + }, + { + $lookup: { + from: 'users', + localField: 'closedUserId', + foreignField: '_id', + as: 'userDoc', + }, + }, + { + $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, + }, + { + $group: { + _id: '$closedUserId', + responseTime: { $sum: '$totalResponseTime' }, + avgResponseTime: { $avg: '$avgResponseTime' }, + count: { $sum: '$count' }, + fullName: { $first: '$fullName' }, + avatar: { $first: '$avatar' }, + chartDatas: { + $push: { + date: '$date', + count: '$count', + }, + }, + }, + }, + ]); + + // Variables holds every user's response time. + const teamMembers: any = []; + const responseUserData: any = {}; + + let allResponseTime = 0; + let totalCount = 0; + const aggregatedTrend = {}; + + for (const userData of insightAggregateData) { + // responseUserData + responseUserData[userData._id] = { + responseTime: userData.responseTime, + count: userData.count, + avgResponseTime: userData.avgResponseTime, + fullName: userData.fullName, + avatar: userData.avatar, + }; + // team members gather + const fixedChartData = await fixChartData(userData.chartDatas, 'date', 'count'); + userData.chartDatas.forEach(row => { + if (row.date in aggregatedTrend) { + aggregatedTrend[row.date] += row.count; + } else { + aggregatedTrend[row.date] = row.count; + } + }); + + teamMembers.push({ + data: { + fullName: userData.fullName, + avatar: userData.avatar, + graph: fixedChartData, + }, + }); + // calculate allResponseTime to render average responseTime + allResponseTime += userData.responseTime; + totalCount += userData.count; + } + + if (insightAggregateData.length < 1) { + return { teamMembers: [], trend: [] }; + } + + const trend = await fixChartData( + Object.keys(aggregatedTrend) + .sort() + .map(key => { + return { date: key, count: aggregatedTrend[key] }; + }), + 'date', + 'count', + ); + const time = Math.floor(allResponseTime / totalCount); + + return { trend, teamMembers, time }; + }, + + /** + * Calculates average ConversationMessages frequency time for second + */ + async insightsConversationSummary(_root, args: IListArgs) { + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateData = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { internal: false }, { userId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.customerId': { $exists: true }, + }, + }, + lookupHelper.diffSecondCalc, + { + $lookup: { + from: 'users', + localField: 'userId', + foreignField: '_id', + as: 'userDoc', + }, + }, + { + $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, + }, + { + $group: { + _id: { + date: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } }, + user: '$userId', + }, + userId: { $first: '$userId' }, + fullName: { $first: '$fullName' }, + avatar: { $first: '$avatar' }, + date: { $first: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } } }, + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + if (insightAggregateData.length === 0) { + return { + avg: [{ title: 'Average all operator response time', count: 0 }], + trend: [], + teamMembers: [], + }; + } + + const averageTotal = + insightAggregateData.reduce((preVal, currVal) => ({ + avgSecond: preVal.avgSecond + currVal.avgSecond, + })).avgSecond / insightAggregateData.length; + + const summaryChart = await fixChartData(insightAggregateData, 'date', 'avgSecond'); + + const userIds = await insightAggregateData.map(item => item.userId).filter((v, i, a) => a.indexOf(v) === i); + + const perUserChart: object[] = []; + + for (const userId of userIds) { + const perData = insightAggregateData.filter(item => item.userId === userId); + const perChart = await fixChartData(perData, 'date', 'avgSecond'); + + perUserChart.push({ + avatar: perData[0].avatar, + fullName: perData[0].fullName, + graph: perChart, + }); + } + + return { + avg: [{ title: 'Average all operator response time', count: averageTotal }], + trend: summaryChart, + teamMembers: perUserChart, + }; + }, + + /** + * Calculates average ConversationMessages spec CustomerAvg + */ + async insightsConversationCustomerAvg(_root, args: IListArgs) { + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateCustomer = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { internal: false }, { customerId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.userId': { $exists: true }, + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Average all customer response time', + count: insightAggregateCustomer.length ? insightAggregateCustomer[0].avgSecond : 0, + }, + ]; + }, + + /** + * Calculates average ConversationMessages spec InternalMsgsAvg + */ + async insightsConversationInternalAvg(_root, args: IListArgs) { + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + lookupHelper.lookupPrevMsg.$lookup.pipeline[1].$project.sizeMentionedIds = { $size: '$mentionedUserIds' }; + + const insightAggregateInternal = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { userId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.sizeMentionedIds': { $gt: 0 }, + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Average internal response time', + count: insightAggregateInternal.length ? insightAggregateInternal[0].avgSecond : 0, + }, + ]; + }, + + /** + * Calculates average ConversationMessages spec Overall + */ + async insightsConversationOverallAvg(_root, args: IListArgs) { + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateAllAvg = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + $or: [ + { $and: [{ userId: { $exists: true } }, { 'prevMsg.customerId': { $exists: true } }] }, + { $and: [{ customerId: { $exists: true } }, { 'prevMsg.userId': { $exists: true } }] }, + ], + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + // all duration average + const conversationMatch = await getConversationSelectorToMsg(integrationIds, brandIds); + + const insightAggregateDurationAvg = await Conversations.aggregate([ + { + $match: { + $and: [conversationMatch, messageSelector], + }, + }, + { + $lookup: { + from: 'conversation_messages', + let: { checkConversation: '$_id' }, + pipeline: [ + { + $match: { + $expr: { $eq: ['$conversationId', '$$checkConversation'] }, + }, + }, + { + $project: { createdAt: 1 }, + }, + ], + as: 'allMsgs', + }, + }, + { + $addFields: { + firstMsg: { $slice: ['$allMsgs', 1] }, + lastMsg: { $slice: ['$allMsgs', -1] }, + }, + }, + { + $project: { + _id: 1, + createdAt: 1, + firstMsg: 1, + lastMsg: 1, + }, + }, + { $unwind: '$firstMsg' }, + { $unwind: '$lastMsg' }, + { + $addFields: { + diffSec: { + $divide: [{ $subtract: ['$lastMsg.createdAt', '$firstMsg.createdAt'] }, 1000], + }, + }, + }, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Overall average', + count: insightAggregateAllAvg.length ? insightAggregateAllAvg[0].avgSecond : 0, + }, + { + title: 'Summary duration average', + count: insightAggregateDurationAvg.length ? insightAggregateDurationAvg[0].avgSecond : 0, + }, + ]; + }, +}; + +moduleRequireLogin(insightQueries); + +moduleCheckPermission(insightQueries, 'showInsights'); + +export default insightQueries; diff --git a/src/data/resolvers/queries/internalNotes.ts b/src/data/resolvers/queries/internalNotes.ts index 9b33d15b8..4f0a0e8f1 100644 --- a/src/data/resolvers/queries/internalNotes.ts +++ b/src/data/resolvers/queries/internalNotes.ts @@ -1,17 +1,17 @@ -import { InternalNotes } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -const internalNoteQueries = { - /** - * InternalNotes list - */ - internalNotes(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { - return InternalNotes.find({ contentType, contentTypeId }).sort({ - createdDate: 1, - }); - }, -}; - -moduleRequireLogin(internalNoteQueries); - -export default internalNoteQueries; +import { InternalNotes } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; + +const internalNoteQueries = { + /** + * InternalNotes list + */ + internalNotes(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { + return InternalNotes.find({ contentType, contentTypeId }).sort({ + createdDate: 1, + }); + }, +}; + +moduleRequireLogin(internalNoteQueries); + +export default internalNoteQueries; diff --git a/src/data/resolvers/queries/knowledgeBase.ts b/src/data/resolvers/queries/knowledgeBase.ts index d5c5a24fe..473671ba8 100644 --- a/src/data/resolvers/queries/knowledgeBase.ts +++ b/src/data/resolvers/queries/knowledgeBase.ts @@ -1,160 +1,161 @@ -import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; - -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -/* Articles list & total count helper */ -const articlesQuery = async ({ categoryIds }: { categoryIds: string[] }) => { - const query: any = {}; - - // filter articles by category i - if (categoryIds) { - const categories = await KnowledgeBaseCategories.find({ - _id: { - $in: categoryIds, - }, - }); - - let articleIds: any = []; - - for (const category of categories) { - articleIds = articleIds.concat(category.articleIds || []); - } - - query._id = { - $in: articleIds, - }; - } - - return query; -}; - -/* Categories list & total count helper */ -const categoriesQuery = async ({ topicIds }: { topicIds: string[] }) => { - const query: any = {}; - - // filter categories by topic id - if (topicIds) { - let categoryIds: any = []; - - const topics = await KnowledgeBaseTopics.find({ - _id: { - $in: topicIds, - }, - }); - - for (const topic of topics) { - categoryIds = categoryIds.concat(topic.categoryIds || []); - } - - query._id = { - $in: categoryIds, - }; - } - - return query; -}; - -const knowledgeBaseQueries = { - /** - * Article list - */ - async knowledgeBaseArticles(_root, args: { page: number; perPage: number; categoryIds: string[] }) { - const query = await articlesQuery(args); - const articles = KnowledgeBaseArticles.find(query).sort({ - createdData: -1, - }); - - return paginate(articles, args); - }, - - /** - * Article detail - */ - knowledgeBaseArticleDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseArticles.findOne({ _id }); - }, - - /** - * Total article count - */ - async knowledgeBaseArticlesTotalCount(_root, args: { categoryIds: string[] }) { - const query = await articlesQuery(args); - - return KnowledgeBaseArticles.find(query).countDocuments(); - }, - - /** - * Category list - */ - async knowledgeBaseCategories(_root, args: { page: number; perPage: number; topicIds: string[] }) { - const query = await categoriesQuery(args); - - const categories = KnowledgeBaseCategories.find(query).sort({ - modifiedDate: -1, - }); - - return paginate(categories, args); - }, - - /** - * Category detail - */ - knowledgeBaseCategoryDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseCategories.findOne({ _id }).then(category => { - return category; - }); - }, - - /** - * Category total count - */ - async knowledgeBaseCategoriesTotalCount(_root, args: { topicIds: string[] }) { - const query = await categoriesQuery(args); - - return KnowledgeBaseCategories.find(query).countDocuments(); - }, - - /** - * Get last category - */ - knowledgeBaseCategoriesGetLast() { - return KnowledgeBaseCategories.findOne({}).sort({ createdDate: -1 }); - }, - - /** - * Topic list - */ - knowledgeBaseTopics(_root, args: { page: number; perPage: number }) { - const topics = paginate(KnowledgeBaseTopics.find({}), args); - return topics.sort({ modifiedDate: -1 }); - }, - - /** - * Topic detail - */ - knowledgeBaseTopicDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseTopics.findOne({ _id }); - }, - - /** - * Total topic count - */ - knowledgeBaseTopicsTotalCount() { - return KnowledgeBaseTopics.find({}).countDocuments(); - }, -}; - -requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticleDetail'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticlesTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicsTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicDetail'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesGetLast'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoryDetail'); - -checkPermission(knowledgeBaseQueries, 'knowledgeBaseArticles', 'showKnowledgeBase', []); -checkPermission(knowledgeBaseQueries, 'knowledgeBaseTopics', 'showKnowledgeBase', []); -checkPermission(knowledgeBaseQueries, 'knowledgeBaseCategories', 'showKnowledgeBase', []); - -export default knowledgeBaseQueries; +import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; + +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +/* Articles list & total count helper */ +const articlesQuery = async ({ categoryIds }: { categoryIds: string[] }) => { + const query: any = {}; + + // filter articles by category i + if (categoryIds) { + const categories = await KnowledgeBaseCategories.find({ + _id: { + $in: categoryIds, + }, + }); + + let articleIds: any = []; + + for (const category of categories) { + articleIds = articleIds.concat(category.articleIds || []); + } + + query._id = { + $in: articleIds, + }; + } + + return query; +}; + +/* Categories list & total count helper */ +const categoriesQuery = async ({ topicIds }: { topicIds: string[] }) => { + const query: any = {}; + + // filter categories by topic id + if (topicIds) { + let categoryIds: any = []; + + const topics = await KnowledgeBaseTopics.find({ + _id: { + $in: topicIds, + }, + }); + + for (const topic of topics) { + categoryIds = categoryIds.concat(topic.categoryIds || []); + } + + query._id = { + $in: categoryIds, + }; + } + + return query; +}; + +const knowledgeBaseQueries = { + /** + * Article list + */ + async knowledgeBaseArticles(_root, args: { page: number; perPage: number; categoryIds: string[] }) { + const query = await articlesQuery(args); + const articles = KnowledgeBaseArticles.find(query).sort({ + createdData: -1, + }); + + return paginate(articles, args); + }, + + /** + * Article detail + */ + knowledgeBaseArticleDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseArticles.findOne({ _id }); + }, + + /** + * Total article count + */ + async knowledgeBaseArticlesTotalCount(_root, args: { categoryIds: string[] }) { + const query = await articlesQuery(args); + + return KnowledgeBaseArticles.find(query).countDocuments(); + }, + + /** + * Category list + */ + async knowledgeBaseCategories(_root, args: { page: number; perPage: number; topicIds: string[] }) { + const query = await categoriesQuery(args); + + const categories = KnowledgeBaseCategories.find(query).sort({ + modifiedDate: -1, + }); + + return paginate(categories, args); + }, + + /** + * Category detail + */ + knowledgeBaseCategoryDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseCategories.findOne({ _id }).then(category => { + return category; + }); + }, + + /** + * Category total count + */ + async knowledgeBaseCategoriesTotalCount(_root, args: { topicIds: string[] }) { + const query = await categoriesQuery(args); + + return KnowledgeBaseCategories.find(query).countDocuments(); + }, + + /** + * Get last category + */ + knowledgeBaseCategoriesGetLast(_root, _args, { commonQuerySelector }: IContext) { + return KnowledgeBaseCategories.findOne(commonQuerySelector).sort({ createdDate: -1 }); + }, + + /** + * Topic list + */ + knowledgeBaseTopics(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + const topics = paginate(KnowledgeBaseTopics.find(commonQuerySelector), args); + return topics.sort({ modifiedDate: -1 }); + }, + + /** + * Topic detail + */ + knowledgeBaseTopicDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseTopics.findOne({ _id }); + }, + + /** + * Total topic count + */ + knowledgeBaseTopicsTotalCount(_root, _args, { commonQuerySelector }: IContext) { + return KnowledgeBaseTopics.find(commonQuerySelector).countDocuments(); + }, +}; + +requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticleDetail'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticlesTotalCount'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicsTotalCount'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicDetail'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesGetLast'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesTotalCount'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoryDetail'); + +checkPermission(knowledgeBaseQueries, 'knowledgeBaseArticles', 'showKnowledgeBase', []); +checkPermission(knowledgeBaseQueries, 'knowledgeBaseTopics', 'showKnowledgeBase', []); +checkPermission(knowledgeBaseQueries, 'knowledgeBaseCategories', 'showKnowledgeBase', []); + +export default knowledgeBaseQueries; diff --git a/src/data/resolvers/queries/messengerApps.ts b/src/data/resolvers/queries/messengerApps.ts index 979690bd9..fccdf06f9 100644 --- a/src/data/resolvers/queries/messengerApps.ts +++ b/src/data/resolvers/queries/messengerApps.ts @@ -1,12 +1,13 @@ import { MessengerApps } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const messengerAppQueries = { /* * MessengerApps list */ - messengerApps(_root, { kind }: { kind: string }) { - const query: any = {}; + messengerApps(_root, { kind }: { kind: string }, { commonQuerySelector }: IContext) { + const query: any = commonQuerySelector; if (kind) { query.kind = kind; @@ -18,8 +19,8 @@ const messengerAppQueries = { /* * MessengerApps count */ - messengerAppsCount(_root, { kind }: { kind: string }) { - const query: any = {}; + messengerAppsCount(_root, { kind }: { kind: string }, { commonQuerySelector }: IContext) { + const query: any = commonQuerySelector; if (kind) { query.kind = kind; diff --git a/src/data/resolvers/queries/notifications.ts b/src/data/resolvers/queries/notifications.ts index f9c2b4096..6667572f0 100644 --- a/src/data/resolvers/queries/notifications.ts +++ b/src/data/resolvers/queries/notifications.ts @@ -1,7 +1,7 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { NOTIFICATION_MODULES } from '../../constants'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const notificationQueries = { @@ -22,7 +22,7 @@ const notificationQueries = { page: number; perPage: number; }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const sort = { date: -1 }; const selector: any = { receiver: user._id }; @@ -47,7 +47,7 @@ const notificationQueries = { /** * Notification counts */ - notificationCounts(_root, { requireRead }: { requireRead: boolean }, { user }: { user: IUserDocument }) { + notificationCounts(_root, { requireRead }: { requireRead: boolean }, { user }: IContext) { const selector: any = { receiver: user._id }; if (requireRead) { @@ -67,7 +67,7 @@ const notificationQueries = { /** * Get per user configuration */ - notificationsGetConfigurations(_root, _args, { user }: { user: IUserDocument }) { + notificationsGetConfigurations(_root, _args, { user }: IContext) { return NotificationConfigurations.find({ user: user._id }); }, }; diff --git a/src/data/resolvers/queries/products.ts b/src/data/resolvers/queries/products.ts index 029d3d7fa..01b2439b7 100644 --- a/src/data/resolvers/queries/products.ts +++ b/src/data/resolvers/queries/products.ts @@ -1,5 +1,6 @@ import { Products } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const productQueries = { @@ -14,8 +15,9 @@ const productQueries = { ids, ...pagintationArgs }: { ids: string[]; type: string; searchValue: string; page: number; perPage: number }, + { commonQuerySelector }: IContext, ) { - const filter: any = {}; + const filter: any = commonQuerySelector; if (type) { filter.type = type; @@ -36,8 +38,8 @@ const productQueries = { /** * Get all products count. We will use it in pager */ - productsTotalCount(_root, { type }: { type: string }) { - const filter: any = {}; + productsTotalCount(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + const filter: any = commonQuerySelector; if (type) { filter.type = type; diff --git a/src/data/resolvers/queries/responseTemplates.ts b/src/data/resolvers/queries/responseTemplates.ts index ebf92f18d..40698f42e 100644 --- a/src/data/resolvers/queries/responseTemplates.ts +++ b/src/data/resolvers/queries/responseTemplates.ts @@ -1,5 +1,6 @@ import { ResponseTemplates } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; interface IListParams { @@ -9,10 +10,10 @@ interface IListParams { searchValue: string; } -const generateFilter = (args: IListParams) => { +const generateFilter = (commonSelector, args: IListParams) => { const { brandId, searchValue } = args; - const filter: any = {}; + const filter: any = commonSelector; if (brandId) { filter.brandId = brandId; @@ -32,8 +33,8 @@ const responseTemplateQueries = { /** * Response templates list */ - responseTemplates(_root, args: IListParams) { - const filter = generateFilter(args); + responseTemplates(_root, args: IListParams, { commonQuerySelector }: IContext) { + const filter = generateFilter(commonQuerySelector, args); return paginate(ResponseTemplates.find(filter), args); }, @@ -41,8 +42,8 @@ const responseTemplateQueries = { /** * Get all response templates count. We will use it in pager */ - responseTemplatesTotalCount(_root, args: IListParams) { - const filter = generateFilter(args); + responseTemplatesTotalCount(_root, args: IListParams, { commonQuerySelector }: IContext) { + const filter = generateFilter(commonQuerySelector, args); return ResponseTemplates.find(filter).countDocuments(); }, diff --git a/src/data/resolvers/queries/scripts.ts b/src/data/resolvers/queries/scripts.ts index 289dff864..dfefae368 100644 --- a/src/data/resolvers/queries/scripts.ts +++ b/src/data/resolvers/queries/scripts.ts @@ -1,20 +1,21 @@ import { Scripts } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const scriptQueries = { /** * Scripts list */ - scripts(_root, args: { page: number; perPage: number }) { - return paginate(Scripts.find({}), args); + scripts(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + return paginate(Scripts.find(commonQuerySelector), args); }, /** * Get all scripts count. We will use it in pager */ - scriptsTotalCount() { - return Scripts.find({}).countDocuments(); + scriptsTotalCount(_root, _args, { commonQuerySelector }: IContext) { + return Scripts.find(commonQuerySelector).countDocuments(); }, }; diff --git a/src/data/resolvers/queries/segments.ts b/src/data/resolvers/queries/segments.ts index 2237756c6..199668960 100644 --- a/src/data/resolvers/queries/segments.ts +++ b/src/data/resolvers/queries/segments.ts @@ -1,19 +1,20 @@ import { Segments } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const segmentQueries = { /** * Segments list */ - segments(_root, { contentType }: { contentType: string }) { - return Segments.find({ contentType }).sort({ name: 1 }); + segments(_root, { contentType }: { contentType: string }, { commonQuerySelector }: IContext) { + return Segments.find({ ...commonQuerySelector, contentType }).sort({ name: 1 }); }, /** * Only segment that has no sub segments */ - async segmentsGetHeads() { - return Segments.find({ $or: [{ subOf: { $exists: false } }, { subOf: '' }] }); + async segmentsGetHeads(_root, _args, { commonQuerySelector }: IContext) { + return Segments.find({ ...commonQuerySelector, $or: [{ subOf: { $exists: false } }, { subOf: '' }] }); }, /** diff --git a/src/data/resolvers/queries/tags.ts b/src/data/resolvers/queries/tags.ts index 1bb263fde..1bc366f0e 100644 --- a/src/data/resolvers/queries/tags.ts +++ b/src/data/resolvers/queries/tags.ts @@ -1,12 +1,13 @@ import { Tags } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const tagQueries = { /** * Tags list */ - tags(_root, { type }: { type: string }) { - return Tags.find({ type }).sort({ name: 1 }); + tags(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Tags.find({ ...commonQuerySelector, type }).sort({ name: 1 }); }, /** diff --git a/src/data/resolvers/queries/tasks.ts b/src/data/resolvers/queries/tasks.ts index 0d1e46703..5aee9a4dc 100644 --- a/src/data/resolvers/queries/tasks.ts +++ b/src/data/resolvers/queries/tasks.ts @@ -1,32 +1,32 @@ -import { Tasks } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateTaskCommonFilters } from './boardUtils'; - -const taskQueries = { - /** - * Tasks list - */ - async tasks(_root, args: IListParams) { - const filter = await generateTaskCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Tasks.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Tasks detail - */ - taskDetail(_root, { _id }: { _id: string }) { - return Tasks.findOne({ _id }); - }, -}; - -moduleRequireLogin(taskQueries); - -checkPermission(taskQueries, 'tasks', 'showTasks', []); - -export default taskQueries; +import { Tasks } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IListParams } from './boards'; +import { generateTaskCommonFilters } from './boardUtils'; + +const taskQueries = { + /** + * Tasks list + */ + async tasks(_root, args: IListParams) { + const filter = await generateTaskCommonFilters(args); + const sort = { order: 1, createdAt: -1 }; + + return Tasks.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Tasks detail + */ + taskDetail(_root, { _id }: { _id: string }) { + return Tasks.findOne({ _id }); + }, +}; + +moduleRequireLogin(taskQueries); + +checkPermission(taskQueries, 'tasks', 'showTasks', []); + +export default taskQueries; diff --git a/src/data/resolvers/queries/tickets.ts b/src/data/resolvers/queries/tickets.ts index 516585ed6..3f2aac717 100644 --- a/src/data/resolvers/queries/tickets.ts +++ b/src/data/resolvers/queries/tickets.ts @@ -1,32 +1,32 @@ -import { Tickets } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateTicketCommonFilters } from './boardUtils'; - -const ticketQueries = { - /** - * Tickets list - */ - async tickets(_root, args: IListParams) { - const filter = await generateTicketCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Tickets.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Tickets detail - */ - ticketDetail(_root, { _id }: { _id: string }) { - return Tickets.findOne({ _id }); - }, -}; - -moduleRequireLogin(ticketQueries); - -checkPermission(ticketQueries, 'tickets', 'showTickets', []); - -export default ticketQueries; +import { Tickets } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IListParams } from './boards'; +import { generateTicketCommonFilters } from './boardUtils'; + +const ticketQueries = { + /** + * Tickets list + */ + async tickets(_root, args: IListParams) { + const filter = await generateTicketCommonFilters(args); + const sort = { order: 1, createdAt: -1 }; + + return Tickets.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Tickets detail + */ + ticketDetail(_root, { _id }: { _id: string }) { + return Tickets.findOne({ _id }); + }, +}; + +moduleRequireLogin(ticketQueries); + +checkPermission(ticketQueries, 'tickets', 'showTickets', []); + +export default ticketQueries; diff --git a/src/data/resolvers/queries/users.ts b/src/data/resolvers/queries/users.ts index 4cc9a9dba..16985d743 100644 --- a/src/data/resolvers/queries/users.ts +++ b/src/data/resolvers/queries/users.ts @@ -1,6 +1,6 @@ import { Conversations, Users } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; interface IListArgs { @@ -82,7 +82,7 @@ const userQueries = { /** * Current user */ - currentUser(_root, _args, { user }: { user: IUserDocument }) { + currentUser(_root, _args, { user }: IContext) { if (user) { return Users.findOne({ _id: user._id, isActive: { $ne: false } }); } diff --git a/src/data/resolvers/tasks.ts b/src/data/resolvers/tasks.ts index dbe778202..15810c4fc 100644 --- a/src/data/resolvers/tasks.ts +++ b/src/data/resolvers/tasks.ts @@ -1,6 +1,6 @@ import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; import { ITaskDocument } from '../../db/models/definitions/tasks'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { @@ -34,7 +34,7 @@ export default { return Stages.findOne({ _id: task.stageId }); }, - isWatched(task: ITaskDocument, _args, { user }: { user: IUserDocument }) { + isWatched(task: ITaskDocument, _args, { user }: IContext) { const watchedUserIds = task.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { diff --git a/src/data/resolvers/tickets.ts b/src/data/resolvers/tickets.ts index 4114eb662..bf7263d7b 100644 --- a/src/data/resolvers/tickets.ts +++ b/src/data/resolvers/tickets.ts @@ -1,6 +1,6 @@ import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; import { ITicketDocument } from '../../db/models/definitions/tickets'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { @@ -34,7 +34,7 @@ export default { return Stages.findOne({ _id: ticket.stageId }); }, - isWatched(ticket: ITicketDocument, _args, { user }: { user: IUserDocument }) { + isWatched(ticket: ITicketDocument, _args, { user }: IContext) { const watchedUserIds = ticket.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { diff --git a/src/data/schema/config.ts b/src/data/schema/config.ts index ba241cf1a..8b9c50189 100644 --- a/src/data/schema/config.ts +++ b/src/data/schema/config.ts @@ -24,12 +24,17 @@ export const types = ` secretAccessKey: String region: String } + + type ENV { + USE_BRAND_RESTRICTIONS: String + } `; export const queries = ` configsDetail(code: String!): Config configsVersions: ProjectInfos engagesConfigDetail: EngagesConfig + configsGetEnv: ENV `; export const mutations = ` diff --git a/src/data/schema/user.ts b/src/data/schema/user.ts index 8f6a0447d..93200a3df 100644 --- a/src/data/schema/user.ts +++ b/src/data/schema/user.ts @@ -57,6 +57,7 @@ export const types = ` emailSignatures: JSON getNotificationByEmail: Boolean groupIds: [String] + brandIds: [String] isOwner: Boolean permissionActions: JSON @@ -75,6 +76,7 @@ const commonParams = ` links: UserLinks, channelIds: [String], groupIds: [String] + brandIds: [String] `; const commonSelector = ` diff --git a/src/data/types.ts b/src/data/types.ts new file mode 100644 index 000000000..9a9305073 --- /dev/null +++ b/src/data/types.ts @@ -0,0 +1,10 @@ +import * as express from 'express'; +import { IUserDocument } from '../db/models/definitions/users'; + +export interface IContext { + res: express.Response; + user: IUserDocument; + docModifier: (doc: T) => any; + brandIdSelector: {}; + commonQuerySelector: {}; +} diff --git a/src/db/connection.ts b/src/db/connection.ts index 53defc1d7..365c826c4 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -57,7 +57,15 @@ export const graphqlRequest = async (source: string = '', name: string = '', arg }, }; - const response: any = await graphql(schema, source, rootValue, context || { user, res }, args); + const finalContext = context || { user, res }; + + finalContext.docModifier = doc => { + return doc; + }; + + finalContext.commonQuerySelector = {}; + + const response: any = await graphql(schema, source, rootValue, finalContext, args); if (response.errors || !response.data) { throw response.errors; diff --git a/src/db/models/Companies.ts b/src/db/models/Companies.ts index ec79978d2..35929854a 100644 --- a/src/db/models/Companies.ts +++ b/src/db/models/Companies.ts @@ -159,6 +159,7 @@ export const loadClass = () => { // Checking duplicated fields of company await this.checkDuplication(companyFields, companyIds); + let scopeBrandIds: string[] = []; let tagIds: string[] = []; let names: string[] = []; let emails: string[] = []; @@ -174,6 +175,9 @@ export const loadClass = () => { const companyEmails = companyObj.emails || []; const companyPhones = companyObj.phones || []; + // Merging scopeBrandIds + scopeBrandIds = [...scopeBrandIds, ...(companyObj.scopeBrandIds || [])]; + // Merging company's tag into 1 array tagIds = tagIds.concat(companyTags); @@ -192,21 +196,16 @@ export const loadClass = () => { } } - // Removing Duplicated Tags from company + // Removing Duplicates tagIds = Array.from(new Set(tagIds)); - - // Removing Duplicated names from company names = Array.from(new Set(names)); - - // Removing Duplicated names from company emails = Array.from(new Set(emails)); - - // Removing Duplicated names from company phones = Array.from(new Set(phones)); // Creating company with properties const company = await Companies.createCompany({ ...companyFields, + scopeBrandIds, tagIds, mergedIds: companyIds, names, diff --git a/src/db/models/Customers.ts b/src/db/models/Customers.ts index 92561730c..a358f3f7e 100644 --- a/src/db/models/Customers.ts +++ b/src/db/models/Customers.ts @@ -255,6 +255,7 @@ export const loadClass = () => { // Checking duplicated fields of customer await Customers.checkDuplication(customerFields, customerIds); + let scopeBrandIds: string[] = []; let tagIds: string[] = []; let companyIds: string[] = []; @@ -277,6 +278,9 @@ export const loadClass = () => { // get last customer's integrationId customerFields.integrationId = customerObj.integrationId; + // Merging scopeBrandIds + scopeBrandIds = [...scopeBrandIds, ...(customerObj.scopeBrandIds || [])]; + const customerTags: string[] = customerObj.tagIds || []; const customerCompanies: string[] = customerObj.companyIds || []; @@ -292,21 +296,17 @@ export const loadClass = () => { } } - // Removing Duplicated Tags from customer + // Removing Duplicates + scopeBrandIds = Array.from(new Set(scopeBrandIds)); tagIds = Array.from(new Set(tagIds)); - - // Removing Duplicated Companies from customer companyIds = Array.from(new Set(companyIds)); - - // Removing Duplicated Emails from customer emails = Array.from(new Set(emails)); - - // Removing Duplicated Phones from customer phones = Array.from(new Set(phones)); // Creating customer with properties const customer = await this.createCustomer({ ...customerFields, + scopeBrandIds, tagIds, companyIds, mergedIds: customerIds, diff --git a/src/db/models/Permissions.ts b/src/db/models/Permissions.ts index bdc1bedf1..ab1e453e2 100644 --- a/src/db/models/Permissions.ts +++ b/src/db/models/Permissions.ts @@ -17,7 +17,6 @@ export interface IPermissionModel extends Model { } export interface IUserGroupModel extends Model { - generateDocs(groupId: string, memberIds?: string[]): Array<{ userId: string; groupId: string }>; createGroup(doc: IUserGroup, memberIds?: string[]): Promise; updateGroup(_id: string, doc: IUserGroup, memberIds?: string[]): Promise; removeGroup(_id: string): Promise; @@ -152,6 +151,8 @@ export const userGroupLoadClass = () => { throw new Error(`Group not found with id ${_id}`); } + await Users.updateMany({ groupIds: { $in: [_id] } }, { $pull: { groupIds: { $in: [_id] } } }); + return groupObj.remove(); } } diff --git a/src/db/models/Users.ts b/src/db/models/Users.ts index cf27bc307..0e3b0089b 100644 --- a/src/db/models/Users.ts +++ b/src/db/models/Users.ts @@ -18,6 +18,7 @@ interface IEditProfile { interface IUpdateUser extends IEditProfile { password?: string; groupIds?: string[]; + brandIds?: string[]; } export interface IUserModel extends Model { @@ -152,8 +153,11 @@ export const loadClass = () => { /** * Update user information */ - public static async updateUser(_id: string, { username, email, password, details, links, groupIds }: IUpdateUser) { - const doc = { username, email, password, details, links, groupIds }; + public static async updateUser( + _id: string, + { username, email, password, details, links, groupIds, brandIds }: IUpdateUser, + ) { + const doc = { username, email, password, details, links, groupIds, brandIds }; // Checking duplicated email await this.checkDuplication({ email, idsToExclude: _id }); @@ -484,6 +488,7 @@ export const loadClass = () => { details: _user.details, isOwner: _user.isOwner, groupIds: _user.groupIds, + brandIds: _user.brandIds, username: _user.username, }; diff --git a/src/db/models/definitions/activityLogs.ts b/src/db/models/definitions/activityLogs.ts index 28e513ae2..bd32a50ba 100644 --- a/src/db/models/definitions/activityLogs.ts +++ b/src/db/models/definitions/activityLogs.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, ACTIVITY_PERFORMER_TYPES, ACTIVITY_TYPES } from './constants'; +import { field } from './utils'; export interface IActionPerformer { type: string; diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index a5d23fb20..49258821a 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { BOARD_TYPES, PIPELINE_VISIBLITIES, PROBABILITY } from './constants'; +import { field, schemaWrapper } from './utils'; interface ICommonFields { userId?: string; @@ -115,15 +115,17 @@ export const commonItemFieldsSchema = { modifiedBy: field({ type: String }), }; -export const boardSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - isDefault: field({ - type: Boolean, - default: false, +export const boardSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String }), + isDefault: field({ + type: Boolean, + default: false, + }), + ...commonFieldsSchema, }), - ...commonFieldsSchema, -}); +); export const pipelineSchema = new Schema({ _id: field({ pkey: true }), diff --git a/src/db/models/definitions/brands.ts b/src/db/models/definitions/brands.ts index 5cbff8fb2..2577d0782 100644 --- a/src/db/models/definitions/brands.ts +++ b/src/db/models/definitions/brands.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IBrandEmailConfig { type?: string; diff --git a/src/db/models/definitions/channels.ts b/src/db/models/definitions/channels.ts index ea0be7108..821f2d853 100644 --- a/src/db/models/definitions/channels.ts +++ b/src/db/models/definitions/channels.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IChannel { name?: string; diff --git a/src/db/models/definitions/common.ts b/src/db/models/definitions/common.ts index a72199020..024bcbd1c 100644 --- a/src/db/models/definitions/common.ts +++ b/src/db/models/definitions/common.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IRule extends Document { _id: string; kind: string; diff --git a/src/db/models/definitions/companies.ts b/src/db/models/definitions/companies.ts index ab536e697..7501c125e 100644 --- a/src/db/models/definitions/companies.ts +++ b/src/db/models/definitions/companies.ts @@ -8,7 +8,7 @@ import { STATUSES, } from './constants'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface ILink { linkedIn?: string; @@ -22,6 +22,7 @@ export interface ILink { interface ILinkDocument extends ILink, Document {} export interface ICompany { + scopeBrandIds?: string[]; primaryName?: string; avatar?: string; names?: string[]; @@ -72,113 +73,115 @@ const linkSchema = new Schema( { _id: false }, ); -export const companySchema = new Schema({ - _id: field({ pkey: true }), - - createdAt: field({ type: Date, label: 'Created at' }), - modifiedAt: field({ type: Date, label: 'Modified at' }), - - primaryName: field({ - type: String, - label: 'Name', - }), - - names: field({ - type: [String], - optional: true, - }), - - avatar: field({ - type: String, - optional: true, - }), - - size: field({ - type: Number, - label: 'Size', - optional: true, - }), - - industry: field({ - type: String, - enum: COMPANY_INDUSTRY_TYPES, - label: 'Industry', - optional: true, - }), - - website: field({ - type: String, - label: 'Website', - optional: true, - }), - - plan: field({ - type: String, - label: 'Plan', - optional: true, +export const companySchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + + createdAt: field({ type: Date, label: 'Created at' }), + modifiedAt: field({ type: Date, label: 'Modified at' }), + + primaryName: field({ + type: String, + label: 'Name', + }), + + names: field({ + type: [String], + optional: true, + }), + + avatar: field({ + type: String, + optional: true, + }), + + size: field({ + type: Number, + label: 'Size', + optional: true, + }), + + industry: field({ + type: String, + enum: COMPANY_INDUSTRY_TYPES, + label: 'Industry', + optional: true, + }), + + website: field({ + type: String, + label: 'Website', + optional: true, + }), + + plan: field({ + type: String, + label: 'Plan', + optional: true, + }), + + parentCompanyId: field({ + type: String, + optional: true, + label: 'Parent Company', + }), + + primaryEmail: field({ type: String, optional: true, label: 'Email' }), + emails: field({ type: [String], optional: true }), + + primaryPhone: field({ type: String, optional: true, label: 'Phone' }), + phones: field({ type: [String], optional: true }), + + ownerId: field({ type: String, optional: true, label: 'Owner' }), + + leadStatus: field({ + type: String, + enum: COMPANY_LEAD_STATUS_TYPES, + optional: true, + label: 'Lead Status', + }), + + status: field({ + type: String, + enum: STATUSES.ALL, + default: STATUSES.ACTIVE, + optional: true, + label: 'Status', + }), + + lifecycleState: field({ + type: String, + enum: COMPANY_LIFECYCLE_STATE_TYPES, + optional: true, + label: 'Lifecycle State', + }), + + businessType: field({ + type: String, + enum: COMPANY_BUSINESS_TYPES, + optional: true, + label: 'Business Type', + }), + + description: field({ type: String, optional: true }), + employees: field({ type: Number, optional: true, label: 'Employees' }), + doNotDisturb: field({ + type: String, + optional: true, + label: 'Do not disturb', + }), + links: field({ type: linkSchema, default: {} }), + + tagIds: field({ + type: [String], + optional: true, + }), + + // Merged company ids + mergedIds: field({ type: [String], optional: true }), + + customFieldsData: field({ + type: Object, + }), }), - - parentCompanyId: field({ - type: String, - optional: true, - label: 'Parent Company', - }), - - primaryEmail: field({ type: String, optional: true, label: 'Email' }), - emails: field({ type: [String], optional: true }), - - primaryPhone: field({ type: String, optional: true, label: 'Phone' }), - phones: field({ type: [String], optional: true }), - - ownerId: field({ type: String, optional: true, label: 'Owner' }), - - leadStatus: field({ - type: String, - enum: COMPANY_LEAD_STATUS_TYPES, - optional: true, - label: 'Lead Status', - }), - - status: field({ - type: String, - enum: STATUSES.ALL, - default: STATUSES.ACTIVE, - optional: true, - label: 'Status', - }), - - lifecycleState: field({ - type: String, - enum: COMPANY_LIFECYCLE_STATE_TYPES, - optional: true, - label: 'Lifecycle State', - }), - - businessType: field({ - type: String, - enum: COMPANY_BUSINESS_TYPES, - optional: true, - label: 'Business Type', - }), - - description: field({ type: String, optional: true }), - employees: field({ type: Number, optional: true, label: 'Employees' }), - doNotDisturb: field({ - type: String, - optional: true, - label: 'Do not disturb', - }), - links: field({ type: linkSchema, default: {} }), - - tagIds: field({ - type: [String], - optional: true, - }), - - // Merged company ids - mergedIds: field({ type: [String], optional: true }), - - customFieldsData: field({ - type: Object, - }), -}); +); diff --git a/src/db/models/definitions/configs.ts b/src/db/models/definitions/configs.ts index cc80820a1..b1a0de9d9 100644 --- a/src/db/models/definitions/configs.ts +++ b/src/db/models/definitions/configs.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IConfig { code: string; diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index ad3849fc7..977b05356 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; interface IEngageDataRules { kind: string; diff --git a/src/db/models/definitions/conversations.ts b/src/db/models/definitions/conversations.ts index 383b7a870..14bf74717 100644 --- a/src/db/models/definitions/conversations.ts +++ b/src/db/models/definitions/conversations.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { CONVERSATION_STATUSES } from './constants'; +import { field } from './utils'; export interface IConversation { content?: string; diff --git a/src/db/models/definitions/customers.ts b/src/db/models/definitions/customers.ts index d428be28a..1c3d80e46 100644 --- a/src/db/models/definitions/customers.ts +++ b/src/db/models/definitions/customers.ts @@ -2,7 +2,7 @@ import { Document, Schema } from 'mongoose'; import { CUSTOMER_LEAD_STATUS_TYPES, CUSTOMER_LIFECYCLE_STATE_TYPES, STATUSES } from './constants'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface ILocation { remoteAddress: string; @@ -44,6 +44,7 @@ export interface ILink { interface ILinkDocument extends ILink, Document {} export interface ICustomer { + scopeBrandIds?: string[]; firstName?: string; lastName?: string; primaryEmail?: string; @@ -148,82 +149,84 @@ const linkSchema = new Schema( { _id: false }, ); -export const customerSchema = new Schema({ - _id: field({ pkey: true }), +export const customerSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), - createdAt: field({ type: Date, label: 'Created at' }), - modifiedAt: field({ type: Date, label: 'Modified at' }), - avatar: field({ type: String, optional: true }), + createdAt: field({ type: Date, label: 'Created at' }), + modifiedAt: field({ type: Date, label: 'Modified at' }), + avatar: field({ type: String, optional: true }), - firstName: field({ type: String, label: 'First name', optional: true }), - lastName: field({ type: String, label: 'Last name', optional: true }), + firstName: field({ type: String, label: 'First name', optional: true }), + lastName: field({ type: String, label: 'Last name', optional: true }), - primaryEmail: field({ type: String, label: 'Primary Email', optional: true }), - emails: field({ type: [String], optional: true }), - hasValidEmail: field({ type: Boolean, optional: true }), + primaryEmail: field({ type: String, label: 'Primary Email', optional: true }), + emails: field({ type: [String], optional: true }), + hasValidEmail: field({ type: Boolean, optional: true }), - primaryPhone: field({ type: String, label: 'Primary Phone', optional: true }), - phones: field({ type: [String], optional: true }), - profileScore: field({ type: Number, index: true, optional: true }), + primaryPhone: field({ type: String, label: 'Primary Phone', optional: true }), + phones: field({ type: [String], optional: true }), + profileScore: field({ type: Number, index: true, optional: true }), - ownerId: field({ type: String, optional: true }), - position: field({ type: String, optional: true, label: 'Position' }), - department: field({ type: String, optional: true, label: 'Department' }), + ownerId: field({ type: String, optional: true }), + position: field({ type: String, optional: true, label: 'Position' }), + department: field({ type: String, optional: true, label: 'Department' }), - leadStatus: field({ - type: String, - enum: CUSTOMER_LEAD_STATUS_TYPES, - optional: true, - label: 'Lead Status', - }), + leadStatus: field({ + type: String, + enum: CUSTOMER_LEAD_STATUS_TYPES, + optional: true, + label: 'Lead Status', + }), - status: field({ - type: String, - enum: STATUSES.ALL, - default: STATUSES.ACTIVE, - optional: true, - label: 'Status', - index: true, - }), + status: field({ + type: String, + enum: STATUSES.ALL, + default: STATUSES.ACTIVE, + optional: true, + label: 'Status', + index: true, + }), - lifecycleState: field({ - type: String, - enum: CUSTOMER_LIFECYCLE_STATE_TYPES, - optional: true, - label: 'Lifecycle State', - }), + lifecycleState: field({ + type: String, + enum: CUSTOMER_LIFECYCLE_STATE_TYPES, + optional: true, + label: 'Lifecycle State', + }), - hasAuthority: field({ type: String, optional: true, label: 'Has authority' }), - description: field({ type: String, optional: true, label: 'Description' }), - doNotDisturb: field({ - type: String, - optional: true, - label: 'Do not disturb', - }), - links: field({ type: linkSchema, default: {} }), + hasAuthority: field({ type: String, optional: true, label: 'Has authority' }), + description: field({ type: String, optional: true, label: 'Description' }), + doNotDisturb: field({ + type: String, + optional: true, + label: 'Do not disturb', + }), + links: field({ type: linkSchema, default: {} }), - isUser: field({ type: Boolean, label: 'Is user', optional: true }), + isUser: field({ type: Boolean, label: 'Is user', optional: true }), - integrationId: field({ type: String, optional: true }), - tagIds: field({ type: [String], optional: true, index: true }), - companyIds: field({ type: [String], optional: true }), + integrationId: field({ type: String, optional: true }), + tagIds: field({ type: [String], optional: true, index: true }), + companyIds: field({ type: [String], optional: true }), - // Merged customer ids - mergedIds: field({ type: [String], optional: true }), + // Merged customer ids + mergedIds: field({ type: [String], optional: true }), - customFieldsData: field({ type: Object, optional: true }), - messengerData: field({ type: messengerSchema, optional: true }), + customFieldsData: field({ type: Object, optional: true }), + messengerData: field({ type: messengerSchema, optional: true }), - location: field({ type: locationSchema, optional: true }), + location: field({ type: locationSchema, optional: true }), - // if customer is not a user then we will contact with this visitor using - // this information - visitorContactInfo: field({ - type: visitorContactSchema, - optional: true, - label: 'Visitor contact info', - }), - urlVisits: Object, + // if customer is not a user then we will contact with this visitor using + // this information + visitorContactInfo: field({ + type: visitorContactSchema, + optional: true, + label: 'Visitor contact info', + }), + urlVisits: Object, - deviceTokens: field({ type: [String], default: [] }), -}); + deviceTokens: field({ type: [String], default: [] }), + }), +); diff --git a/src/db/models/definitions/deals.ts b/src/db/models/definitions/deals.ts index ee7eb09d5..8f27ea83f 100644 --- a/src/db/models/definitions/deals.ts +++ b/src/db/models/definitions/deals.ts @@ -1,7 +1,7 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; import { PRODUCT_TYPES } from './constants'; +import { field } from './utils'; export interface IProduct { name: string; diff --git a/src/db/models/definitions/emailDeliveries.ts b/src/db/models/definitions/emailDeliveries.ts index a7b283748..b65e258f1 100644 --- a/src/db/models/definitions/emailDeliveries.ts +++ b/src/db/models/definitions/emailDeliveries.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; interface IAttachmentParams { data: string; diff --git a/src/db/models/definitions/emailTemplates.ts b/src/db/models/definitions/emailTemplates.ts index e3669297c..98514ebbe 100644 --- a/src/db/models/definitions/emailTemplates.ts +++ b/src/db/models/definitions/emailTemplates.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IEmailTemplate { name: string; @@ -10,8 +10,10 @@ export interface IEmailTemplateDocument extends IEmailTemplate, Document { _id: string; } -export const emailTemplateSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - content: field({ type: String, optional: true }), -}); +export const emailTemplateSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String }), + content: field({ type: String, optional: true }), + }), +); diff --git a/src/db/models/definitions/engages.ts b/src/db/models/definitions/engages.ts index 0bef558d9..00532156d 100644 --- a/src/db/models/definitions/engages.ts +++ b/src/db/models/definitions/engages.ts @@ -1,7 +1,7 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { IRule, ruleSchema } from './common'; import { MESSENGER_KINDS, METHODS, SENT_AS_CHOICES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface IScheduleDate { type?: string; @@ -96,33 +96,35 @@ const messengerSchema = new Schema( { _id: false }, ); -export const engageMessageSchema = new Schema({ - _id: field({ pkey: true }), - kind: field({ type: String }), - segmentId: field({ type: String, optional: true }), // TODO Remove - segmentIds: field({ - type: [String], - optional: true, - }), - brandIds: field({ - type: [String], - optional: true, - }), - customerIds: field({ type: [String] }), - title: field({ type: String }), - fromUserId: field({ type: String }), - method: field({ - type: String, - enum: METHODS.ALL, - }), - isDraft: field({ type: Boolean }), - isLive: field({ type: Boolean }), - stopDate: field({ type: Date }), - createdDate: field({ type: Date }), - tagIds: field({ type: [String], optional: true }), - messengerReceivedCustomerIds: field({ type: [String] }), +export const engageMessageSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + kind: field({ type: String }), + segmentId: field({ type: String, optional: true }), // TODO Remove + segmentIds: field({ + type: [String], + optional: true, + }), + brandIds: field({ + type: [String], + optional: true, + }), + customerIds: field({ type: [String] }), + title: field({ type: String }), + fromUserId: field({ type: String }), + method: field({ + type: String, + enum: METHODS.ALL, + }), + isDraft: field({ type: Boolean }), + isLive: field({ type: Boolean }), + stopDate: field({ type: Date }), + createdDate: field({ type: Date }), + tagIds: field({ type: [String], optional: true }), + messengerReceivedCustomerIds: field({ type: [String] }), - email: field({ type: emailSchema }), - scheduleDate: field({ type: scheduleDateSchema }), - messenger: field({ type: messengerSchema }), -}); + email: field({ type: emailSchema }), + scheduleDate: field({ type: scheduleDateSchema }), + messenger: field({ type: messengerSchema }), + }), +); diff --git a/src/db/models/definitions/fields.ts b/src/db/models/definitions/fields.ts index 482fd9b57..4f9ae4855 100644 --- a/src/db/models/definitions/fields.ts +++ b/src/db/models/definitions/fields.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { FIELDS_GROUPS_CONTENT_TYPES } from './constants'; +import { field } from './utils'; export interface IField { contentType?: string; diff --git a/src/db/models/definitions/forms.ts b/src/db/models/definitions/forms.ts index c03a69bdc..480e0b79a 100644 --- a/src/db/models/definitions/forms.ts +++ b/src/db/models/definitions/forms.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { IRule, ruleSchema } from './common'; +import { field, schemaWrapper } from './utils'; export interface ICallout extends Document { title?: string; @@ -56,24 +56,26 @@ const submissionSchema = new Schema( ); // schema for form document -export const formSchema = new Schema({ - _id: field({ pkey: true }), - title: field({ type: String, optional: true }), - description: field({ - type: String, - optional: true, - }), - buttonText: field({ type: String, optional: true }), - themeColor: field({ type: String, optional: true }), - code: field({ type: String }), - createdUserId: field({ type: String }), - createdDate: field({ - type: Date, - default: Date.now, +export const formSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + title: field({ type: String, optional: true }), + description: field({ + type: String, + optional: true, + }), + buttonText: field({ type: String, optional: true }), + themeColor: field({ type: String, optional: true }), + code: field({ type: String }), + createdUserId: field({ type: String }), + createdDate: field({ + type: Date, + default: Date.now, + }), + callout: field({ type: calloutSchema, default: {} }), + viewCount: field({ type: Number }), + contactsGathered: field({ type: Number }), + submissions: field({ type: [submissionSchema] }), + rules: field({ type: [ruleSchema] }), }), - callout: field({ type: calloutSchema, default: {} }), - viewCount: field({ type: Number }), - contactsGathered: field({ type: Number }), - submissions: field({ type: [submissionSchema] }), - rules: field({ type: [ruleSchema] }), -}); +); diff --git a/src/db/models/definitions/importHistory.ts b/src/db/models/definitions/importHistory.ts index 72f1095d8..04263ed69 100644 --- a/src/db/models/definitions/importHistory.ts +++ b/src/db/models/definitions/importHistory.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IImportHistory { success: number; diff --git a/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index e99494309..eccc27699 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { FORM_LOAD_TYPES, FORM_SUCCESS_ACTIONS, KIND_CHOICES, MESSENGER_DATA_AVAILABILITY } from './constants'; +import { field } from './utils'; export interface ILink { twitter?: string; diff --git a/src/db/models/definitions/internalNotes.ts b/src/db/models/definitions/internalNotes.ts index 18b16153e..fba114edb 100644 --- a/src/db/models/definitions/internalNotes.ts +++ b/src/db/models/definitions/internalNotes.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { ACTIVITY_CONTENT_TYPES } from './constants'; +import { field } from './utils'; export interface IInternalNote { contentType: string; diff --git a/src/db/models/definitions/knowledgebase.ts b/src/db/models/definitions/knowledgebase.ts index e5033a242..8c2cc7a20 100644 --- a/src/db/models/definitions/knowledgebase.ts +++ b/src/db/models/definitions/knowledgebase.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { PUBLISH_STATUSES } from './constants'; +import { field, schemaWrapper } from './utils'; interface ICommonFields { createdBy: string; @@ -85,24 +85,26 @@ export const categorySchema = new Schema({ ...commonFields, }); -export const topicSchema = new Schema({ - _id: field({ pkey: true }), - title: field({ type: String }), - description: field({ type: String, optional: true }), - brandId: field({ type: String, optional: true }), +export const topicSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + title: field({ type: String }), + description: field({ type: String, optional: true }), + brandId: field({ type: String, optional: true }), - categoryIds: field({ - type: [String], - required: false, - }), + categoryIds: field({ + type: [String], + required: false, + }), - color: field({ type: String, optional: true }), - backgroundImage: field({ type: String, optional: true }), + color: field({ type: String, optional: true }), + backgroundImage: field({ type: String, optional: true }), - languageCode: field({ - type: String, - optional: true, - }), + languageCode: field({ + type: String, + optional: true, + }), - ...commonFields, -}); + ...commonFields, + }), +); diff --git a/src/db/models/definitions/messengerApps.ts b/src/db/models/definitions/messengerApps.ts index 564054605..9fc459802 100644 --- a/src/db/models/definitions/messengerApps.ts +++ b/src/db/models/definitions/messengerApps.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IGoogleCredentials { access_token: string; @@ -33,16 +33,18 @@ export interface IMessengerAppDocument extends IMessengerApp, Document { } // Messenger apps =============== -export const messengerAppSchema = new Schema({ - _id: field({ pkey: true }), - - kind: field({ - type: String, - enum: ['googleMeet', 'knowledgebase', 'lead'], +export const messengerAppSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + + kind: field({ + type: String, + enum: ['googleMeet', 'knowledgebase', 'lead'], + }), + + name: field({ type: String }), + accountId: field({ type: String, optional: true }), + showInInbox: field({ type: Boolean, default: false }), + credentials: field({ type: Object }), }), - - name: field({ type: String }), - accountId: field({ type: String, optional: true }), - showInInbox: field({ type: Boolean, default: false }), - credentials: field({ type: Object }), -}); +); diff --git a/src/db/models/definitions/notifications.ts b/src/db/models/definitions/notifications.ts index 5ab2d08b5..c381f29f4 100644 --- a/src/db/models/definitions/notifications.ts +++ b/src/db/models/definitions/notifications.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { NOTIFICATION_TYPES } from './constants'; +import { field } from './utils'; export interface INotification { notifType?: string; diff --git a/src/db/models/definitions/permissions.ts b/src/db/models/definitions/permissions.ts index f458baf84..952bbb618 100644 --- a/src/db/models/definitions/permissions.ts +++ b/src/db/models/definitions/permissions.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IPermission { module?: string; diff --git a/src/db/models/definitions/responseTemplates.ts b/src/db/models/definitions/responseTemplates.ts index 1522ed882..cb280f9d5 100644 --- a/src/db/models/definitions/responseTemplates.ts +++ b/src/db/models/definitions/responseTemplates.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IResponseTemplate { name?: string; @@ -12,10 +12,12 @@ export interface IResponseTemplateDocument extends IResponseTemplate, Document { _id: string; } -export const responseTemplateSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - content: field({ type: String }), - brandId: field({ type: String }), - files: field({ type: Array }), -}); +export const responseTemplateSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String }), + content: field({ type: String }), + brandId: field({ type: String }), + files: field({ type: Array }), + }), +); diff --git a/src/db/models/definitions/scripts.ts b/src/db/models/definitions/scripts.ts index b47db88eb..b071a4145 100644 --- a/src/db/models/definitions/scripts.ts +++ b/src/db/models/definitions/scripts.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IScript { name: string; @@ -14,12 +14,14 @@ export interface IScriptDocument extends IScript, Document { _id: string; } -export const scriptSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - messengerId: field({ type: String }), - messengerBrandCode: field({ type: String }), - kbTopicId: field({ type: String }), - leadIds: field({ type: [String] }), - leadMaps: field({ type: [Object] }), -}); +export const scriptSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String }), + messengerId: field({ type: String }), + messengerBrandCode: field({ type: String }), + kbTopicId: field({ type: String }), + leadIds: field({ type: [String] }), + leadMaps: field({ type: [Object] }), + }), +); diff --git a/src/db/models/definitions/segments.ts b/src/db/models/definitions/segments.ts index 65972e0df..7e5a9fa63 100644 --- a/src/db/models/definitions/segments.ts +++ b/src/db/models/definitions/segments.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { ACTIVITY_CONTENT_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface ICondition { field: string; @@ -53,16 +53,18 @@ const conditionSchema = new Schema( { _id: false }, ); -export const segmentSchema = new Schema({ - _id: field({ pkey: true }), - contentType: field({ - type: String, - enum: ACTIVITY_CONTENT_TYPES.ALL, +export const segmentSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + contentType: field({ + type: String, + enum: ACTIVITY_CONTENT_TYPES.ALL, + }), + name: field({ type: String }), + description: field({ type: String, optional: true }), + subOf: field({ type: String, optional: true }), + color: field({ type: String }), + connector: field({ type: String }), + conditions: field({ type: [conditionSchema] }), }), - name: field({ type: String }), - description: field({ type: String, optional: true }), - subOf: field({ type: String, optional: true }), - color: field({ type: String }), - connector: field({ type: String }), - conditions: field({ type: [conditionSchema] }), -}); +); diff --git a/src/db/models/definitions/tags.ts b/src/db/models/definitions/tags.ts index 61c167976..937fe04d9 100644 --- a/src/db/models/definitions/tags.ts +++ b/src/db/models/definitions/tags.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { TAG_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface ITag { name: string; @@ -14,14 +14,16 @@ export interface ITagDocument extends ITag, Document { createdAt: Date; } -export const tagSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - type: field({ - type: String, - enum: TAG_TYPES.ALL, +export const tagSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String }), + type: field({ + type: String, + enum: TAG_TYPES.ALL, + }), + colorCode: field({ type: String }), + createdAt: field({ type: Date }), + objectCount: field({ type: Number }), }), - colorCode: field({ type: String }), - createdAt: field({ type: Date }), - objectCount: field({ type: Number }), -}); +); diff --git a/src/db/models/definitions/tasks.ts b/src/db/models/definitions/tasks.ts index 84aca92d1..1f4d8668b 100644 --- a/src/db/models/definitions/tasks.ts +++ b/src/db/models/definitions/tasks.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { field } from './utils'; export interface ITask extends IItemCommonFields { priority?: string; diff --git a/src/db/models/definitions/tickets.ts b/src/db/models/definitions/tickets.ts index 459993f9c..1f26d5abf 100644 --- a/src/db/models/definitions/tickets.ts +++ b/src/db/models/definitions/tickets.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { field } from './utils'; export interface ITicket extends IItemCommonFields { priority?: string; diff --git a/src/db/models/definitions/users.ts b/src/db/models/definitions/users.ts index 31b836fbe..78c94a10e 100644 --- a/src/db/models/definitions/users.ts +++ b/src/db/models/definitions/users.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IEmailSignature { brandId?: string; @@ -46,6 +46,7 @@ export interface IUser { details?: IDetail; links?: ILink; isActive?: boolean; + brandIds?: string[]; groupIds?: string[]; deviceTokens?: string[]; } @@ -113,6 +114,7 @@ export const userSchema = new Schema({ details: field({ type: detailSchema, default: {} }), links: field({ type: linkSchema, default: {} }), isActive: field({ type: Boolean, default: true }), + brandIds: field({ type: [String] }), groupIds: field({ type: [String] }), deviceTokens: field({ type: [String], default: [] }), }); diff --git a/src/db/models/definitions/utils.ts b/src/db/models/definitions/utils.ts new file mode 100644 index 000000000..faa05981e --- /dev/null +++ b/src/db/models/definitions/utils.ts @@ -0,0 +1,26 @@ +import * as Random from 'meteor-random'; + +/* + * Mongoose field options wrapper + */ +export const field = options => { + const { pkey, type, optional } = options; + + if (type === String && !pkey && !optional) { + options.validate = /\S+/; + } + + // TODO: remove + if (pkey) { + options.type = String; + options.default = () => Random.id(); + } + + return options; +}; + +export const schemaWrapper = schema => { + schema.add({ scopeBrandIds: [String] }); + + return schema; +}; diff --git a/src/middlewares/integrationsApiMiddleware.ts b/src/middlewares/integrationsApiMiddleware.ts index c0309d6db..e373188d2 100644 --- a/src/middlewares/integrationsApiMiddleware.ts +++ b/src/middlewares/integrationsApiMiddleware.ts @@ -1,4 +1,4 @@ -import { ActivityLogs, ConversationMessages, Conversations, Customers } from '../db/models'; +import { ActivityLogs, ConversationMessages, Conversations, Customers, Integrations } from '../db/models'; import { CONVERSATION_STATUSES } from '../db/models/definitions/constants'; import { graphqlPubsub } from '../pubsub'; @@ -17,7 +17,16 @@ const integrationsApiMiddleware = async (req, res) => { const doc = JSON.parse(payload); if (action === 'create-customer') { - const customer = await Customers.createCustomer(doc); + const integration = await Integrations.findOne({ _id: doc.integrationId }); + + if (!integration) { + throw new Error(`Integration not found: ${doc.integrationId}`); + } + + const customer = await Customers.createCustomer({ + ...doc, + scopeBrandIds: integration.brandId, + }); return res.json({ _id: customer._id }); } diff --git a/src/workers/bulkInsert.ts b/src/workers/bulkInsert.ts index 57011d82b..bb4dd2143 100644 --- a/src/workers/bulkInsert.ts +++ b/src/workers/bulkInsert.ts @@ -1,10 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as xlsxPopulate from 'xlsx-populate'; +import { checkFieldNames } from '../data/modules/coc/utils'; import { can } from '../data/permissions/utils'; import { ImportHistory } from '../db/models'; import { IUserDocument } from '../db/models/definitions/users'; -import { checkFieldNames } from '../db/models/utils'; import { createWorkers, splitToCore } from './utils'; export const intervals: any[] = []; @@ -13,7 +13,11 @@ export const intervals: any[] = []; * Receives and saves xls file in private/xlsImports folder * and imports customers to the database */ -export const importXlsFile = async (file: any, type: string, { user }: { user: IUserDocument }) => { +export const importXlsFile = async ( + file: any, + type: string, + { user, scopeBrandIds }: { scopeBrandIds: string[]; user: IUserDocument }, +) => { return new Promise(async (resolve, reject) => { if (!(await can('importXlsFile', user))) { return reject(new Error('Permission denied!')); @@ -93,8 +97,9 @@ export const importXlsFile = async (file: any, type: string, { user }: { user: I const percentagePerData = Number(((1 / usedSheets.length) * 100).toFixed(3)); const workerData = { - contentType: type, + scopeBrandIds, user, + contentType: type, properties, importHistoryId: importHistory._id, percentagePerData, diff --git a/src/workers/bulkInsert.worker.ts b/src/workers/bulkInsert.worker.ts index 42aee11df..acbe81b8a 100644 --- a/src/workers/bulkInsert.worker.ts +++ b/src/workers/bulkInsert.worker.ts @@ -20,7 +20,7 @@ connect().then(async () => { return; } - const { result, contentType, properties, user, importHistoryId, percentagePerData } = workerData; + const { user, scopeBrandIds, result, contentType, properties, importHistoryId, percentagePerData } = workerData; let percentage = '0'; let create: any = Customers.createCustomer; @@ -47,6 +47,7 @@ connect().then(async () => { // Customer or company object to import const coc: any = { + scopeBrandIds, customFieldsData: {}, }; diff --git a/src/workers/index.ts b/src/workers/index.ts index 229471ea5..4f82e8466 100644 --- a/src/workers/index.ts +++ b/src/workers/index.ts @@ -75,6 +75,8 @@ app.post('/import-file', async (req: any, res) => { debugRequest(debugWorkers, req); + const scopeBrandIds = JSON.parse(req.cookies.scopeBrandIds || '[]'); + form.parse(req, async (_err, fields: any, response) => { let status = ''; @@ -89,7 +91,7 @@ app.post('/import-file', async (req: any, res) => { return res.json(status); } - importXlsFile(response.file, fields.type, { user: req.user }) + importXlsFile(response.file, fields.type, { scopeBrandIds, user: req.user }) .then(result => { debugResponse(debugWorkers, req); return res.json(result);