diff --git a/api/src/dtos/listings/listings-query-body.dto.ts b/api/src/dtos/listings/listings-query-body.dto.ts index 393b320f5f..ae5ac165e1 100644 --- a/api/src/dtos/listings/listings-query-body.dto.ts +++ b/api/src/dtos/listings/listings-query-body.dto.ts @@ -54,7 +54,6 @@ export class ListingsQueryBody extends PaginationAllowsAllQueryParams { }) @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) - @Type(() => ListingFilterParams) @IsEnum(ListingOrderByKeys, { groups: [ValidationsGroupsEnum.default], each: true, diff --git a/api/src/dtos/listings/listings-query-params.dto.ts b/api/src/dtos/listings/listings-query-params.dto.ts index 5801b2c448..8c3cb43f90 100644 --- a/api/src/dtos/listings/listings-query-params.dto.ts +++ b/api/src/dtos/listings/listings-query-params.dto.ts @@ -56,7 +56,6 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { }) @IsArray({ groups: [ValidationsGroupsEnum.default] }) @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) - @Type(() => ListingFilterParams) @IsEnum(ListingOrderByKeys, { groups: [ValidationsGroupsEnum.default], each: true, diff --git a/api/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts b/api/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts index 6fe92ae659..c36982b817 100644 --- a/api/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts +++ b/api/src/dtos/multiselect-questions/multiselect-question-filter-params.dto.ts @@ -1,25 +1,37 @@ -import { BaseFilter } from '../shared/base-filter.dto'; import { Expose } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + MultiselectQuestionsApplicationSectionEnum, + MultiselectQuestionsStatusEnum, +} from '@prisma/client'; +import { BaseFilter } from '../shared/base-filter.dto'; import { MultiselectQuestionFilterKeys } from '../../enums/multiselect-questions/filter-key-enum'; -import { MultiselectQuestionsApplicationSectionEnum } from '@prisma/client'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; export class MultiselectQuestionFilterParams extends BaseFilter { @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional({ - example: 'uuid', + enum: MultiselectQuestionsApplicationSectionEnum, + enumName: 'MultiselectQuestionsApplicationSectionEnum', + example: 'preferences', }) + [MultiselectQuestionFilterKeys.applicationSection]?: MultiselectQuestionsApplicationSectionEnum; + + @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiPropertyOptional({ + example: 'uuid', + }) [MultiselectQuestionFilterKeys.jurisdiction]?: string; @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional({ - enum: MultiselectQuestionsApplicationSectionEnum, - enumName: 'MultiselectQuestionsApplicationSectionEnum', - example: 'preferences', + enum: MultiselectQuestionsStatusEnum, + enumName: 'MultiselectQuestionsStatusEnum', + example: 'active', }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - [MultiselectQuestionFilterKeys.applicationSection]?: MultiselectQuestionsApplicationSectionEnum; + [MultiselectQuestionFilterKeys.status]?: MultiselectQuestionsStatusEnum; } diff --git a/api/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts b/api/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts index 32f4877a17..05d41e59bc 100644 --- a/api/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts +++ b/api/src/dtos/multiselect-questions/multiselect-question-query-params.dto.ts @@ -1,10 +1,23 @@ -import { Expose, Type } from 'class-transformer'; +import { Expose, Transform, Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsArray, + IsEnum, + IsString, + Validate, + ValidateNested, +} from 'class-validator'; import { ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { MultiselectQuestionFilterParams } from './multiselect-question-filter-params.dto'; -import { ArrayMaxSize, IsArray, ValidateNested } from 'class-validator'; +import { PaginationAllowsAllQueryParams } from '../shared/pagination.dto'; +import { SearchStringLengthCheck } from '../../decorators/search-string-length-check.decorator'; +import { MultiselectQuestionOrderByKeys } from '../../enums/multiselect-questions/order-by-enum'; +import { MultiselectQuestionViews } from '../../enums/multiselect-questions/view-enum'; +import { OrderByEnum } from '../../enums/shared/order-by-enum'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; +import { OrderQueryParamValidator } from '../../utilities/order-by-validator'; -export class MultiselectQuestionQueryParams { +export class MultiselectQuestionQueryParams extends PaginationAllowsAllQueryParams { @Expose() @ApiPropertyOptional({ type: [String], @@ -18,4 +31,65 @@ export class MultiselectQuestionQueryParams { @Type(() => MultiselectQuestionFilterParams) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) filter?: MultiselectQuestionFilterParams[]; + + @Expose() + @ApiPropertyOptional({ + enum: MultiselectQuestionOrderByKeys, + enumName: 'MultiselectQuestionOrderByKeys', + example: ['status'], + default: ['status'], + isArray: true, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @IsEnum(MultiselectQuestionOrderByKeys, { + groups: [ValidationsGroupsEnum.default], + each: true, + }) + @Validate(OrderQueryParamValidator, { + groups: [ValidationsGroupsEnum.default], + }) + orderBy?: MultiselectQuestionOrderByKeys[]; + + @Expose() + @ApiPropertyOptional({ + enum: OrderByEnum, + enumName: 'OrderByEnum', + example: ['desc'], + default: ['desc'], + isArray: true, + }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Transform(({ value }) => { + return value ? value.map((val) => val.toLowerCase()) : undefined; + }) + @IsEnum(OrderByEnum, { groups: [ValidationsGroupsEnum.default], each: true }) + @Validate(OrderQueryParamValidator, { + groups: [ValidationsGroupsEnum.default], + }) + orderDir?: OrderByEnum[]; + + @Expose() + @ApiPropertyOptional({ + type: String, + example: 'search', + }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @SearchStringLengthCheck('search', { + message: 'Search must be at least 3 characters', + groups: [ValidationsGroupsEnum.default], + }) + search?: string; + + @Expose() + @ApiPropertyOptional({ + enum: MultiselectQuestionViews, + enumName: 'MultiselectQuestionViews', + example: 'base', + }) + @IsEnum(MultiselectQuestionViews, { + groups: [ValidationsGroupsEnum.default], + }) + view?: MultiselectQuestionViews; } diff --git a/api/src/enums/multiselect-questions/filter-key-enum.ts b/api/src/enums/multiselect-questions/filter-key-enum.ts index 398269f1eb..a5ef8a9de6 100644 --- a/api/src/enums/multiselect-questions/filter-key-enum.ts +++ b/api/src/enums/multiselect-questions/filter-key-enum.ts @@ -1,4 +1,5 @@ export enum MultiselectQuestionFilterKeys { - jurisdiction = 'jurisdiction', applicationSection = 'applicationSection', + jurisdiction = 'jurisdiction', + status = 'status', } diff --git a/api/src/enums/multiselect-questions/order-by-enum.ts b/api/src/enums/multiselect-questions/order-by-enum.ts new file mode 100644 index 0000000000..934a32be57 --- /dev/null +++ b/api/src/enums/multiselect-questions/order-by-enum.ts @@ -0,0 +1,6 @@ +export enum MultiselectQuestionOrderByKeys { + jurisdiction = 'jurisdiction', + name = 'name', + status = 'status', + updatedAt = 'updatedAt', +} diff --git a/api/src/services/multiselect-question.service.ts b/api/src/services/multiselect-question.service.ts index 15be2471bc..fbc102bce7 100644 --- a/api/src/services/multiselect-question.service.ts +++ b/api/src/services/multiselect-question.service.ts @@ -26,8 +26,10 @@ import { MultiselectQuestionFilterKeys } from '../enums/multiselect-questions/fi import { MultiselectQuestionViews } from '../enums/multiselect-questions/view-enum'; import { permissionActions } from '../enums/permissions/permission-actions-enum'; import { buildFilter } from '../utilities/build-filter'; +import { buildOrderByForMultiselectQuestions } from '../utilities/build-order-by'; import { doJurisdictionHaveFeatureFlagSet } from '../utilities/feature-flag-utilities'; import { mapTo } from '../utilities/mapTo'; +import { calculateSkip, calculateTake } from '../utilities/pagination-helpers'; export const includeViews: Partial< Record @@ -74,19 +76,47 @@ export class MultiselectQuestionService { async list( params: MultiselectQuestionQueryParams, ): Promise { - let rawMultiselectQuestions = + const whereClause = this.buildWhere(params); + + const count = await this.prisma.multiselectQuestions.count({ + where: whereClause, + }); + + // if passed in page and limit would result in no results because there aren't that many + // multiselectQuestions revert back to the first page + let page = params.page; + if (count && params.limit && params.limit !== 'all' && params.page > 1) { + if (Math.ceil(count / params.limit) < params.page) { + page = 1; + } + } + + const query = { + skip: calculateSkip(params.limit, page), + take: calculateTake(params.limit), + orderBy: buildOrderByForMultiselectQuestions( + params.orderBy, + params.orderDir, + ), + where: whereClause, + }; + + const rawMultiselectQuestions = await this.prisma.multiselectQuestions.findMany({ - include: includeViews.fundamentals, - where: this.buildWhere(params), + ...query, + include: includeViews[params.view ?? 'fundamentals'], }); - rawMultiselectQuestions = rawMultiselectQuestions.map((msq) => { - return { - ...msq, - jurisdictions: [msq.jurisdiction], - }; - }); - return mapTo(MultiselectQuestion, rawMultiselectQuestions); + // TODO: Can be removed after MSQ refactor + const multiselectQuestionsWithJurisdictions = rawMultiselectQuestions.map( + (msq) => { + return { + ...msq, + jurisdictions: [msq.jurisdiction], + }; + }, + ); + return mapTo(MultiselectQuestion, multiselectQuestionsWithJurisdictions); } /* @@ -96,42 +126,63 @@ export class MultiselectQuestionService { params: MultiselectQuestionQueryParams, ): Prisma.MultiselectQuestionsWhereInput { const filters: Prisma.MultiselectQuestionsWhereInput[] = []; - if (!params?.filter?.length) { - return { - AND: filters, - }; + if (params?.filter?.length) { + params.filter.forEach((filter) => { + if (filter[MultiselectQuestionFilterKeys.applicationSection]) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[MultiselectQuestionFilterKeys.applicationSection], + key: MultiselectQuestionFilterKeys.applicationSection, + caseSensitive: true, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + applicationSection: filt, + })), + }); + } else if (filter[MultiselectQuestionFilterKeys.jurisdiction]) { + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[MultiselectQuestionFilterKeys.jurisdiction], + key: MultiselectQuestionFilterKeys.jurisdiction, + caseSensitive: true, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + jurisdiction: { + id: filt, + }, + })), + }); + } else if (filter[MultiselectQuestionFilterKeys.status]) { + console.log(filter[MultiselectQuestionFilterKeys.status]); + const builtFilter = buildFilter({ + $comparison: filter.$comparison, + $include_nulls: false, + value: filter[MultiselectQuestionFilterKeys.status], + key: MultiselectQuestionFilterKeys.status, + caseSensitive: true, + }); + filters.push({ + OR: builtFilter.map((filt) => ({ + status: filt, + })), + }); + } + }); } - params.filter.forEach((filter) => { - if (filter[MultiselectQuestionFilterKeys.jurisdiction]) { - const builtFilter = buildFilter({ - $comparison: filter.$comparison, - $include_nulls: false, - value: filter[MultiselectQuestionFilterKeys.jurisdiction], - key: MultiselectQuestionFilterKeys.jurisdiction, - caseSensitive: true, - }); - filters.push({ - OR: builtFilter.map((filt) => ({ - jurisdiction: { - id: filt, - }, - })), - }); - } else if (filter[MultiselectQuestionFilterKeys.applicationSection]) { - const builtFilter = buildFilter({ - $comparison: filter.$comparison, - $include_nulls: false, - value: filter[MultiselectQuestionFilterKeys.applicationSection], - key: MultiselectQuestionFilterKeys.applicationSection, - caseSensitive: true, - }); - filters.push({ - OR: builtFilter.map((filt) => ({ - applicationSection: filt, - })), - }); - } - }); + + if (params?.search) { + filters.push({ + name: { + contains: params.search, + mode: Prisma.QueryMode.insensitive, + }, + }); + } + return { AND: filters, }; diff --git a/api/src/utilities/build-order-by.ts b/api/src/utilities/build-order-by.ts index b301a41a12..de0ea6ce6a 100644 --- a/api/src/utilities/build-order-by.ts +++ b/api/src/utilities/build-order-by.ts @@ -1,7 +1,48 @@ +import { Prisma } from '@prisma/client'; import { ApplicationOrderByKeys } from '../enums/applications/order-by-enum'; import { ListingOrderByKeys } from '../enums/listings/order-by-enum'; +import { MultiselectQuestionOrderByKeys } from '../enums/multiselect-questions/order-by-enum'; import { OrderByEnum } from '../enums/shared/order-by-enum'; -import { Prisma } from '@prisma/client'; + +/* + This constructs the "orderBy" part of a prisma query + We are guaranteed to have the same length for both the orderBy and orderDir arrays +*/ +export const buildOrderBy = (orderBy?: string[], orderDir?: OrderByEnum[]) => { + if (!orderBy?.length) { + return undefined; + } + return orderBy.map((param, index) => ({ + [param]: orderDir[index], + })); +}; + +/* + Constructs the "orderBy" part of the prisma query and maps the values to + the appropriate application field +*/ +export const buildOrderByForApplications = ( + orderBy?: string[], + orderDir?: OrderByEnum[], +): Prisma.ApplicationsOrderByWithRelationInput[] => { + if (!orderBy?.length || orderBy.length !== orderDir?.length) { + return undefined; + } + + return orderBy.map((param, index) => { + switch (param) { + case ApplicationOrderByKeys.firstName: + return { applicant: { firstName: orderDir[index] } }; + case ApplicationOrderByKeys.lastName: + return { applicant: { lastName: orderDir[index] } }; + case ApplicationOrderByKeys.createdAt: + return { createdAt: orderDir[index] }; + case ApplicationOrderByKeys.submissionDate: + case undefined: + return { submissionDate: orderDir[index] }; + } + }) as Prisma.ApplicationsOrderByWithRelationInput[]; +}; /* Constructs the "orderBy" part of the prisma query and maps the values to @@ -57,40 +98,30 @@ export const buildOrderByForListings = ( /* Constructs the "orderBy" part of the prisma query and maps the values to - the appropriate application field + the appropriate multiselectQuestion field */ -export const buildOrderByForApplications = ( +export const buildOrderByForMultiselectQuestions = ( orderBy?: string[], orderDir?: OrderByEnum[], -): Prisma.ApplicationsOrderByWithRelationInput[] => { +): Prisma.MultiselectQuestionsOrderByWithRelationInput[] => { if (!orderBy?.length || orderBy.length !== orderDir?.length) { return undefined; } + orderBy.push(ListingOrderByKeys.name); + orderDir.push(OrderByEnum.ASC); + return orderBy.map((param, index) => { switch (param) { - case ApplicationOrderByKeys.firstName: - return { applicant: { firstName: orderDir[index] } }; - case ApplicationOrderByKeys.lastName: - return { applicant: { lastName: orderDir[index] } }; - case ApplicationOrderByKeys.createdAt: - return { createdAt: orderDir[index] }; - case ApplicationOrderByKeys.submissionDate: + case MultiselectQuestionOrderByKeys.jurisdiction: + return { jurisdiction: { name: orderDir[index] } }; + case MultiselectQuestionOrderByKeys.status: + return { status: orderDir[index] }; + case MultiselectQuestionOrderByKeys.updatedAt: + return { updatedAt: orderDir[index] }; + case ListingOrderByKeys.name: case undefined: - return { submissionDate: orderDir[index] }; + return { name: orderDir[index] }; } - }) as Prisma.ApplicationsOrderByWithRelationInput[]; -}; - -/* - This constructs the "orderBy" part of a prisma query - We are guaranteed to have the same length for both the orderBy and orderDir arrays -*/ -export const buildOrderBy = (orderBy?: string[], orderDir?: OrderByEnum[]) => { - if (!orderBy?.length) { - return undefined; - } - return orderBy.map((param, index) => ({ - [param]: orderDir[index], - })); + }) as Prisma.MultiselectQuestionsOrderByWithRelationInput[]; }; diff --git a/api/test/integration/multiselect-question.e2e-spec.ts b/api/test/integration/multiselect-question.e2e-spec.ts index 2b37fd3f4b..3c02aea23c 100644 --- a/api/test/integration/multiselect-question.e2e-spec.ts +++ b/api/test/integration/multiselect-question.e2e-spec.ts @@ -80,23 +80,20 @@ describe('MultiselectQuestion Controller Tests', () => { data: jurisdictionFactory(), }); - const multiselectQuestion = await prisma.multiselectQuestions.create({ + await prisma.multiselectQuestions.create({ data: multiselectQuestionFactory(jurisdictionB.id), }); - const multiselectQuestionB = await prisma.multiselectQuestions.create({ + await prisma.multiselectQuestions.create({ data: multiselectQuestionFactory(jurisdictionB.id), }); const res = await request(app.getHttpServer()) - .get(`/multiselectQuestions?`) + .get(`/multiselectQuestions`) .set({ passkey: process.env.API_PASS_KEY || '' }) .set('Cookie', cookies) .expect(200); expect(res.body.length).toBeGreaterThanOrEqual(2); - const multiselectQuestions = res.body.map((value) => value.text); - expect(multiselectQuestions).toContain(multiselectQuestion.text); - expect(multiselectQuestions).toContain(multiselectQuestionB.text); }); it('should get multiselect questions from list endpoint when params are sent', async () => { @@ -420,23 +417,20 @@ describe('MultiselectQuestion Controller Tests', () => { }), }); - const multiselectQuestionA = await prisma.multiselectQuestions.create({ + await prisma.multiselectQuestions.create({ data: multiselectQuestionFactory(jurisdictionB.id, {}, true), }); - const multiselectQuestionB = await prisma.multiselectQuestions.create({ + await prisma.multiselectQuestions.create({ data: multiselectQuestionFactory(jurisdictionB.id, {}, true), }); const res = await request(app.getHttpServer()) - .get(`/multiselectQuestions?`) + .get(`/multiselectQuestions`) .set({ passkey: process.env.API_PASS_KEY || '' }) .set('Cookie', cookies) .expect(200); expect(res.body.length).toBeGreaterThanOrEqual(2); - const multiselectQuestions = res.body.map((value) => value.name); - expect(multiselectQuestions).toContain(multiselectQuestionA.name); - expect(multiselectQuestions).toContain(multiselectQuestionB.name); }); it('should get multiselect questions from list endpoint when params are sent', async () => { diff --git a/api/test/unit/services/multiselect-question.service.spec.ts b/api/test/unit/services/multiselect-question.service.spec.ts index 6d0701606f..4e27fd8939 100644 --- a/api/test/unit/services/multiselect-question.service.spec.ts +++ b/api/test/unit/services/multiselect-question.service.spec.ts @@ -9,11 +9,15 @@ import { } from '@prisma/client'; import MultiselectQuestion from '../../../src/dtos/multiselect-questions/multiselect-question.dto'; import { MultiselectQuestionCreate } from '../../../src/dtos/multiselect-questions/multiselect-question-create.dto'; +import { MultiselectQuestionFilterParams } from '../../../src/dtos/multiselect-questions/multiselect-question-filter-params.dto'; import { MultiselectQuestionQueryParams } from '../../../src/dtos/multiselect-questions/multiselect-question-query-params.dto'; import { MultiselectQuestionUpdate } from '../../../src/dtos/multiselect-questions/multiselect-question-update.dto'; import { Compare } from '../../../src/dtos/shared/base-filter.dto'; import { User } from '../../../src/dtos/users/user.dto'; import { FeatureFlagEnum } from '../../../src/enums/feature-flags/feature-flags-enum'; +import { MultiselectQuestionOrderByKeys } from '../../../src/enums/multiselect-questions/order-by-enum'; +import { MultiselectQuestionViews } from '../../../src/enums/multiselect-questions/view-enum'; +import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; import { MultiselectQuestionService } from '../../../src/services/multiselect-question.service'; import { PermissionService } from '../../../src/services/permission.service'; import { PrismaService } from '../../../src/services/prisma.service'; @@ -95,6 +99,7 @@ describe('Testing multiselect question service', () => { it('should get records with empty param call to list()', async () => { const date = new Date(); const mockedValue = mockMultiselectQuestionSet(3, date); + prisma.multiselectQuestions.count = jest.fn().mockResolvedValue(3); prisma.multiselectQuestions.findMany = jest .fn() .mockResolvedValue(mockedValue); @@ -200,20 +205,29 @@ describe('Testing multiselect question service', () => { jurisdiction: true, multiselectOptions: true, }, + skip: 0, where: { AND: [], }, }); }); - it('should get records with paramaterized call to list()', async () => { + it('should get records with parameterized call to list()', async () => { const date = new Date(); const mockedValue = mockMultiselectQuestionSet(3, date); + prisma.multiselectQuestions.count = jest.fn().mockResolvedValue(3); prisma.multiselectQuestions.findMany = jest .fn() .mockResolvedValue(mockedValue); const params: MultiselectQuestionQueryParams = { + view: MultiselectQuestionViews.base, + page: 1, + limit: 10, + orderBy: [MultiselectQuestionOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + // Because enableMSQV2 is off + search: 'text', filter: [ { $comparison: Compare['='], @@ -322,8 +336,19 @@ describe('Testing multiselect question service', () => { expect(prisma.multiselectQuestions.findMany).toHaveBeenCalledWith({ include: { jurisdiction: true, + listings: true, multiselectOptions: true, }, + orderBy: [ + { + name: 'asc', + }, + { + name: 'asc', + }, + ], + skip: 0, + take: 10, where: { AND: [ { @@ -335,10 +360,130 @@ describe('Testing multiselect question service', () => { }, ], }, + { + name: { + contains: 'text', + mode: 'insensitive', + }, + }, ], }, }); }); + + it('should return first page if params are more than count', async () => { + const date = new Date(); + const mockedValue = mockMultiselectQuestionSet(5, date); + prisma.multiselectQuestions.count = jest.fn().mockResolvedValue(5); + prisma.multiselectQuestions.findMany = jest + .fn() + .mockResolvedValue(mockedValue); + + const params: MultiselectQuestionQueryParams = { + view: MultiselectQuestionViews.base, + page: 1, + limit: 3, + orderBy: [MultiselectQuestionOrderByKeys.name], + orderDir: [OrderByEnum.ASC], + }; + + await service.list(params); + + expect(prisma.multiselectQuestions.findMany).toHaveBeenCalledWith({ + include: { + jurisdiction: true, + listings: true, + multiselectOptions: true, + }, + orderBy: [ + { + name: 'asc', + }, + { + name: 'asc', + }, + ], + skip: 0, + take: 3, + where: { + AND: [], + }, + }); + + expect(prisma.multiselectQuestions.count).toHaveBeenCalledWith({ + where: { + AND: [], + }, + }); + }); + }); + + describe('buildWhere', () => { + it('should return a where clause for filter jurisdiction', () => { + const jurisdictionId = randomUUID(); + const filter = [ + { + $comparison: '=', + jurisdiction: jurisdictionId, + } as MultiselectQuestionFilterParams, + ]; + const whereClause = service.buildWhere({ filter: filter }); + + expect(whereClause).toStrictEqual({ + AND: [ + { + OR: [ + { + jurisdiction: { + id: { + equals: jurisdictionId, + }, + }, + }, + ], + }, + ], + }); + }); + + it('should return a where clause for filter status', () => { + const status = ListingsStatusEnum.active; + const filter = [ + { $comparison: '=', status: status } as MultiselectQuestionFilterParams, + ]; + const whereClause = service.buildWhere({ filter: filter }); + + expect(whereClause).toStrictEqual({ + AND: [ + { + OR: [ + { + status: { + equals: status, + }, + }, + ], + }, + ], + }); + }); + + it('should return a where clause for search', () => { + const search = 'searchName'; + + const whereClause = service.buildWhere({ search: search }); + + expect(whereClause).toStrictEqual({ + AND: [ + { + name: { + contains: search, + mode: 'insensitive', + }, + }, + ], + }); + }); }); describe('findOne', () => { diff --git a/api/test/unit/utilities/build-order-by.spec.ts b/api/test/unit/utilities/build-order-by.spec.ts index 8904e97fb0..e6cdb191c5 100644 --- a/api/test/unit/utilities/build-order-by.spec.ts +++ b/api/test/unit/utilities/build-order-by.spec.ts @@ -1,115 +1,174 @@ import { ListingOrderByKeys } from '../../../src/enums/listings/order-by-enum'; +import { MultiselectQuestionOrderByKeys } from '../../../src/enums/multiselect-questions/order-by-enum'; import { OrderByEnum } from '../../../src/enums/shared/order-by-enum'; import { buildOrderBy, buildOrderByForListings, + buildOrderByForMultiselectQuestions, } from '../../../src/utilities/build-order-by'; -describe('Testing buildOrderByForListings', () => { - it('should return applicationDueDate in array when orderBy contains applicationDueDate', () => { +describe('Testing buildOrderBy', () => { + it('should return correctly mapped array when both arrays have the same length', () => { expect( - buildOrderByForListings( - [ListingOrderByKeys.applicationDates], - [OrderByEnum.ASC], - ), - ).toEqual([{ applicationDueDate: 'asc' }, { name: 'asc' }]); + buildOrderBy(['order1', 'order2'], [OrderByEnum.ASC, OrderByEnum.DESC]), + ).toEqual([{ order1: 'asc' }, { order2: 'desc' }]); }); - it('should return marketingType in array when orderBy contains marketingType', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.marketingType], - [OrderByEnum.ASC], - ), - ).toEqual([{ marketingType: 'asc' }, { name: 'asc' }]); + it('should return empty array when both arrays are empty', () => { + expect(buildOrderBy([], [])).toEqual(undefined); }); - it('should return closedAt in array when orderBy contains mostRecentlyClosed', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.mostRecentlyClosed], - [OrderByEnum.ASC], - ), - ).toEqual([{ closedAt: 'asc' }, { name: 'asc' }]); - }); + describe('Testing buildOrderByForListings', () => { + it('should return applicationDueDate in array when orderBy contains applicationDueDate', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.applicationDates], + [OrderByEnum.ASC], + ), + ).toEqual([{ applicationDueDate: 'asc' }, { name: 'asc' }]); + }); - it('should return publishedAt in array when orderBy contains mostRecentlyPublished', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.mostRecentlyPublished], - [OrderByEnum.ASC], - ), - ).toEqual([{ publishedAt: 'asc' }, { name: 'asc' }]); - }); + it('should return marketingType in array when orderBy contains marketingType', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.marketingType], + [OrderByEnum.ASC], + ), + ).toEqual([{ marketingType: 'asc' }, { name: 'asc' }]); + }); - it('should return updatedAt in array when orderBy contains mostRecentlyUpdated', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.mostRecentlyUpdated], - [OrderByEnum.ASC], - ), - ).toEqual([{ updatedAt: 'asc' }, { name: 'asc' }]); - }); + it('should return closedAt in array when orderBy contains mostRecentlyClosed', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.mostRecentlyClosed], + [OrderByEnum.ASC], + ), + ).toEqual([{ closedAt: 'asc' }, { name: 'asc' }]); + }); - it('should return name in array when orderBy contains name', () => { - expect( - buildOrderByForListings([ListingOrderByKeys.name], [OrderByEnum.ASC]), - ).toEqual([{ name: 'asc' }, { name: 'asc' }]); - }); + it('should return publishedAt in array when orderBy contains mostRecentlyPublished', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.mostRecentlyPublished], + [OrderByEnum.ASC], + ), + ).toEqual([{ publishedAt: 'asc' }, { name: 'asc' }]); + }); - it('should return marketingType in array when orderBy contains status', () => { - expect( - buildOrderByForListings([ListingOrderByKeys.status], [OrderByEnum.ASC]), - ).toEqual([{ status: 'asc' }, { name: 'asc' }]); - }); + it('should return updatedAt in array when orderBy contains mostRecentlyUpdated', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.mostRecentlyUpdated], + [OrderByEnum.ASC], + ), + ).toEqual([{ updatedAt: 'asc' }, { name: 'asc' }]); + }); - it('should return unitsAvailable in array when orderBy contains unitsAvailable', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.unitsAvailable], - [OrderByEnum.ASC], - ), - ).toEqual([{ unitsAvailable: 'asc' }, { name: 'asc' }]); - }); + it('should return name in array when orderBy contains name', () => { + expect( + buildOrderByForListings([ListingOrderByKeys.name], [OrderByEnum.ASC]), + ).toEqual([{ name: 'asc' }, { name: 'asc' }]); + }); - it('should return isWaitlistOpen in array when orderBy contains waitlistOpen', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.waitlistOpen], - [OrderByEnum.ASC], - ), - ).toEqual([{ isWaitlistOpen: 'asc' }, { name: 'asc' }]); - }); + it('should return marketingType in array when orderBy contains status', () => { + expect( + buildOrderByForListings([ListingOrderByKeys.status], [OrderByEnum.ASC]), + ).toEqual([{ status: 'asc' }, { name: 'asc' }]); + }); - it('should return undefined when orderBy contains a value that is not an enum', () => { - expect(buildOrderByForListings(['order1'], [OrderByEnum.ASC])).toEqual([ - undefined, - { name: 'asc' }, - ]); - }); + it('should return unitsAvailable in array when orderBy contains unitsAvailable', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.unitsAvailable], + [OrderByEnum.ASC], + ), + ).toEqual([{ unitsAvailable: 'asc' }, { name: 'asc' }]); + }); - it('should return undefined when arrays are of unequal length', () => { - expect( - buildOrderByForListings( - [ListingOrderByKeys.name], - [OrderByEnum.ASC, OrderByEnum.ASC], - ), - ).toEqual(undefined); - }); + it('should return isWaitlistOpen in array when orderBy contains waitlistOpen', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.waitlistOpen], + [OrderByEnum.ASC], + ), + ).toEqual([{ isWaitlistOpen: 'asc' }, { name: 'asc' }]); + }); - it('should return undefined when both arrays are empty', () => { - expect(buildOrderByForListings([], [])).toEqual(undefined); - }); -}); + it('should return undefined when orderBy contains a value that is not an enum', () => { + expect(buildOrderByForListings(['order1'], [OrderByEnum.ASC])).toEqual([ + undefined, + { name: 'asc' }, + ]); + }); -describe('Testing buildOrderBy', () => { - it('should return correctly mapped array when both arrays have the same length', () => { - expect( - buildOrderBy(['order1', 'order2'], [OrderByEnum.ASC, OrderByEnum.DESC]), - ).toEqual([{ order1: 'asc' }, { order2: 'desc' }]); + it('should return undefined when arrays are of unequal length', () => { + expect( + buildOrderByForListings( + [ListingOrderByKeys.name], + [OrderByEnum.ASC, OrderByEnum.ASC], + ), + ).toEqual(undefined); + }); + + it('should return undefined when both arrays are empty', () => { + expect(buildOrderByForListings([], [])).toEqual(undefined); + }); }); - it('should return empty array when both arrays are empty', () => { - expect(buildOrderBy([], [])).toEqual(undefined); + describe('Testing buildOrderByForMultiselectQuestions', () => { + it('should return name in array when orderBy contains name', () => { + expect( + buildOrderByForMultiselectQuestions( + [MultiselectQuestionOrderByKeys.name], + [OrderByEnum.ASC], + ), + ).toEqual([{ name: 'asc' }, { name: 'asc' }]); + }); + + it('should return status in array when orderBy contains status', () => { + expect( + buildOrderByForMultiselectQuestions( + [MultiselectQuestionOrderByKeys.status], + [OrderByEnum.ASC], + ), + ).toEqual([{ status: 'asc' }, { name: 'asc' }]); + }); + + it('should return jurisdiction in array when orderBy contains jurisdiction', () => { + expect( + buildOrderByForMultiselectQuestions( + [MultiselectQuestionOrderByKeys.jurisdiction], + [OrderByEnum.ASC], + ), + ).toEqual([{ jurisdiction: { name: 'asc' } }, { name: 'asc' }]); + }); + + it('should return updatedAt in array when orderBy contains updatedAt', () => { + expect( + buildOrderByForMultiselectQuestions( + [MultiselectQuestionOrderByKeys.updatedAt], + [OrderByEnum.ASC], + ), + ).toEqual([{ updatedAt: 'asc' }, { name: 'asc' }]); + }); + + it('should return undefined when orderBy contains a value that is not an enum', () => { + expect( + buildOrderByForMultiselectQuestions(['order1'], [OrderByEnum.ASC]), + ).toEqual([undefined, { name: 'asc' }]); + }); + + it('should return undefined when arrays are of unequal length', () => { + expect( + buildOrderByForMultiselectQuestions( + [ListingOrderByKeys.name], + [OrderByEnum.ASC, OrderByEnum.ASC], + ), + ).toEqual(undefined); + }); + + it('should return undefined when both arrays are empty', () => { + expect(buildOrderByForMultiselectQuestions([], [])).toEqual(undefined); + }); }); }); diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index ded40ad6b2..c39f72bd80 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -1273,8 +1273,20 @@ export class MultiselectQuestionsService { */ list( params: { + /** */ + page?: number + /** */ + limit?: number | "all" /** */ filter?: MultiselectQuestionFilterParams[] + /** */ + orderBy?: MultiselectQuestionOrderByKeys[] + /** */ + orderDir?: OrderByEnum[] + /** */ + search?: string + /** */ + view?: MultiselectQuestionViews } = {} as any, options: IRequestOptions = {} ): Promise { @@ -1282,7 +1294,15 @@ export class MultiselectQuestionsService { let url = basePath + "/multiselectQuestions" const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - configs.params = { filter: params["filter"] } + configs.params = { + page: params["page"], + limit: params["limit"], + filter: params["filter"], + orderBy: params["orderBy"], + orderDir: params["orderDir"], + search: params["search"], + view: params["view"], + } /** 适配ios13,get请求不允许带body */ @@ -6600,19 +6620,40 @@ export interface MultiselectQuestionUpdate { } export interface MultiselectQuestionQueryParams { + /** */ + page?: number + + /** */ + limit?: number | "all" + /** */ filter?: string[] + + /** */ + orderBy?: MultiselectQuestionOrderByKeys[] + + /** */ + orderDir?: OrderByEnum[] + + /** */ + search?: string + + /** */ + view?: MultiselectQuestionViews } export interface MultiselectQuestionFilterParams { /** */ $comparison: EnumMultiselectQuestionFilterParamsComparison + /** */ + applicationSection?: MultiselectQuestionsApplicationSectionEnum + /** */ jurisdiction?: string /** */ - applicationSection?: MultiselectQuestionsApplicationSectionEnum + status?: MultiselectQuestionsStatusEnum } export interface AddressInput { @@ -8065,6 +8106,18 @@ export enum FeatureFlagEnum { "hideCloseListingButton" = "hideCloseListingButton", "swapCommunityTypeWithPrograms" = "swapCommunityTypeWithPrograms", } + +export enum MultiselectQuestionOrderByKeys { + "jurisdiction" = "jurisdiction", + "name" = "name", + "status" = "status", + "updatedAt" = "updatedAt", +} + +export enum MultiselectQuestionViews { + "base" = "base", + "fundamentals" = "fundamentals", +} export enum EnumMultiselectQuestionFilterParamsComparison { "=" = "=", "<>" = "<>",