From a284d595ee4d5e91a7607e4c999c4a2814f99638 Mon Sep 17 00:00:00 2001 From: Tomasz Puch Date: Tue, 8 Nov 2022 07:58:46 +0100 Subject: [PATCH] feat: custom fields on server-side [CU-xhcmx-63] (#161) * feat: custom fields on server-side * feat: graphql for custom comments field --- jest.config.ts | 2 +- .../utils/__tests__/functions.test.js | 4 +- .../utils/__tests__/parsers.test.js | 68 ++++++++++++++++++- server/controllers/utils/parsers.ts | 28 +++++++- server/graphql/queries/findAllInHierarchy.ts | 4 ++ server/services/graphql.ts | 46 +++++++++++-- .../utils/__tests__/functions.test.js | 4 +- 7 files changed, 142 insertions(+), 14 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 03ac5333..dc1ae298 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,7 +3,7 @@ import { defaults as tsjPreset } from "ts-jest/presets"; const config: Config.InitialOptions = { name: "Unit test", - testMatch: ["**/__tests__/?(*.)+(spec|test).ts"], + testMatch: ["**/__tests__/?(*.)+(spec|test).(t|j)s"], transform: { ...tsjPreset.transform, }, diff --git a/server/controllers/utils/__tests__/functions.test.js b/server/controllers/utils/__tests__/functions.test.js index 3086fc64..7621b23f 100644 --- a/server/controllers/utils/__tests__/functions.test.js +++ b/server/controllers/utils/__tests__/functions.test.js @@ -8,13 +8,13 @@ describe("Test Comments controller functions utils", () => { describe("Errors serialization", () => { test("Should throw error PluginError", () => { try { - throw new PluginError(400, "Error message", { + throw new PluginError.default(400, "Error message", { content: { param: "Parameter value", }, }); } catch (e) { - expect(e).toBeInstanceOf(PluginError); + expect(e).toBeInstanceOf(PluginError.default); expect(e).toHaveProperty("status", 400); expect(e).toHaveProperty("name", "Strapi:Plugin:Comments"); expect(e).toHaveProperty("message", "Error message"); diff --git a/server/controllers/utils/__tests__/parsers.test.js b/server/controllers/utils/__tests__/parsers.test.js index e3b4173a..79e6aab2 100644 --- a/server/controllers/utils/__tests__/parsers.test.js +++ b/server/controllers/utils/__tests__/parsers.test.js @@ -20,14 +20,14 @@ describe("Test Comments controller parsers utils", () => { const fields = ["content"]; test("Should contain default property 'populate'", () => { - expect(flatInput()).toHaveProperty( + expect(flatInput({ query: {} })).toHaveProperty( ["populate", "threadOf", "populate", "authorUser"], true ); }); test("Should assign relation to 'query'", () => { - expect(flatInput({ relation })).toHaveProperty( + expect(flatInput({ relation, query: {} })).toHaveProperty( ["query", "related"], relation ); @@ -159,5 +159,69 @@ describe("Test Comments controller parsers utils", () => { pagination.pageSize ); }); + + test("Should assign filtering by comments status", () => { + const filterByValue = "APPROVED"; + const result = flatInput({ + relation, + query: { + ...query, + filterBy: "APPROVAL_STATUS", + filterByValue, + }, + sort, + fields, + }); + + expect(result).toHaveProperty(["query", "approvalStatus"], filterByValue); + expect(() => { + flatInput({ + relation, + query: { + filterBy: "APPROVAL_STATUS", + }, + sort, + fields, + }); + }).toThrow(); + }); + + test("Should assign filtering by creation date", () => { + const filterByValue = new Date().toUTCString(); + const result = flatInput({ + relation, + query: { + ...query, + filterBy: "DATE_CREATED", + filterByValue, + }, + sort, + fields, + }); + + expect(result).toHaveProperty(["query", "createdAt", "$between", 0]); + expect(result).toHaveProperty(["query", "createdAt", "$between", 1]); + expect(() => { + flatInput({ + relation, + query: { + filterBy: "DATE_CREATED", + }, + sort, + fields, + }); + }).toThrow(); + expect(() => { + flatInput({ + relation, + query: { + filterByValue: "X", + filterBy: "DATE_CREATED", + }, + sort, + fields, + }); + }).toThrow(); + }); }); }); diff --git a/server/controllers/utils/parsers.ts b/server/controllers/utils/parsers.ts index 1f2e9773..51679452 100644 --- a/server/controllers/utils/parsers.ts +++ b/server/controllers/utils/parsers.ts @@ -1,12 +1,14 @@ import { OnlyStrings } from "strapi-typed"; import { FlatInput, ToBeFixed } from "../../../types"; +import PluginError from "../../utils/error"; +import { assertNotEmpty } from "../../utils/functions"; export const flatInput = ( payload: FlatInput> ) => { const { relation, query, sort, pagination, fields } = payload; - const { populate = {}, ...restQuery } = query; + const { populate = {}, filterBy, filterByValue, ...restQuery } = query; const filters = restQuery?.filters || restQuery; const orOperator = (filters?.$or || []).filter( (_: ToBeFixed) => !Object.keys(_).includes("removed") @@ -42,6 +44,30 @@ export const flatInput = ( }; } + if (filterBy === "DATE_CREATED") { + const date = new Date(filterByValue); + + if (!filterByValue || Number.isNaN(+date)) { + throw new PluginError(400, 'Invalid date specified in "filterByValue"'); + } + + const start = date.setHours(0, 0, 0, 0); + const end = date.setHours(23, 59, 59, 999); + + filters.createdAt = { + $between: [start, end], + }; + } + + if (filterBy === "APPROVAL_STATUS") { + assertNotEmpty( + filterByValue, + new PluginError(400, 'Empty "filterByValue" parameter') + ); + + filters.approvalStatus = filterByValue; + } + return { query: { ...filters, diff --git a/server/graphql/queries/findAllInHierarchy.ts b/server/graphql/queries/findAllInHierarchy.ts index 8a98d5c9..a4b7be13 100644 --- a/server/graphql/queries/findAllInHierarchy.ts +++ b/server/graphql/queries/findAllInHierarchy.ts @@ -20,12 +20,16 @@ export = ({ strapi, nexus }: StrapiGraphQLContext) => { const { nonNull, list, stringArg } = nexus; const { service: getService } = strapi.plugin("graphql"); const { args } = getService("internals"); + const { + naming: { getFiltersInputTypeName }, + } = getService("utils"); return { type: nonNull(list("CommentNested")), args: { relation: nonNull(stringArg()), sort: args.SortArg, + filters: getFiltersInputTypeName(contentType), }, // @ts-ignore async resolve(obj: Object, args: findAllInHierarchyProps) { diff --git a/server/services/graphql.ts b/server/services/graphql.ts index faa7b990..56d7d1b1 100644 --- a/server/services/graphql.ts +++ b/server/services/graphql.ts @@ -3,8 +3,10 @@ import { IServiceGraphQL, ToBeFixed } from "../../types"; import { has, propEq, isNil, isDate, isObject } from "lodash/fp"; import { inputObjectType } from "nexus"; +import { assertNotEmpty } from "../utils/functions"; const virtualScalarAttributes = ["id"]; +const customFields = ["filterBy", "filterByValue"]; export = ({ strapi }: StrapiContext): IServiceGraphQL => { const { service: getService } = strapi.plugin("graphql"); @@ -65,6 +67,9 @@ export = ({ strapi }: StrapiContext): IServiceGraphQL => { for (const operator of rootLevelOperators()) { operator.add(t, filtersTypeName); } + + t.field("filterBy", { type: "String" }); + t.field("filterByValue", { type: "String" }); }, }); }; @@ -132,7 +137,7 @@ export = ({ strapi }: StrapiContext): IServiceGraphQL => { }; const graphQLFiltersToStrapiQuery = ( - filters: ToBeFixed, + queryFilters: ToBeFixed, contentType: ToBeFixed = {} ): Array | ToBeFixed => { const { isStrapiScalar, isMedia, isRelation } = @@ -142,18 +147,23 @@ export = ({ strapi }: StrapiContext): IServiceGraphQL => { const ROOT_LEVEL_OPERATORS = [operators.and, operators.or, operators.not]; // Handle unwanted scenario where there is no filters defined - if (isNil(filters)) { + if (isNil(queryFilters)) { return {}; } // If filters is a collection, then apply the transformation to every item of the list - if (Array.isArray(filters)) { - return filters.map((filtersItem) => - graphQLFiltersToStrapiQuery(filtersItem, contentType) - ); + if (Array.isArray(queryFilters)) { + return queryFilters.reduce((acc, filtersItem) => { + if (!customFields.includes(filtersItem)) { + acc.push(graphQLFiltersToStrapiQuery(filtersItem, contentType)); + } + + return acc; + }); } const resultMap: ToBeFixed = {}; + const { filterBy, filterByValue, ...filters } = queryFilters; const { attributes } = contentType; const isAttribute = (attributeName: string): boolean => { @@ -208,6 +218,30 @@ export = ({ strapi }: StrapiContext): IServiceGraphQL => { } } + if (filterBy === "DATE_CREATED") { + const date = new Date(filterByValue); + + if (!filterByValue || Number.isNaN(+date)) { + throw new Error('Invalid date specified in "filterByValue"'); + } + + const start = date.setHours(0, 0, 0, 0); + const end = date.setHours(23, 59, 59, 999); + + resultMap.createdAt = { + $between: [start, end], + }; + } + + if (filterBy === "APPROVAL_STATUS") { + assertNotEmpty( + filterByValue, + new Error('Empty "filterByValue" parameter') + ); + + resultMap.approvalStatus = filterByValue; + } + return resultMap; }; diff --git a/server/services/utils/__tests__/functions.test.js b/server/services/utils/__tests__/functions.test.js index c37cf0ee..d6b308a5 100644 --- a/server/services/utils/__tests__/functions.test.js +++ b/server/services/utils/__tests__/functions.test.js @@ -27,7 +27,7 @@ describe("Test service functions utils", () => { try { resolveUserContextError({ id: 1 }); } catch (e) { - expect(e).toBeInstanceOf(PluginError); + expect(e).toBeInstanceOf(PluginError.default); expect(e).toHaveProperty("status", 401); } }); @@ -36,7 +36,7 @@ describe("Test service functions utils", () => { try { resolveUserContextError(); } catch (e) { - expect(e).toBeInstanceOf(PluginError); + expect(e).toBeInstanceOf(PluginError.default); expect(e).toHaveProperty("status", 403); } });