From 5e7933df060accec72f0f163dc9eef3c637f4005 Mon Sep 17 00:00:00 2001 From: Mateusz Ziarko Date: Fri, 15 Apr 2022 11:58:37 +0200 Subject: [PATCH] feat: issue #115 support rest select fields in query --- README.md | 2 + __mocks__/initSetup.ts | 15 ++++-- package.json | 2 +- server/controllers/client.ts | 8 ++-- .../utils/__tests__/parsers.test.js | 6 +++ server/controllers/utils/parsers.ts | 8 ++-- server/graphql/queries/findAllInHierarchy.ts | 1 - server/services/__tests__/common.test.ts | 23 ++++++++++ server/services/admin.ts | 4 +- server/services/common.ts | 20 ++++++-- types/contentTypes.ts | 1 + types/controllers.d.ts | 2 +- types/services.d.ts | 16 +++---- yarn.lock | 46 +++++++++++++++++-- 14 files changed, 123 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 237f0b2..66699ac 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,7 @@ Return a hierarchical tree structure of comments for specified instance of Conte #### Strapi REST API properties support: +- [field selection](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/populating-fields.html#field-selection) - [sorting](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/sort-pagination.html#sorting) ### Get Comments (flat structure) @@ -350,6 +351,7 @@ Return a flat structure of comments for specified instance of Content Type like #### Strapi REST API properties support: - [filtering](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/filtering-locale-publication.html#filtering) +- [field selection](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/populating-fields.html#field-selection) - [sorting](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/sort-pagination.html#sorting) - [pagination](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/sort-pagination.html#pagination) diff --git a/__mocks__/initSetup.ts b/__mocks__/initSetup.ts index b5b3559..66ff796 100644 --- a/__mocks__/initSetup.ts +++ b/__mocks__/initSetup.ts @@ -1,4 +1,4 @@ -import { get, set, isEmpty } from "lodash"; +import { get, set, pick, isEmpty } from "lodash"; // @ts-ignore export = (config: any = {}, toStore: boolean = false, database: any = {}) => { @@ -18,9 +18,18 @@ export = (config: any = {}, toStore: boolean = false, database: any = {}) => { const [handler, rest] = uid.split("::"); const [collection] = rest.split("."); const values = get(mock.db, `${handler}.${collection}.records`, []); + + const parseValues = (values: any[], args: any = {}) => values.map(_ => { + const { select = [] } = args; + if (!isEmpty(select)) { + return pick(_, [...select, 'threadOf', 'authorUser']); // Relation fields can't be "unselected" + } + return _; + }) + return { - findOne: async () => new Promise((resolve) => resolve(values[0])), - findMany: async () => new Promise((resolve) => resolve(values)), + findOne: async (args: any) => new Promise((resolve) => resolve(parseValues(values, args)[0])), + findMany: async (args: any) => new Promise((resolve) => resolve(parseValues(values, args))), findWithCount: async () => new Promise((resolve) => resolve([values, values.length])), count: async () => new Promise((resolve) => resolve(values.length)), diff --git a/package.json b/package.json index 2f55159..a2f5f4a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "ts-node": "^10.7.0", - "strapi-typed": "^1.0.5", + "strapi-typed": "^1.0.7", "typescript": "^4.5.5" }, "peerDependencies": { diff --git a/server/controllers/client.ts b/server/controllers/client.ts index f316d74..2764456 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -37,6 +37,7 @@ const controllers: IControllerClient = { const { sort: querySort, pagination: queryPagination, + fields, ...filterQuery } = query || {}; @@ -48,7 +49,8 @@ const controllers: IControllerClient = { relation, filterQuery, sort || querySort, - pagination || queryPagination + pagination || queryPagination, + fields ) ); } catch (e: ToBeFixed) { @@ -63,14 +65,14 @@ const controllers: IControllerClient = { const { params, query, sort } = ctx; const { relation } = parseParams<{ relation: string }>(params); - const { sort: querySort, ...filterQuery } = query || {}; + const { sort: querySort, fields, ...filterQuery } = query || {}; try { assertParamsPresent<{ relation: string }>(params, ["relation"]); return await this.getService("common").findAllInHierarchy( { - ...flatInput(relation, filterQuery, sort || querySort), + ...flatInput(relation, filterQuery, sort || querySort, undefined, fields), dropBlockedThreads: true, } ); diff --git a/server/controllers/utils/__tests__/parsers.test.js b/server/controllers/utils/__tests__/parsers.test.js index c911620..e2457de 100644 --- a/server/controllers/utils/__tests__/parsers.test.js +++ b/server/controllers/utils/__tests__/parsers.test.js @@ -17,6 +17,7 @@ describe("Test Comments controller parsers utils", () => { page: 3, pageSize: 5, }; + const fields = ["content"]; test("Should contain default property 'populate'", () => { expect(flatInput()).toHaveProperty( @@ -66,6 +67,11 @@ describe("Test Comments controller parsers utils", () => { ); }); + test("Should assign fields", () => { + const result = flatInput(relation, filters, sort, undefined, fields); + expect(result).toHaveProperty(["fields", 0], fields[0]); + }); + test("Should build complex output", () => { const day = 24 * 60 * 60 * 1000; const now = Date.now().toString(); diff --git a/server/controllers/utils/parsers.ts b/server/controllers/utils/parsers.ts index 727c7f6..2fcc4fc 100644 --- a/server/controllers/utils/parsers.ts +++ b/server/controllers/utils/parsers.ts @@ -1,11 +1,12 @@ -import { Id } from "strapi-typed"; +import { Id, OnlyStrings, StrapiRequestQueryFieldsClause } from "strapi-typed"; import { ToBeFixed } from "../../../types"; -export const flatInput = ( +export const flatInput = ( relation: Id, query: ToBeFixed, sort: ToBeFixed, - pagination?: ToBeFixed + pagination?: ToBeFixed, + fields?: StrapiRequestQueryFieldsClause>, ) => { const filters = query?.filters || query; const orOperator = (filters?.$or || []).filter( @@ -24,5 +25,6 @@ export const flatInput = ( }, pagination, sort, + fields, }; }; diff --git a/server/graphql/queries/findAllInHierarchy.ts b/server/graphql/queries/findAllInHierarchy.ts index b046a90..a75fe3d 100644 --- a/server/graphql/queries/findAllInHierarchy.ts +++ b/server/graphql/queries/findAllInHierarchy.ts @@ -14,7 +14,6 @@ type findAllInHierarchyProps = { relation: Id; filters: ToBeFixed; sort: ToBeFixed; - pagination: ToBeFixed; }; export = ({ strapi, nexus }: StrapiGraphQLContext) => { diff --git a/server/services/__tests__/common.test.ts b/server/services/__tests__/common.test.ts index 73f65a5..82d93e7 100644 --- a/server/services/__tests__/common.test.ts +++ b/server/services/__tests__/common.test.ts @@ -13,6 +13,16 @@ const setup = (config = {}, toStore = false, database = {}) => { }); }; +const filterOutUndefined = (_: any) => Object.keys(_).reduce((prev, curr) => { + if (_[curr] !== undefined) { + return { + ...prev, + [curr]: _[curr], + } + } + return prev; +}, {}); + afterEach(() => { Object.defineProperty(global, "strapi", {}); }); @@ -412,6 +422,19 @@ describe("Test Comments service functions utils", () => { expect(result).toHaveProperty(["data", 3, "content"], db[3].content); }); + test("Should return structure with selected fields only (+mandatory ones for logic)", async () => { // Default fields are: id, related, threadOf, gotThread + const result = await getPluginService( + "common" + ).findAllFlat({ query: { related }, fields: ['content'] }, relatedEntity); + expect(result).toHaveProperty("data"); + expect(result).not.toHaveProperty("meta"); + expect(result.data.length).toBe(4); + expect(Object.keys(filterOutUndefined(result.data[0]))).toHaveLength(6); + expect(Object.keys(filterOutUndefined(result.data[1]))).toHaveLength(6); + expect(Object.keys(filterOutUndefined(result.data[2]))).toHaveLength(6); + expect(Object.keys(filterOutUndefined(result.data[3]))).toHaveLength(6); + }); + test("Should return structure with pagination", async () => { const result = await getPluginService( "common" diff --git a/server/services/admin.ts b/server/services/admin.ts index ebddd40..6ce3d7b 100644 --- a/server/services/admin.ts +++ b/server/services/admin.ts @@ -140,7 +140,7 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ $or: [{ removed: false }, { removed: null }], }; - let params: StrapiDBQueryArgs = { + let params: StrapiDBQueryArgs = { where: !isEmpty(filters) ? { ...defaultWhere, @@ -177,7 +177,7 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ }, }, }); - const total = await strapi.db.query(getModelUid("comment")).count({ + const total = await strapi.db.query(getModelUid("comment")).count({ where: params.where, }); const relatedEntities = diff --git a/server/services/common.ts b/server/services/common.ts index 8153d32..780a2ae 100644 --- a/server/services/common.ts +++ b/server/services/common.ts @@ -10,6 +10,7 @@ import { parseInt, set, get, + uniq, } from "lodash"; import { Id, @@ -18,6 +19,7 @@ import { StrapiPagination, StrapiResponseMeta, StrapiPaginatedResponse, + StrapiDBQueryArgs, } from "strapi-typed"; import { CommentsPluginConfig, @@ -25,8 +27,9 @@ import { FindAllInHierarhyProps, IServiceCommon, ToBeFixed, + Comment, + RelatedEntity } from "../../types"; -import { Comment, RelatedEntity } from "../../types/contentTypes"; import { REGEX, CONFIG_PARAMS } from "../utils/constants"; import PluginError from "./../utils/error"; import { @@ -81,14 +84,15 @@ export = ({ strapi }: StrapiContext): IServiceCommon => ({ // Find comments in the flat structure async findAllFlat( this: IServiceCommon, - { query = {}, populate = {}, sort, pagination }: FindAllFlatProps, + { query = {}, populate = {}, sort, pagination, fields }: FindAllFlatProps, relatedEntity: RelatedEntity | null = null ): Promise> { + const defaultSelect: Array = ['id', 'related']; const defaultPopulate = { authorUser: true, }; - let queryExtension = {}; + let queryExtension: StrapiDBQueryArgs = {}; if (sort && (isString(sort) || isArray(sort))) { queryExtension = { @@ -102,6 +106,13 @@ export = ({ strapi }: StrapiContext): IServiceCommon => ({ }; } + if (!isNil(fields)) { + queryExtension = { + ...queryExtension, + select: isArray(fields) ? uniq([...fields, ...defaultSelect]) : fields + }; + } + let meta: StrapiResponseMeta = {} as StrapiResponseMeta; if (pagination && isObject(pagination)) { const parsedpagination: StrapiPagination = Object.keys(pagination).reduce( @@ -249,13 +260,14 @@ export = ({ strapi }: StrapiContext): IServiceCommon => ({ query, populate = {}, sort, + fields, startingFromId = null, dropBlockedThreads = false, }: FindAllInHierarhyProps, relatedEntity?: RelatedEntity | null | boolean ): Promise> { const entities = await this.findAllFlat( - { query, populate, sort }, + { query, populate, sort, fields }, relatedEntity ); return buildNestedStructure( diff --git a/types/contentTypes.ts b/types/contentTypes.ts index 8162b3a..0a563e3 100644 --- a/types/contentTypes.ts +++ b/types/contentTypes.ts @@ -40,6 +40,7 @@ export type CommentAuthorPartial = { export type CommentReport = { id: Id; + related: Comment | Id; reason: any; content: string; resolved: boolean; diff --git a/types/controllers.d.ts b/types/controllers.d.ts index 2e0163f..1aacd19 100644 --- a/types/controllers.d.ts +++ b/types/controllers.d.ts @@ -65,7 +65,7 @@ export interface IControllerAdmin { ctx: StrapiRequestContext ): ThrowablePromisedResponse; settingsUpdateConfig( - ctx: StrapiRequestContext + ctx: StrapiRequestContext ): ThrowablePromisedResponse; settingsRestoreConfig( ctx: StrapiRequestContext diff --git a/types/services.d.ts b/types/services.d.ts index e187ee4..5b15bd3 100644 --- a/types/services.d.ts +++ b/types/services.d.ts @@ -10,6 +10,9 @@ import { StrapiQueryParamsParsedFilters, StrapiQueryParamsParsedOrderBy, StrapiUser, + OnlyStrings, + StringMap, + StrapiRequestQueryFieldsClause, } from "strapi-typed"; import { ToBeFixed } from "./common"; import { @@ -26,17 +29,14 @@ export type AdminPaginatedResponse = { result: Array; } & StrapiResponseMeta; -export type FindAllFlatProps = { +export type FindAllFlatProps = { query: { threadOf?: number | string | null; [key: string]: any; } & {}; - populate?: { - [key: string]: any; - }; - sort?: { - [key: string]: any; - }; + populate?: StringMap; + sort?: StringMap; + fields?: StrapiRequestQueryFieldsClause> pagination?: StrapiPagination; }; @@ -74,7 +74,7 @@ export interface IServiceCommon { getPluginStore(): Promise; getLocalConfig(path?: string, defaultValue?: any): T; findAllFlat( - props: FindAllFlatProps, + props: FindAllFlatProps, relatedEntity?: RelatedEntity | null | boolean ): Promise>; findAllInHierarchy( diff --git a/yarn.lock b/yarn.lock index 47fd8e1..5719f93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -793,6 +793,31 @@ pluralize "^8.0.0" subscriptions-transport-ws "0.9.19" +"@strapi/plugin-graphql@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@strapi/plugin-graphql/-/plugin-graphql-4.1.8.tgz#7c3c9698bd191cfed130ddc9f2024ea936228a0e" + integrity sha512-Y5GC2YUU1gCGa392GxH5jvN1YgGoPt5biZpCfBe2u2O/UWB1O+kDZy+OgK6aydN9kI8HFTwxWiaipcunNDYmCw== + dependencies: + "@apollo/federation" "^0.28.0" + "@graphql-tools/schema" "8.1.2" + "@graphql-tools/utils" "^8.0.2" + "@strapi/utils" "4.1.8" + apollo-server-core "3.1.2" + apollo-server-koa "3.1.2" + glob "^7.1.7" + graphql "^15.5.1" + graphql-depth-limit "^1.1.0" + graphql-iso-date "^3.6.1" + graphql-playground-middleware-koa "^1.6.21" + graphql-type-json "^0.3.2" + graphql-type-long "^0.1.1" + graphql-upload "^13.0.0" + koa-compose "^4.1.0" + lodash "4.17.21" + nexus "1.2.0" + pluralize "^8.0.0" + subscriptions-transport-ws "0.9.19" + "@strapi/utils@4.1.6", "@strapi/utils@^4.1.6": version "4.1.6" resolved "https://registry.yarnpkg.com/@strapi/utils/-/utils-4.1.6.tgz#edfdec2470143b2437cc95a5119ef5fa7d892eb4" @@ -804,6 +829,17 @@ lodash "4.17.21" yup "0.32.9" +"@strapi/utils@4.1.8", "@strapi/utils@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@strapi/utils/-/utils-4.1.8.tgz#22c5b10b145f0ce005aefdb5611552ac73d02b23" + integrity sha512-FMC9gNQ+cXQNRkOaIiom6NlMIqqzo1gA9W/MkR/qezKIsivp+2nC+RsY4amS70D58yU/kjr1BdN5FtnwuyzOLQ== + dependencies: + "@sindresorhus/slugify" "1.1.0" + date-fns "2.24.0" + http-errors "1.8.0" + lodash "4.17.21" + yup "0.32.9" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -3369,7 +3405,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -3987,10 +4023,10 @@ stack-utils@^2.0.3: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -strapi-typed@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/strapi-typed/-/strapi-typed-1.0.5.tgz#2bf0574c767058ec0fc85c2d32c8d45d0910942d" - integrity sha512-00K1pnyCfT3Oe/Z4jjJrWDd02c43NWaTHdngVUIKHOEDiroQBOHz/MsFRrVOf5x7HF/rgGhNsCcq4n89HoNLig== +strapi-typed@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/strapi-typed/-/strapi-typed-1.0.7.tgz#6f7a9bc5619138a842f456d7fb7db7854ca5f2d5" + integrity sha512-g6mpK1GSwpse6rOJ6bCdpQYdL1glHrSxtFjn+drdeZL3k5jEsIzp+MXDGomeAo04mP7KycJ3l+j+ZM0r/ob2LQ== dependencies: "@strapi/plugin-graphql" "^4.1.6" "@strapi/utils" "^4.1.6"