From 3855a5399939dc8a661cfd7c6a5b1984e87261ab Mon Sep 17 00:00:00 2001 From: mit-27 Date: Tue, 4 Jun 2024 03:22:26 -0400 Subject: [PATCH 1/3] :sparkles: Add cursor pagination for GET:crm/contacts API --- packages/api/prisma/schema.prisma | 2 +- packages/api/scripts/init.sql | 3 +- .../api/src/crm/contact/contact.controller.ts | 21 +++++++- .../crm/contact/services/contact.service.ts | 53 ++++++++++++++++--- .../api/src/crm/contact/sync/sync.service.ts | 14 ++--- .../types/dto/get-contacts-query.dto.ts | 26 +++++++++ packages/api/swagger/swagger-spec.json | 18 +++++++ 7 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index ded59f6b5..41498245c 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -164,7 +164,7 @@ model crm_contacts { id_crm_contact String @id(map: "pk_crm_contacts") @db.Uuid first_name String last_name String - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String remote_platform String diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 3ea0bac32..44aa7f31e 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -465,13 +465,14 @@ CREATE TABLE crm_contacts id_crm_contact uuid NOT NULL, first_name text NOT NULL, last_name text NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NOT NULL, remote_platform text NOT NULL, id_crm_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_crm_contacts PRIMARY KEY ( id_crm_contact ), + CONSTRAINT force_createdAt_unique UNIQUE ( created_at ), CONSTRAINT FK_23 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index b3eaed11a..7d8edf6d0 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -8,6 +8,8 @@ import { Param, UseGuards, Headers, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ContactService } from './services/contact.service'; import { LoggerService } from '@@core/logger/logger.service'; @@ -26,6 +28,7 @@ import { import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiCustomResponse } from '@@core/utils/types'; +import { GetContactsQueryDto } from './types/dto/get-contacts-query.dto' @ApiTags('crm/contacts') @Controller('crm/contacts') @@ -55,22 +58,38 @@ export class ContactController { type: Boolean, description: 'Set to true to include data from the original CRM software.', }) + @ApiQuery({ + name: 'pageSize', + required: false, + type: Number, + description: 'Set to get the number of records.' + }) + @ApiQuery({ + name: 'cursor', + required: false, + type: String, + description: 'Set to get the number of records after this cursor.' + }) @ApiCustomResponse(UnifiedContactOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getContacts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: GetContactsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.contactService.getContacts( remoteSource, linkedUserId, remote_data, + pageSize, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 1467fd3da..d6da221c3 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -11,7 +11,7 @@ import { UnifiedContactOutput, } from '@crm/contact/types/model.unified'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; @@ -116,8 +116,8 @@ export class ContactService { 'id' in source_contact ? String(source_contact.id) : 'contact_id' in source_contact - ? String(source_contact.contact_id) - : undefined; + ? String(source_contact.contact_id) + : undefined; const existingContact = await this.prisma.crm_contacts.findFirst({ where: { @@ -493,11 +493,36 @@ export class ContactService { integrationId: string, linkedUserId: string, remote_data?: boolean, - ): Promise { + pageSize?: number, + cursor?: string + ): Promise<{ data: UnifiedContactOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const contacts = await this.prisma.crm_contacts.findMany({ + // Default PageSize + const defaultPageSize = 10; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_contacts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_contact: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let contacts = await this.prisma.crm_contacts.findMany({ + take: pageSize ? pageSize + 1 : defaultPageSize + 1, + cursor: cursor ? { + id_crm_contact: cursor + } : undefined, + orderBy: { + modified_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -509,6 +534,18 @@ export class ContactService { }, }); + let prev_cursor = null; + let next_cursor = null; + + if ((pageSize && contacts.length === (pageSize + 1)) || (!pageSize && contacts.length === (defaultPageSize + 1))) { + next_cursor = Buffer.from(contacts[contacts.length - 1].id_crm_contact).toString('base64'); + contacts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( contacts.map(async (contact) => { // Fetch field mappings for the contact @@ -587,7 +624,11 @@ export class ContactService { id_linked_user: linkedUserId, }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/contact/sync/sync.service.ts b/packages/api/src/crm/contact/sync/sync.service.ts index b4485acf0..c23d41d86 100644 --- a/packages/api/src/crm/contact/sync/sync.service.ts +++ b/packages/api/src/crm/contact/sync/sync.service.ts @@ -85,12 +85,12 @@ export class SyncService implements OnModuleInit { const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -369,7 +369,7 @@ export class SyncService implements OnModuleInit { id_crm_contact: uuid, first_name: '', last_name: '', - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts b/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts new file mode 100644 index 000000000..371562d0b --- /dev/null +++ b/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts @@ -0,0 +1,26 @@ +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator' + +export class GetContactsQueryDto { + + @IsOptional() + @Transform(({ value }) => value === 'true' ? true : value === 'false' ? false : value) + @IsBoolean() + remote_data: boolean; + + @IsOptional() + @IsNumber() + @Transform(p => Number(p.value)) + pageSize: number; + + @IsOptional() + @Transform(p => Buffer.from(p.value, 'base64').toString()) + @IsUUID() + cursor: string; + + + + + + +} \ No newline at end of file diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index a33cc91c7..cda557771 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -1549,6 +1549,24 @@ "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { From c45781160688504f68f07f98591abc479a4cd1ce Mon Sep 17 00:00:00 2001 From: mit-27 Date: Mon, 10 Jun 2024 02:50:38 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20Add=20cursor=20pagination=20for?= =?UTF-8?q?=20Unified=20CRM=20APIs=20and=20fix=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/prisma/schema.prisma | 18 +- packages/api/scripts/init.sql | 25 +- .../utils/dtos/fetch-objects-query.dto.ts | 44 +++ packages/api/src/crm/@lib/@utils/index.ts | 61 ++-- .../api/src/crm/company/company.controller.ts | 17 +- .../crm/company/services/company.service.ts | 47 ++- .../src/crm/company/services/hubspot/index.ts | 1 + .../api/src/crm/contact/contact.controller.ts | 27 +- .../crm/contact/services/contact.service.ts | 15 +- .../types/dto/get-contacts-query.dto.ts | 26 -- packages/api/src/crm/deal/deal.controller.ts | 17 +- .../api/src/crm/deal/services/deal.service.ts | 47 ++- .../src/crm/deal/services/hubspot/index.ts | 5 +- .../src/crm/deal/services/hubspot/mappers.ts | 8 +- .../api/src/crm/deal/sync/sync.service.ts | 3 +- .../crm/engagement/engagement.controller.ts | 18 +- .../engagement/services/engagement.service.ts | 50 ++- packages/api/src/crm/note/note.controller.ts | 23 +- .../api/src/crm/note/services/note.service.ts | 46 ++- .../src/crm/stage/services/stage.service.ts | 46 ++- .../api/src/crm/stage/stage.controller.ts | 17 +- .../api/src/crm/task/services/task.service.ts | 46 ++- packages/api/src/crm/task/task.controller.ts | 24 +- .../api/src/crm/user/services/user.service.ts | 47 ++- packages/api/src/crm/user/user.controller.ts | 23 +- packages/api/src/main.ts | 4 + .../api/src/ticketing/@lib/@utils/index.ts | 12 +- packages/api/swagger/swagger-spec.json | 322 +++++++++++++++++- 28 files changed, 841 insertions(+), 198 deletions(-) create mode 100644 packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts delete mode 100644 packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 41498245c..36d4ee09b 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -141,7 +141,7 @@ model crm_companies { name String? industry String? number_of_employees BigInt? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_companies") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String? remote_platform String? @@ -164,7 +164,7 @@ model crm_contacts { id_crm_contact String @id(map: "pk_crm_contacts") @db.Uuid first_name String last_name String - created_at DateTime @unique(map: "force_createdat_unique") @default(now()) @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_contacts") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String remote_platform String @@ -184,8 +184,8 @@ model crm_deals { id_crm_deal String @id(map: "pk_crm_deal") @db.Uuid name String description String - amount BigInt - created_at DateTime @db.Timestamp(6) + amount BigInt? + created_at DateTime @unique(map: "force_createdat_unique_crm_deals") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String? remote_platform String? @@ -207,7 +207,7 @@ model crm_deals { model crm_deals_stages { id_crm_deals_stage String @id(map: "pk_crm_deal_stages") @db.Uuid stage_name String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_deals_stages") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid remote_id String? @@ -251,7 +251,7 @@ model crm_engagements { subject String? start_at DateTime? @db.Timestamp(6) end_time DateTime? @db.Timestamp(6) - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_engagements") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) remote_id String? id_linked_user String? @db.Uuid @@ -269,7 +269,7 @@ model crm_engagements { model crm_notes { id_crm_note String @id(map: "pk_crm_notes") @db.Uuid content String - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_notes") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_crm_company String? @db.Uuid id_crm_contact String? @db.Uuid @@ -312,7 +312,7 @@ model crm_tasks { status String? due_date DateTime? @db.Timestamp(6) finished_date DateTime? @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_tasks") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_crm_user String? @db.Uuid id_crm_company String? @db.Uuid @@ -333,7 +333,7 @@ model crm_users { id_crm_user String @id(map: "pk_crm_users") @db.Uuid name String? email String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_users") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid remote_id String? diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 44aa7f31e..6425329d6 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -283,11 +283,12 @@ CREATE TABLE crm_users id_crm_user uuid NOT NULL, name text NULL, email text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, + CONSTRAINT force_createdAt_unique_crm_users UNIQUE ( created_at ), CONSTRAINT PK_crm_users PRIMARY KEY ( id_crm_user ) ); @@ -304,11 +305,12 @@ CREATE TABLE crm_deals_stages ( id_crm_deals_stage uuid NOT NULL, stage_name text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, + CONSTRAINT force_createdAt_unique_crm_deals_stages UNIQUE ( created_at ), CONSTRAINT PK_crm_deal_stages PRIMARY KEY ( id_crm_deals_stage ) ); @@ -472,7 +474,7 @@ CREATE TABLE crm_contacts id_crm_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_crm_contacts PRIMARY KEY ( id_crm_contact ), - CONSTRAINT force_createdAt_unique UNIQUE ( created_at ), + CONSTRAINT force_createdAt_unique_crm_contacts UNIQUE ( created_at ), CONSTRAINT FK_23 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); @@ -497,13 +499,14 @@ CREATE TABLE crm_companies name text NULL, industry text NULL, number_of_employees bigint NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NULL, remote_platform text NULL, id_crm_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_crm_companies PRIMARY KEY ( id_crm_company ), + CONSTRAINT force_createdAt_unique_crm_companies UNIQUE ( created_at ), CONSTRAINT FK_24 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); @@ -744,7 +747,7 @@ CREATE TABLE crm_engagements subject text NULL, start_at timestamp NULL, end_time timestamp NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, remote_id text NULL, id_linked_user uuid NULL, @@ -752,6 +755,7 @@ CREATE TABLE crm_engagements id_crm_company uuid NULL, id_crm_user uuid NULL, CONSTRAINT PK_crm_engagement PRIMARY KEY ( id_crm_engagement ), + CONSTRAINT force_createdAt_unique_crm_engagements UNIQUE ( created_at ), CONSTRAINT FK_crm_engagement_crm_user FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_29 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); @@ -818,8 +822,8 @@ CREATE TABLE crm_deals id_crm_deal uuid NOT NULL, name text NOT NULL, description text NOT NULL, - amount bigint NOT NULL, - created_at timestamp NOT NULL, + amount bigint NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NULL, remote_platform text NULL, @@ -828,6 +832,7 @@ CREATE TABLE crm_deals id_linked_user uuid NULL, id_crm_company uuid NULL, CONSTRAINT PK_crm_deal PRIMARY KEY ( id_crm_deal ), + CONSTRAINT force_createdAt_unique_crm_deals UNIQUE ( created_at ), CONSTRAINT FK_22 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_21 FOREIGN KEY ( id_crm_deals_stage ) REFERENCES crm_deals_stages ( id_crm_deals_stage ), CONSTRAINT FK_47_1 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) @@ -1036,7 +1041,7 @@ CREATE TABLE crm_tasks status text NULL, due_date timestamp NULL, finished_date timestamp NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_crm_user uuid NULL, id_crm_company uuid NULL, @@ -1045,6 +1050,7 @@ CREATE TABLE crm_tasks remote_id text NULL, remote_platform text NULL, CONSTRAINT PK_crm_task PRIMARY KEY ( id_crm_task ), + CONSTRAINT force_createdAt_unique_crm_tasks UNIQUE ( created_at ), CONSTRAINT FK_26 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ), CONSTRAINT FK_25 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_27 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) @@ -1078,7 +1084,7 @@ CREATE TABLE crm_notes ( id_crm_note uuid NOT NULL, content text NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_crm_company uuid NULL, id_crm_contact uuid NULL, @@ -1088,6 +1094,7 @@ CREATE TABLE crm_notes remote_platform text NULL, id_crm_user uuid NULL, CONSTRAINT PK_crm_notes PRIMARY KEY ( id_crm_note ), + CONSTRAINT force_createdAt_unique_crm_notes UNIQUE ( created_at ), CONSTRAINT FK_19 FOREIGN KEY ( id_crm_contact ) REFERENCES crm_contacts ( id_crm_contact ), CONSTRAINT FK_18 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ), CONSTRAINT FK_20 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) diff --git a/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts b/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts new file mode 100644 index 000000000..7da1509fc --- /dev/null +++ b/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty, ApiQuery } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator' + + +// To provide a default pageSize +const DEFAULT_PAGE_SIZE = 50; + +export class FetchObjectsQueryDto { + + @ApiProperty({ + name: 'remote_data', + description: 'Set to true to include data from the original software.', + required: false + }) + @IsOptional() + @Transform(({ value }) => value === 'true' ? true : value === 'false' ? false : value) + @IsBoolean() + remote_data: boolean; + + @ApiProperty({ + name: 'pageSize', + required: false, + description: 'Set to get the number of records.' + }) + @IsOptional() + @IsNumber() + @Transform(p => Number(p.value)) + pageSize: number = DEFAULT_PAGE_SIZE; + + @ApiProperty({ + name: 'cursor', + required: false, + description: 'Set to get the number of records after this cursor.' + }) + @IsOptional() + @Transform(p => Buffer.from(p.value, 'base64').toString()) + @IsUUID() + cursor: string; + +} + + + diff --git a/packages/api/src/crm/@lib/@utils/index.ts b/packages/api/src/crm/@lib/@utils/index.ts index b8dfc67eb..921fa64c1 100644 --- a/packages/api/src/crm/@lib/@utils/index.ts +++ b/packages/api/src/crm/@lib/@utils/index.ts @@ -84,7 +84,8 @@ export class Utils { id_crm_user: uuid, }, }); - if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -98,7 +99,8 @@ export class Utils { id_crm_user: uuid, }, }); - if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + if (!res) return; return res; } catch (error) { throw new Error(error); @@ -114,9 +116,10 @@ export class Utils { }, }); if (!res) { - throw new Error( - `crm_user not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + // throw new Error( + // `crm_user not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + return; } return res.id_crm_user; } catch (error) { @@ -166,7 +169,8 @@ export class Utils { }, }); if (!res) { - throw new Error(`crm_companies not found for uuid ${uuid}`); + // throw new Error(`crm_companies not found for uuid ${uuid}`); + return; } return res.remote_id; } catch (error) { @@ -183,9 +187,10 @@ export class Utils { }, }); if (!res) { - throw new Error( - `crm_companies not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + return; + // throw new Error( + // `crm_companies not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); } return res.id_crm_company; } catch (error) { @@ -200,7 +205,8 @@ export class Utils { id_crm_deals_stage: uuid, }, }); - if (!res) throw new Error(`crm_deals_stages not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_deals_stages not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -216,9 +222,10 @@ export class Utils { }, }); if (!res) { - throw new Error( - `crm_deals_stages not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + return; + // throw new Error( + // `crm_deals_stages not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); } return res.id_crm_deals_stage; } catch (error) { @@ -233,7 +240,8 @@ export class Utils { id_crm_contact: uuid, }, }); - if (!res) throw new Error(`crm_contacts not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_contacts not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -248,10 +256,11 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) - throw new Error( - `crm_contacts not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + // if (!res) + // throw new Error( + // `crm_contacts not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + if (!res) return; return res.id_crm_contact; } catch (error) { throw new Error(error); @@ -265,7 +274,8 @@ export class Utils { id_crm_deal: uuid, }, }); - if (!res) throw new Error(`crm_deals not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_deals not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -280,10 +290,11 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) - throw new Error( - `crm_deals not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + // if (!res) + // throw new Error( + // `crm_deals not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + if (!res) return; return res.id_crm_deal; } catch (error) { throw new Error(error); @@ -308,7 +319,7 @@ export class Utils { return priority === 'High' ? 'HIGH' : priority === 'Medium' - ? 'MEDIUM' - : 'LOW'; + ? 'MEDIUM' + : 'LOW'; } } diff --git a/packages/api/src/crm/company/company.controller.ts b/packages/api/src/crm/company/company.controller.ts index c11446c7b..a04e3034d 100644 --- a/packages/api/src/crm/company/company.controller.ts +++ b/packages/api/src/crm/company/company.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -18,6 +20,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { CompanyService } from './services/company.service'; @@ -26,7 +29,9 @@ import { UnifiedCompanyOutput, } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/companies') @Controller('crm/companies') export class CompanyController { @@ -49,28 +54,26 @@ export class CompanyController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedCompanyOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getCompanies( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.companyService.getCompanies( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/company/services/company.service.ts b/packages/api/src/crm/company/services/company.service.ts index b86fa027a..156a31cda 100644 --- a/packages/api/src/crm/company/services/company.service.ts +++ b/packages/api/src/crm/company/services/company.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCompanyInput, @@ -454,10 +454,36 @@ export class CompanyService { async getCompanies( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCompanyOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const companies = await this.prisma.crm_companies.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_companies.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_company: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let companies = await this.prisma.crm_companies.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_company: cursor + } : undefined, + orderBy: { + modified_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -469,6 +495,15 @@ export class CompanyService { }, }); + if (companies.length === (pageSize + 1)) { + next_cursor = Buffer.from(companies[companies.length - 1].id_crm_company).toString('base64'); + companies.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedCompanies: UnifiedCompanyOutput[] = await Promise.all( companies.map(async (company) => { const values = await this.prisma.value.findMany({ @@ -548,7 +583,11 @@ export class CompanyService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/company/services/hubspot/index.ts b/packages/api/src/crm/company/services/hubspot/index.ts index 8187a6c4e..3c2a0b667 100644 --- a/packages/api/src/crm/company/services/hubspot/index.ts +++ b/packages/api/src/crm/company/services/hubspot/index.ts @@ -102,6 +102,7 @@ export class HubspotService implements ICompanyService { }); this.logger.log(`Synced hubspot companies !`); + return { data: resp.data.results, message: 'Hubspot companies retrieved', diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index 7d8edf6d0..3518a35fd 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -24,12 +24,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiCustomResponse } from '@@core/utils/types'; -import { GetContactsQueryDto } from './types/dto/get-contacts-query.dto' +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; + +@ApiBearerAuth('JWT') @ApiTags('crm/contacts') @Controller('crm/contacts') export class ContactController { @@ -52,31 +55,13 @@ export class ContactController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original CRM software.', - }) - @ApiQuery({ - name: 'pageSize', - required: false, - type: Number, - description: 'Set to get the number of records.' - }) - @ApiQuery({ - name: 'cursor', - required: false, - type: String, - description: 'Set to get the number of records after this cursor.' - }) @ApiCustomResponse(UnifiedContactOutput) @UseGuards(ApiKeyAuthGuard) @Get() @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getContacts( @Headers('x-connection-token') connection_token: string, - @Query() query: GetContactsQueryDto, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = @@ -87,8 +72,8 @@ export class ContactController { return this.contactService.getContacts( remoteSource, linkedUserId, - remote_data, pageSize, + remote_data, cursor ); } catch (error) { diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 509c7dbd9..e3f17000a 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -486,15 +486,15 @@ export class ContactService { async getContacts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - pageSize?: number, cursor?: string ): Promise<{ data: UnifiedContactOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - // Default PageSize - const defaultPageSize = 10; + let prev_cursor = null; + let next_cursor = null; if (cursor) { const isCursorPresent = await this.prisma.crm_contacts.findFirst({ @@ -510,12 +510,12 @@ export class ContactService { } let contacts = await this.prisma.crm_contacts.findMany({ - take: pageSize ? pageSize + 1 : defaultPageSize + 1, + take: pageSize + 1, cursor: cursor ? { id_crm_contact: cursor } : undefined, orderBy: { - modified_at: 'asc' + created_at: 'asc' }, where: { remote_platform: integrationId.toLowerCase(), @@ -528,10 +528,7 @@ export class ContactService { }, }); - let prev_cursor = null; - let next_cursor = null; - - if ((pageSize && contacts.length === (pageSize + 1)) || (!pageSize && contacts.length === (defaultPageSize + 1))) { + if (contacts.length === (pageSize + 1)) { next_cursor = Buffer.from(contacts[contacts.length - 1].id_crm_contact).toString('base64'); contacts.pop(); } diff --git a/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts b/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts deleted file mode 100644 index 371562d0b..000000000 --- a/packages/api/src/crm/contact/types/dto/get-contacts-query.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Transform } from 'class-transformer'; -import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator' - -export class GetContactsQueryDto { - - @IsOptional() - @Transform(({ value }) => value === 'true' ? true : value === 'false' ? false : value) - @IsBoolean() - remote_data: boolean; - - @IsOptional() - @IsNumber() - @Transform(p => Number(p.value)) - pageSize: number; - - @IsOptional() - @Transform(p => Buffer.from(p.value, 'base64').toString()) - @IsUUID() - cursor: string; - - - - - - -} \ No newline at end of file diff --git a/packages/api/src/crm/deal/deal.controller.ts b/packages/api/src/crm/deal/deal.controller.ts index 2c464ac9b..5d5a6c2bf 100644 --- a/packages/api/src/crm/deal/deal.controller.ts +++ b/packages/api/src/crm/deal/deal.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -18,12 +20,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { DealService } from './services/deal.service'; import { UnifiedDealInput, UnifiedDealOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/deals') @Controller('crm/deals') export class DealController { @@ -46,25 +51,21 @@ export class DealController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedDealOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getDeals( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.dealService.getDeals(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.dealService.getDeals(remoteSource, linkedUserId, pageSize, remote_data, cursor); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/deal/services/deal.service.ts b/packages/api/src/crm/deal/services/deal.service.ts index b73c13194..64d3d30dc 100644 --- a/packages/api/src/crm/deal/services/deal.service.ts +++ b/packages/api/src/crm/deal/services/deal.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedDealInput, UnifiedDealOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -318,16 +318,51 @@ export class DealService { async getDeals( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedDealOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const deals = await this.prisma.crm_deals.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_deals.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_deal: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let deals = await this.prisma.crm_deals.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_deal: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (deals.length === (pageSize + 1)) { + next_cursor = Buffer.from(deals[deals.length - 1].id_crm_deal).toString('base64'); + deals.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedDeals: UnifiedDealOutput[] = await Promise.all( deals.map(async (deal) => { // Fetch field mappings for the ticket @@ -398,7 +433,11 @@ export class DealService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/deal/services/hubspot/index.ts b/packages/api/src/crm/deal/services/hubspot/index.ts index 7e9672693..5c9ed96d3 100644 --- a/packages/api/src/crm/deal/services/hubspot/index.ts +++ b/packages/api/src/crm/deal/services/hubspot/index.ts @@ -53,8 +53,11 @@ export class HubspotService implements IDealService { }, }, ); + + this.logger.log(`Synced hubspot deals !`); + return { - data: resp.data, + data: resp.data.results, message: 'Hubspot deal created', statusCode: 201, }; diff --git a/packages/api/src/crm/deal/services/hubspot/mappers.ts b/packages/api/src/crm/deal/services/hubspot/mappers.ts index 2172e042c..ba2eae95b 100644 --- a/packages/api/src/crm/deal/services/hubspot/mappers.ts +++ b/packages/api/src/crm/deal/services/hubspot/mappers.ts @@ -101,11 +101,17 @@ export class HubspotDealMapper implements IDealMapper { } } + if (deal.properties.amount) { + opts = { + ...opts, + amount: parseFloat(deal.properties.amount) + } + } + return { remote_id: deal.id, name: deal.properties.dealname, description: deal.properties.dealname, // Placeholder if there's no direct mapping - amount: parseFloat(deal.properties.amount), //TODO; stage_id: deal.properties.dealstage, field_mappings, ...opts, diff --git a/packages/api/src/crm/deal/sync/sync.service.ts b/packages/api/src/crm/deal/sync/sync.service.ts index eae790bc4..6bbca8bd2 100644 --- a/packages/api/src/crm/deal/sync/sync.service.ts +++ b/packages/api/src/crm/deal/sync/sync.service.ts @@ -159,7 +159,7 @@ export class SyncService implements OnModuleInit { ); const sourceObject: OriginalDealOutput[] = resp.data; - //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + // this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); //unify the data according to the target obj wanted const unifiedObject = (await unify({ sourceObject, @@ -269,7 +269,6 @@ export class SyncService implements OnModuleInit { modified_at: new Date(), id_linked_user: linkedUserId, description: '', - amount: deal.amount, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/crm/engagement/engagement.controller.ts b/packages/api/src/crm/engagement/engagement.controller.ts index 57daa5a10..dd869c283 100644 --- a/packages/api/src/crm/engagement/engagement.controller.ts +++ b/packages/api/src/crm/engagement/engagement.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; @@ -18,6 +20,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { EngagementService } from './services/engagement.service'; @@ -26,7 +29,9 @@ import { UnifiedEngagementOutput, } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/engagements') @Controller('crm/engagements') export class EngagementController { @@ -49,28 +54,27 @@ export class EngagementController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedEngagementOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getEngagements( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; + return this.engagementService.getEngagements( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/engagement/services/engagement.service.ts b/packages/api/src/crm/engagement/services/engagement.service.ts index 70f895d93..83921540c 100644 --- a/packages/api/src/crm/engagement/services/engagement.service.ts +++ b/packages/api/src/crm/engagement/services/engagement.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedEngagementInput, @@ -124,8 +124,8 @@ export class EngagementService { type === 'CALL' ? CrmObject.engagement_call : type === 'MEETING' - ? CrmObject.engagement_meeting - : CrmObject.engagement_email; + ? CrmObject.engagement_meeting + : CrmObject.engagement_email; //unify the data according to the target obj wanted const unifiedObject = (await unify({ @@ -370,16 +370,50 @@ export class EngagementService { async getEngagements( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedEngagementOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const engagements = await this.prisma.crm_engagements.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_engagements.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_engagement: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let engagements = await this.prisma.crm_engagements.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_engagement: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (engagements.length === (pageSize + 1)) { + next_cursor = Buffer.from(engagements[engagements.length - 1].id_crm_engagement).toString('base64'); + engagements.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedEngagements: UnifiedEngagementOutput[] = await Promise.all( engagements.map(async (engagement) => { // Fetch field mappings for the ticket @@ -458,7 +492,11 @@ export class EngagementService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/note/note.controller.ts b/packages/api/src/crm/note/note.controller.ts index 76572964e..036e39aa8 100644 --- a/packages/api/src/crm/note/note.controller.ts +++ b/packages/api/src/crm/note/note.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -17,12 +19,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { NoteService } from './services/note.service'; import { UnifiedNoteInput, UnifiedNoteOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/notes') @Controller('crm/notes') export class NoteController { @@ -45,25 +50,27 @@ export class NoteController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedNoteOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getNotes( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.noteService.getNotes(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.noteService.getNotes( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/note/services/note.service.ts b/packages/api/src/crm/note/services/note.service.ts index 7157bd1a9..3e23c8fc6 100644 --- a/packages/api/src/crm/note/services/note.service.ts +++ b/packages/api/src/crm/note/services/note.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedNoteInput, UnifiedNoteOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -333,16 +333,50 @@ export class NoteService { async getNotes( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedNoteOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const notes = await this.prisma.crm_notes.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_notes.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_note: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let notes = await this.prisma.crm_notes.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_note: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (notes.length === (pageSize + 1)) { + next_cursor = Buffer.from(notes[notes.length - 1].id_crm_note).toString('base64'); + notes.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedNotes: UnifiedNoteOutput[] = await Promise.all( notes.map(async (note) => { // Fetch field mappings for the ticket @@ -413,7 +447,11 @@ export class NoteService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/stage/services/stage.service.ts b/packages/api/src/crm/stage/services/stage.service.ts index a7d4d4c48..f34d839cb 100644 --- a/packages/api/src/crm/stage/services/stage.service.ts +++ b/packages/api/src/crm/stage/services/stage.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedStageOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -82,16 +82,50 @@ export class StageService { async getStages( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedStageOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const stages = await this.prisma.crm_deals_stages.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_deals_stages.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_deals_stage: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let stages = await this.prisma.crm_deals_stages.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_deals_stage: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (stages.length === (pageSize + 1)) { + next_cursor = Buffer.from(stages[stages.length - 1].id_crm_deals_stage).toString('base64'); + stages.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedStages: UnifiedStageOutput[] = await Promise.all( stages.map(async (stage) => { // Fetch field mappings for the ticket @@ -158,7 +192,11 @@ export class StageService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/stage/stage.controller.ts b/packages/api/src/crm/stage/stage.controller.ts index 47bfdb12a..c75945ef8 100644 --- a/packages/api/src/crm/stage/stage.controller.ts +++ b/packages/api/src/crm/stage/stage.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; @@ -14,12 +16,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { StageService } from './services/stage.service'; import { UnifiedStageOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/stages') @Controller('crm/stages') export class StageController { @@ -42,28 +47,26 @@ export class StageController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedStageOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getStages( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.stageService.getStages( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/task/services/task.service.ts b/packages/api/src/crm/task/services/task.service.ts index b0d8c3b71..a28c05923 100644 --- a/packages/api/src/crm/task/services/task.service.ts +++ b/packages/api/src/crm/task/services/task.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedTaskInput, UnifiedTaskOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -341,16 +341,50 @@ export class TaskService { async getTasks( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTaskOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const tasks = await this.prisma.crm_tasks.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_tasks.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_task: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tasks = await this.prisma.crm_tasks.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_task: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (tasks.length === (pageSize + 1)) { + next_cursor = Buffer.from(tasks[tasks.length - 1].id_crm_task).toString('base64'); + tasks.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTasks: UnifiedTaskOutput[] = await Promise.all( tasks.map(async (task) => { // Fetch field mappings for the ticket @@ -423,7 +457,11 @@ export class TaskService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/task/task.controller.ts b/packages/api/src/crm/task/task.controller.ts index 52e20f9ce..2be474389 100644 --- a/packages/api/src/crm/task/task.controller.ts +++ b/packages/api/src/crm/task/task.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,13 +19,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TaskService } from './services/task.service'; import { UnifiedTaskInput, UnifiedTaskOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/tasks') @Controller('crm/tasks') export class TaskController { @@ -46,25 +51,28 @@ export class TaskController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedTaskOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTasks( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.taskService.getTasks(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + + return this.taskService.getTasks( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/user/services/user.service.ts b/packages/api/src/crm/user/services/user.service.ts index 93b1fb52f..38f47183a 100644 --- a/packages/api/src/crm/user/services/user.service.ts +++ b/packages/api/src/crm/user/services/user.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedUserOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -89,16 +89,51 @@ export class UserService { async getUsers( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedUserOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const users = await this.prisma.crm_users.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_users.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_user: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let users = await this.prisma.crm_users.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_user: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (users.length === (pageSize + 1)) { + next_cursor = Buffer.from(users[users.length - 1].id_crm_user).toString('base64'); + users.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedUsers: UnifiedUserOutput[] = await Promise.all( users.map(async (user) => { // Fetch field mappings for the ticket @@ -166,7 +201,11 @@ export class UserService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/user/user.controller.ts b/packages/api/src/crm/user/user.controller.ts index cd21c6cfe..e23c60371 100644 --- a/packages/api/src/crm/user/user.controller.ts +++ b/packages/api/src/crm/user/user.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { UserService } from './services/user.service'; import { UnifiedUserOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/users') @Controller('crm/users') export class UserController { @@ -42,25 +47,27 @@ export class UserController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedUserOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getUsers( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.userService.getUsers(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.userService.getUsers( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index 61b1ccaf2..2decb23af 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -13,6 +13,10 @@ async function bootstrap() { .setTitle('Unified Panora API') .setDescription('The Panora API description') .setVersion('1.0') + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'JWT' + ) .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/packages/api/src/ticketing/@lib/@utils/index.ts b/packages/api/src/ticketing/@lib/@utils/index.ts index a2fc9daa2..cdc3c216a 100644 --- a/packages/api/src/ticketing/@lib/@utils/index.ts +++ b/packages/api/src/ticketing/@lib/@utils/index.ts @@ -37,7 +37,8 @@ export class Utils { id_tcg_user: uuid, }, }); - if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -66,7 +67,8 @@ export class Utils { id_tcg_contact: uuid, }, }); - if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -97,7 +99,8 @@ export class Utils { id_tcg_user: uuid, }, }); - if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + if (!res) return; return res.email_address; } catch (error) { throw new Error(error); @@ -155,7 +158,8 @@ export class Utils { id_tcg_ticket: uuid, }, }); - if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 10faafd6f..655429fb8 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -495,10 +495,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -526,6 +545,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -597,6 +621,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -637,6 +666,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -690,6 +724,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -768,6 +807,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -789,7 +833,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } @@ -800,6 +844,7 @@ "in": "query", "description": "Set to get the number of records.", "schema": { + "default": 50, "type": "number" } }, @@ -838,6 +883,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -909,6 +959,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -938,6 +993,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -991,6 +1051,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1069,6 +1134,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1090,10 +1160,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1121,6 +1210,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1192,6 +1286,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1245,6 +1344,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -1285,6 +1389,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1363,6 +1472,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1384,10 +1498,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1415,6 +1548,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1486,6 +1624,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -1526,6 +1669,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1579,6 +1727,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1657,6 +1810,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1678,10 +1836,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1709,6 +1886,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1780,6 +1962,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1833,6 +2020,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1911,6 +2103,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1932,10 +2129,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1963,6 +2179,11 @@ }, "tags": [ "crm/stages" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2016,6 +2237,11 @@ }, "tags": [ "crm/stages" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2037,10 +2263,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2068,6 +2313,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -2139,6 +2389,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -2179,6 +2434,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2232,6 +2492,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2310,6 +2575,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2331,10 +2601,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2362,6 +2651,11 @@ }, "tags": [ "crm/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2415,6 +2709,11 @@ }, "tags": [ "crm/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4727,6 +5026,13 @@ "tags": [], "servers": [], "components": { + "securitySchemes": { + "JWT": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + } + }, "schemas": { "CreateUserDto": { "type": "object", From 729a6fda06ea92bdf28fcbfa58594fe83a5c0666 Mon Sep 17 00:00:00 2001 From: mit-27 Date: Tue, 11 Jun 2024 03:26:19 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Add=20cursor=20pagination=20for?= =?UTF-8?q?=20ticketing=20vertical?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +- packages/api/prisma/schema.prisma | 18 +- packages/api/scripts/init.sql | 27 +- .../api/src/crm/company/sync/sync.service.ts | 14 +- .../api/src/crm/deal/sync/sync.service.ts | 2 +- .../src/crm/engagement/sync/sync.service.ts | 2 +- .../api/src/crm/note/sync/sync.service.ts | 2 +- .../api/src/crm/stage/sync/sync.service.ts | 2 +- .../api/src/crm/task/sync/sync.service.ts | 2 +- .../api/src/crm/user/sync/sync.service.ts | 2 +- .../ticketing/account/account.controller.ts | 18 +- .../account/services/account.service.ts | 46 ++- .../ticketing/account/sync/sync.service.ts | 2 +- .../attachment/attachment.controller.ts | 19 +- .../attachment/services/attachment.service.ts | 45 ++- .../collection/collection.controller.ts | 18 +- .../collection/services/collection.service.ts | 48 ++- .../ticketing/collection/sync/sync.service.ts | 2 +- .../ticketing/comment/comment.controller.ts | 18 +- .../comment/services/comment.service.ts | 54 ++- .../ticketing/comment/sync/sync.service.ts | 2 +- .../ticketing/contact/contact.controller.ts | 18 +- .../contact/services/contact.service.ts | 46 ++- .../ticketing/contact/sync/sync.service.ts | 2 +- .../src/ticketing/tag/services/tag.service.ts | 47 ++- .../src/ticketing/tag/sync/sync.service.ts | 2 +- .../api/src/ticketing/tag/tag.controller.ts | 24 +- .../ticketing/team/services/team.service.ts | 46 ++- .../src/ticketing/team/sync/sync.service.ts | 2 +- .../api/src/ticketing/team/team.controller.ts | 24 +- .../ticket/services/ticket.service.ts | 47 ++- .../src/ticketing/ticket/sync/sync.service.ts | 2 +- .../src/ticketing/ticket/ticket.controller.ts | 18 +- .../ticketing/user/services/user.service.ts | 47 ++- .../src/ticketing/user/sync/sync.service.ts | 2 +- .../api/src/ticketing/user/user.controller.ts | 24 +- packages/api/swagger/swagger-spec.json | 319 +++++++++++++++++- 37 files changed, 851 insertions(+), 165 deletions(-) diff --git a/.env.example b/.env.example index ecfd56e50..a9da98ece 100644 --- a/.env.example +++ b/.env.example @@ -68,7 +68,8 @@ GORGIAS_TICKETING_CLOUD_CLIENT_SECRET= GORGIAS_TICKETING_CLOUD_SUBDOMAIN= FRONT_TICKETING_CLOUD_CLIENT_ID= FRONT_TICKETING_CLOUD_CLIENT_SECRET= - +GITLAB_TICKETING_CLOUD_CLIENT_ID= +GITLAB_TICKETING_CLOUD_CLIENT_SECRET= # ================================================ # Webapp settings # Must be set in the perspective of the end user browser diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 36d4ee09b..a8b656ce0 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -472,7 +472,7 @@ model tcg_accounts { name String? domains String[] remote_platform String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_accounts") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid tcg_contacts tcg_contacts[] @@ -486,7 +486,7 @@ model tcg_attachments { file_name String? file_url String? uploader String @db.Uuid - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_attachments") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid id_tcg_ticket String? @db.Uuid @@ -506,7 +506,7 @@ model tcg_collections { remote_platform String? collection_type String? parent_collection String? @db.Uuid - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_collections") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String @db.Uuid } @@ -519,7 +519,7 @@ model tcg_comments { is_private Boolean? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_comments") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) creator_type String? id_tcg_attachment String[] @@ -545,7 +545,7 @@ model tcg_contacts { details String? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_contacts") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) id_tcg_account String? @db.Uuid id_linked_user String? @db.Uuid @@ -560,7 +560,7 @@ model tcg_tags { name String? remote_id String? remote_platform String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_tags") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_tcg_ticket String? @db.Uuid id_linked_user String? @db.Uuid @@ -575,7 +575,7 @@ model tcg_teams { remote_platform String? name String? description String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_teams") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid } @@ -593,7 +593,7 @@ model tcg_tickets { collections String[] completed_at DateTime? @db.Timestamp(6) priority String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_tickets") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) assigned_to String[] remote_id String? @@ -616,7 +616,7 @@ model tcg_users { remote_id String? remote_platform String? teams String[] - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_users") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) id_linked_user String? @db.Uuid tcg_comments tcg_comments[] diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 6425329d6..ed09139ea 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -103,9 +103,10 @@ CREATE TABLE tcg_users remote_id text NULL, remote_platform text NULL, teams text[] NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_users UNIQUE ( created_at ), CONSTRAINT PK_tcg_users PRIMARY KEY ( id_tcg_user ) ); @@ -126,9 +127,10 @@ CREATE TABLE tcg_teams remote_platform text NULL, name text NULL, description text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_teams UNIQUE ( created_at ), CONSTRAINT PK_tcg_teams PRIMARY KEY ( id_tcg_team ) ); @@ -150,9 +152,10 @@ CREATE TABLE tcg_collections remote_platform text NULL, collection_type text NULL, parent_collection uuid NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NOT NULL, + CONSTRAINT force_createdAt_unique_tcg_collections UNIQUE ( created_at ), CONSTRAINT PK_tcg_collections PRIMARY KEY ( id_tcg_collection ) ); @@ -172,9 +175,10 @@ CREATE TABLE tcg_accounts name text NULL, domains text[] NULL, remote_platform text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_accounts UNIQUE ( created_at ), CONSTRAINT PK_tcg_account PRIMARY KEY ( id_tcg_account ) ); @@ -356,7 +360,7 @@ CREATE TABLE tcg_tickets collections text[] NULL, completed_at timestamp NULL, priority text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, assigned_to text[] NULL, remote_id text NULL, @@ -364,6 +368,7 @@ CREATE TABLE tcg_tickets creator_type text NULL, id_tcg_user uuid NULL, id_linked_user uuid NOT NULL, + CONSTRAINT force_createdAt_unique_tcg_tickets UNIQUE ( created_at ), CONSTRAINT PK_tcg_tickets PRIMARY KEY ( id_tcg_ticket ) ); @@ -397,11 +402,12 @@ CREATE TABLE tcg_contacts details text NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, id_tcg_account uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_contact PRIMARY KEY ( id_tcg_contact ), + CONSTRAINT force_createdAt_unique_tcg_contacts UNIQUE ( created_at ), CONSTRAINT FK_49 FOREIGN KEY ( id_tcg_account ) REFERENCES tcg_accounts ( id_tcg_account ) ); @@ -608,11 +614,12 @@ CREATE TABLE tcg_tags name text NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_tcg_ticket uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_tags PRIMARY KEY ( id_tcg_tag ), + CONSTRAINT force_createdAt_unique_tcg_tags UNIQUE ( created_at ), CONSTRAINT FK_48 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); @@ -638,7 +645,7 @@ CREATE TABLE tcg_comments is_private boolean NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, creator_type text NULL, id_tcg_attachment text[] NULL, @@ -647,6 +654,7 @@ CREATE TABLE tcg_comments id_tcg_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_comments PRIMARY KEY ( id_tcg_comment ), + CONSTRAINT force_createdAt_unique_tcg_comments UNIQUE ( created_at ), CONSTRAINT FK_41 FOREIGN KEY ( id_tcg_contact ) REFERENCES tcg_contacts ( id_tcg_contact ), CONSTRAINT FK_40_1 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ), CONSTRAINT FK_42 FOREIGN KEY ( id_tcg_user ) REFERENCES tcg_users ( id_tcg_user ) @@ -946,12 +954,13 @@ CREATE TABLE tcg_attachments file_name text NULL, file_url text NULL, uploader uuid NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, id_tcg_ticket uuid NULL, id_tcg_comment uuid NULL, CONSTRAINT PK_tcg_attachments PRIMARY KEY ( id_tcg_attachment ), + CONSTRAINT force_createdAt_unique_tcg_attachments UNIQUE ( created_at ), CONSTRAINT FK_51 FOREIGN KEY ( id_tcg_comment ) REFERENCES tcg_comments ( id_tcg_comment ), CONSTRAINT FK_50 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); diff --git a/packages/api/src/crm/company/sync/sync.service.ts b/packages/api/src/crm/company/sync/sync.service.ts index baa7de129..48b59dadb 100644 --- a/packages/api/src/crm/company/sync/sync.service.ts +++ b/packages/api/src/crm/company/sync/sync.service.ts @@ -72,12 +72,12 @@ export class SyncService implements OnModuleInit { this.logger.log(`Syncing companies....`); const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -353,7 +353,7 @@ export class SyncService implements OnModuleInit { const uuid = uuidv4(); let data: any = { id_crm_company: uuid, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/deal/sync/sync.service.ts b/packages/api/src/crm/deal/sync/sync.service.ts index 6bbca8bd2..6087f40f9 100644 --- a/packages/api/src/crm/deal/sync/sync.service.ts +++ b/packages/api/src/crm/deal/sync/sync.service.ts @@ -265,7 +265,7 @@ export class SyncService implements OnModuleInit { this.logger.log('deal not exists'); let data: any = { id_crm_deal: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, description: '', diff --git a/packages/api/src/crm/engagement/sync/sync.service.ts b/packages/api/src/crm/engagement/sync/sync.service.ts index bc535c672..034c7de98 100644 --- a/packages/api/src/crm/engagement/sync/sync.service.ts +++ b/packages/api/src/crm/engagement/sync/sync.service.ts @@ -280,7 +280,7 @@ export class SyncService implements OnModuleInit { this.logger.log('engagement not exists'); let data: any = { id_crm_engagement: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/note/sync/sync.service.ts b/packages/api/src/crm/note/sync/sync.service.ts index 9ce345aa0..73ce8de09 100644 --- a/packages/api/src/crm/note/sync/sync.service.ts +++ b/packages/api/src/crm/note/sync/sync.service.ts @@ -260,7 +260,7 @@ export class SyncService implements OnModuleInit { this.logger.log('note not exists'); let data: any = { id_crm_note: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/stage/sync/sync.service.ts b/packages/api/src/crm/stage/sync/sync.service.ts index 1c3974ed1..a427405f1 100644 --- a/packages/api/src/crm/stage/sync/sync.service.ts +++ b/packages/api/src/crm/stage/sync/sync.service.ts @@ -290,7 +290,7 @@ export class SyncService implements OnModuleInit { this.logger.log('stage not exists'); let data: any = { id_crm_deals_stage: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId || '', diff --git a/packages/api/src/crm/task/sync/sync.service.ts b/packages/api/src/crm/task/sync/sync.service.ts index e33555cf5..01d11fea9 100644 --- a/packages/api/src/crm/task/sync/sync.service.ts +++ b/packages/api/src/crm/task/sync/sync.service.ts @@ -269,7 +269,7 @@ export class SyncService implements OnModuleInit { this.logger.log('task not exists'); let data: any = { id_crm_task: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/user/sync/sync.service.ts b/packages/api/src/crm/user/sync/sync.service.ts index f5832a08c..a4917c417 100644 --- a/packages/api/src/crm/user/sync/sync.service.ts +++ b/packages/api/src/crm/user/sync/sync.service.ts @@ -252,7 +252,7 @@ export class SyncService implements OnModuleInit { this.logger.log('user not exists'); let data: any = { id_crm_user: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/account/account.controller.ts b/packages/api/src/ticketing/account/account.controller.ts index 2bfa586a1..f7ce70933 100644 --- a/packages/api/src/ticketing/account/account.controller.ts +++ b/packages/api/src/ticketing/account/account.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { AccountService } from './services/account.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedAccountOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/accounts') @Controller('ticketing/accounts') export class AccountController { @@ -42,29 +47,26 @@ export class AccountController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedAccountOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getAccounts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.accountService.getAccounts( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index 529a89239..b9cedab96 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedAccountOutput } from '../types/model.unified'; @Injectable() @@ -79,18 +79,52 @@ export class AccountService { async getAccounts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedAccountOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const accounts = await this.prisma.tcg_accounts.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_accounts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_account: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let accounts = await this.prisma.tcg_accounts.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_account: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (accounts.length === (pageSize + 1)) { + next_cursor = Buffer.from(accounts[accounts.length - 1].id_tcg_account).toString('base64'); + accounts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedAccounts: UnifiedAccountOutput[] = await Promise.all( accounts.map(async (account) => { // Fetch field mappings for the account @@ -157,7 +191,11 @@ export class AccountService { id_linked_user: linkedUserId, }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index ca6ac5e12..d97246851 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -245,7 +245,7 @@ export class SyncService implements OnModuleInit { id_tcg_account: uuidv4(), name: account.name, domains: account.domains, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/attachment/attachment.controller.ts b/packages/api/src/ticketing/attachment/attachment.controller.ts index 0d3d08c07..b92e3bb62 100644 --- a/packages/api/src/ticketing/attachment/attachment.controller.ts +++ b/packages/api/src/ticketing/attachment/attachment.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -16,6 +18,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { AttachmentService } from './services/attachment.service'; @@ -25,7 +28,9 @@ import { } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/attachments') @Controller('ticketing/attachments') export class AttachmentController { @@ -48,29 +53,27 @@ export class AttachmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedAttachmentOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getAttachments( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; + return this.attachmentService.getAttachments( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index ea1649abf..8736e2fbc 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedAttachmentInput, @@ -205,17 +205,50 @@ export class AttachmentService { async getAttachments( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedAttachmentOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const attachments = await this.prisma.tcg_attachments.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_attachments.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_attachment: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + let attachments = await this.prisma.tcg_attachments.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_attachment: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (attachments.length === (pageSize + 1)) { + next_cursor = Buffer.from(attachments[attachments.length - 1].id_tcg_attachment).toString('base64'); + attachments.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedAttachments: UnifiedAttachmentOutput[] = await Promise.all( attachments.map(async (attachment) => { // Fetch field mappings for the attachment @@ -285,7 +318,11 @@ export class AttachmentService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/collection/collection.controller.ts b/packages/api/src/ticketing/collection/collection.controller.ts index f895182ce..4ff55e22c 100644 --- a/packages/api/src/ticketing/collection/collection.controller.ts +++ b/packages/api/src/ticketing/collection/collection.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,6 +19,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { CollectionService } from './services/collection.service'; @@ -26,7 +29,9 @@ import { } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/collections') @Controller('ticketing/collections') export class CollectionController { @@ -49,29 +54,26 @@ export class CollectionController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedCollectionOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getCollections( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.collectionService.getCollections( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/collection/services/collection.service.ts b/packages/api/src/ticketing/collection/services/collection.service.ts index 58d23f808..243ec0f97 100644 --- a/packages/api/src/ticketing/collection/services/collection.service.ts +++ b/packages/api/src/ticketing/collection/services/collection.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCollectionOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -65,17 +65,51 @@ export class CollectionService { async getCollections( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCollectionOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - console.log('In collection service : ', integrationId); - const collections = await this.prisma.tcg_collections.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_collections.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_collection: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let collections = await this.prisma.tcg_collections.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_collection: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (collections.length === (pageSize + 1)) { + next_cursor = Buffer.from(collections[collections.length - 1].id_tcg_collection).toString('base64'); + collections.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedCollections: UnifiedCollectionOutput[] = await Promise.all( collections.map(async (collection) => { return { @@ -118,7 +152,11 @@ export class CollectionService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/collection/sync/sync.service.ts b/packages/api/src/ticketing/collection/sync/sync.service.ts index 84b70ddb8..c591cfd25 100644 --- a/packages/api/src/ticketing/collection/sync/sync.service.ts +++ b/packages/api/src/ticketing/collection/sync/sync.service.ts @@ -237,7 +237,7 @@ export class SyncService implements OnModuleInit { name: collection.name, description: collection.description, collection_type: collection.collection_type, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index 3127d2ba1..cc8e0b760 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -16,6 +18,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { CommentService } from './services/comment.service'; import { @@ -25,7 +28,9 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiCustomResponse } from '@@core/utils/types'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/comments') @Controller('ticketing/comments') export class CommentController { @@ -48,29 +53,26 @@ export class CommentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedCommentOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getComments( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.commentService.getComments( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index cd7d41984..fd34b2f5e 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCommentInput, @@ -176,13 +176,13 @@ export class CommentService { const opts = target_comment.creator_type === 'contact' ? { - id_tcg_contact: unifiedCommentData.contact_id, - } + id_tcg_contact: unifiedCommentData.contact_id, + } : target_comment.creator_type === 'user' - ? { + ? { id_tcg_user: unifiedCommentData.user_id, } - : {}; //case where nothing is passed for creator or a not authorized value; + : {}; //case where nothing is passed for creator or a not authorized value; if (existingComment) { // Update the existing comment @@ -373,16 +373,50 @@ export class CommentService { async getComments( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCommentOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const comments = await this.prisma.tcg_comments.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_comments.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_comment: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let comments = await this.prisma.tcg_comments.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_comment: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (comments.length === (pageSize + 1)) { + next_cursor = Buffer.from(comments[comments.length - 1].id_tcg_comment).toString('base64'); + comments.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedComments: UnifiedCommentOutput[] = await Promise.all( comments.map(async (comment) => { //WE SHOULDNT HAVE FIELD MAPPINGS FOR COMMENT @@ -455,7 +489,11 @@ export class CommentService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 98f13c20a..a1ed3423c 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -350,7 +350,7 @@ export class SyncService implements OnModuleInit { file_name: attchmt.file_name, file_url: attchmt.file_url, id_tcg_comment: unique_ticketing_comment_id, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), uploader: linkedUserId, //TODO id_tcg_ticket: id_ticket, diff --git a/packages/api/src/ticketing/contact/contact.controller.ts b/packages/api/src/ticketing/contact/contact.controller.ts index 7de05fe31..a6ff6d586 100644 --- a/packages/api/src/ticketing/contact/contact.controller.ts +++ b/packages/api/src/ticketing/contact/contact.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiHeader, ApiTags, + ApiBearerAuth, } from '@nestjs/swagger'; import { ContactService } from './services/contact.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedContactOutput } from './types/model.unified'; import { ApiCustomResponse } from '@@core/utils/types'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/contacts') @Controller('ticketing/contacts') export class ContactController { @@ -42,29 +47,26 @@ export class ContactController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedContactOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getContacts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.contactService.getContacts( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index 345bd59ab..907b3d415 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedContactOutput } from '../types/model.unified'; @Injectable() @@ -81,17 +81,51 @@ export class ContactService { async getContacts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedContactOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const contacts = await this.prisma.tcg_contacts.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_contacts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_contact: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let contacts = await this.prisma.tcg_contacts.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_contact: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (contacts.length === (pageSize + 1)) { + next_cursor = Buffer.from(contacts[contacts.length - 1].id_tcg_contact).toString('base64'); + contacts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( contacts.map(async (contact) => { // Fetch field mappings for the contact @@ -161,7 +195,11 @@ export class ContactService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 937cb48bf..c2a9fbe71 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -278,7 +278,7 @@ export class SyncService implements OnModuleInit { email_address: contact.email_address, phone_number: contact.phone_number, details: contact.details, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index e30eea4e6..70af5063d 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedTagOutput } from '../types/model.unified'; @Injectable() @@ -78,17 +78,52 @@ export class TagService { async getTags( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTagOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const tags = await this.prisma.tcg_tags.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_tags.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_tag: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tags = await this.prisma.tcg_tags.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_tag: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (tags.length === (pageSize + 1)) { + next_cursor = Buffer.from(tags[tags.length - 1].id_tcg_tag).toString('base64'); + tags.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTags: UnifiedTagOutput[] = await Promise.all( tags.map(async (tag) => { // Fetch field mappings for the tag @@ -155,7 +190,11 @@ export class TagService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index be264f495..5fc9bc83a 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -263,7 +263,7 @@ export class SyncService implements OnModuleInit { const data = { id_tcg_tag: uuidv4(), name: tag.name, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_tcg_ticket: id_ticket, id_linked_user: linkedUserId, diff --git a/packages/api/src/ticketing/tag/tag.controller.ts b/packages/api/src/ticketing/tag/tag.controller.ts index 30a84d857..6e133146c 100644 --- a/packages/api/src/ticketing/tag/tag.controller.ts +++ b/packages/api/src/ticketing/tag/tag.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TagService } from './services/tag.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedTagOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/tags') @Controller('ticketing/tags') export class TagController { @@ -42,26 +47,27 @@ export class TagController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTagOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTags( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.tagService.getTags(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.tagService.getTags( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index ee1112b38..faefa11a5 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedTeamOutput } from '../types/model.unified'; @Injectable() @@ -79,18 +79,52 @@ export class TeamService { async getTeams( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTeamOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const teams = await this.prisma.tcg_teams.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_teams.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_team: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let teams = await this.prisma.tcg_teams.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_team: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (teams.length === (pageSize + 1)) { + next_cursor = Buffer.from(teams[teams.length - 1].id_tcg_team).toString('base64'); + teams.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTeams: UnifiedTeamOutput[] = await Promise.all( teams.map(async (team) => { // Fetch field mappings for the team @@ -158,7 +192,11 @@ export class TeamService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index 024da3505..cc82edac2 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -247,7 +247,7 @@ export class SyncService implements OnModuleInit { id_tcg_team: uuidv4(), name: team.name, description: team.description, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/team/team.controller.ts b/packages/api/src/ticketing/team/team.controller.ts index 5131451ac..0d2eb2626 100644 --- a/packages/api/src/ticketing/team/team.controller.ts +++ b/packages/api/src/ticketing/team/team.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TeamService } from './services/team.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedTeamOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/teams') @Controller('ticketing/teams') export class TeamController { @@ -42,26 +47,27 @@ export class TeamController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTeamOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTeams( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.teamService.getTeams(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.teamService.getTeams( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index 459476de7..9c90f862e 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedTicketInput, @@ -412,11 +412,37 @@ export class TicketService { async getTickets( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTicketOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const tickets = await this.prisma.tcg_tickets.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_tickets.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_ticket: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tickets = await this.prisma.tcg_tickets.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_ticket: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -427,6 +453,15 @@ export class TicketService { },*/ }); + if (tickets.length === (pageSize + 1)) { + next_cursor = Buffer.from(tickets[tickets.length - 1].id_tcg_ticket).toString('base64'); + tickets.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTickets: UnifiedTicketOutput[] = await Promise.all( tickets.map(async (ticket) => { // Fetch field mappings for the ticket @@ -506,7 +541,11 @@ export class TicketService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 6cac7cf2a..fdb96ad16 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -271,7 +271,7 @@ export class SyncService implements OnModuleInit { let data: any = { id_tcg_ticket: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/ticket/ticket.controller.ts b/packages/api/src/ticketing/ticket/ticket.controller.ts index aff73ebcb..a47199c9c 100644 --- a/packages/api/src/ticketing/ticket/ticket.controller.ts +++ b/packages/api/src/ticketing/ticket/ticket.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,13 +19,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TicketService } from './services/ticket.service'; import { UnifiedTicketInput, UnifiedTicketOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/tickets') @Controller('ticketing/tickets') export class TicketController { @@ -46,29 +51,26 @@ export class TicketController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTicketOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTickets( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.ticketService.getTickets( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index 329643462..8a4493c93 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedUserOutput } from '../types/model.unified'; @Injectable() @@ -80,17 +80,52 @@ export class UserService { async getUsers( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedUserOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const users = await this.prisma.tcg_users.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_users.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_user: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let users = await this.prisma.tcg_users.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_user: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (users.length === (pageSize + 1)) { + next_cursor = Buffer.from(users[users.length - 1].id_tcg_user).toString('base64'); + users.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedUsers: UnifiedUserOutput[] = await Promise.all( users.map(async (user) => { // Fetch field mappings for the user @@ -160,7 +195,11 @@ export class UserService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index 06129515d..c017a83ba 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -251,7 +251,7 @@ export class SyncService implements OnModuleInit { name: user.name, email_address: user.email_address, teams: user.teams || [], - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, // id_tcg_account: user.account_id || '', diff --git a/packages/api/src/ticketing/user/user.controller.ts b/packages/api/src/ticketing/user/user.controller.ts index ea4c4132c..089b28dd9 100644 --- a/packages/api/src/ticketing/user/user.controller.ts +++ b/packages/api/src/ticketing/user/user.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { UserService } from './services/user.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedUserOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/users') @Controller('ticketing/users') export class UserController { @@ -42,26 +47,27 @@ export class UserController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedUserOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getUsers( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.userService.getUsers(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.userService.getUsers( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 655429fb8..c90e00f0b 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -2735,10 +2735,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2766,6 +2785,11 @@ }, "tags": [ "ticketing/accounts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2819,6 +2843,11 @@ }, "tags": [ "ticketing/accounts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2840,10 +2869,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2871,6 +2919,11 @@ }, "tags": [ "ticketing/collections" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2924,6 +2977,11 @@ }, "tags": [ "ticketing/collections" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2945,10 +3003,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2976,6 +3053,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -3047,6 +3129,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3100,6 +3187,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3178,6 +3270,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3199,10 +3296,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3230,6 +3346,11 @@ }, "tags": [ "ticketing/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3283,6 +3404,11 @@ }, "tags": [ "ticketing/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3304,10 +3430,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3335,6 +3480,11 @@ }, "tags": [ "ticketing/tags" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3388,6 +3538,11 @@ }, "tags": [ "ticketing/tags" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3409,10 +3564,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3440,6 +3614,11 @@ }, "tags": [ "ticketing/teams" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3493,6 +3672,11 @@ }, "tags": [ "ticketing/teams" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3514,10 +3698,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3545,6 +3748,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -3616,6 +3824,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -3645,6 +3858,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3698,6 +3916,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3776,6 +3999,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3797,10 +4025,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3828,6 +4075,11 @@ }, "tags": [ "ticketing/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3881,6 +4133,11 @@ }, "tags": [ "ticketing/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4727,10 +4984,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -4758,6 +5034,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -4829,6 +5110,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4882,6 +5168,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4935,6 +5226,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -5013,6 +5309,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }