From 6a84cb85bc22557104e88ebaf9813e516c1b4c94 Mon Sep 17 00:00:00 2001 From: mateusz-kleszcz Date: Thu, 28 Aug 2025 11:08:46 +0200 Subject: [PATCH 1/2] fix: add pagination to get with children endpoint --- README.md | 51 +- .../services/__tests__/common.service.test.ts | 476 ++++++++++-------- server/src/services/common.service.ts | 158 +++++- server/src/services/utils/functions.ts | 39 -- .../client.controller.validator.ts | 2 + server/src/validators/utils.ts | 2 +- 6 files changed, 442 insertions(+), 286 deletions(-) diff --git a/README.md b/README.md index 5be533d..bf978b5 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ In our minimum support we're following [official Node.js releases timelines](htt > This plugin is designed for **Strapi v5**. To get support for other Strapi versions, please follow the [versions](#-versions) section. **Plugin dependencies** -- `@strapi/plugin-graphql` - required to run GraphQL handled by this plugin + +- `@strapi/plugin-graphql` - required to run GraphQL handled by this plugin **We recommend always using the latest version of Strapi to start your new projects**. @@ -134,7 +135,7 @@ On the dedicated page, you will be able to set up all crucial properties which d To setup amend default plugin configuration we recommend to put following snippet as part of `config/plugins.{js|ts}` or `config//plugins.{js|ts}` file. If the file does not exist yet, you have to create it manually. If you've got already configurations for other plugins stores by this way, use just the `comments` part within exising `plugins` item. ```ts - module.exports = ({ env }) => ({ +module.exports = ({ env }) => ({ //... comments: { enabled: true, @@ -289,6 +290,7 @@ Return a hierarchical tree structure of comments for specified instance of Conte - [field selection](https://docs.strapi.io/dev-docs/api/rest/populate-select#field-selection) - [sorting](https://docs.strapi.io/dev-docs/api/rest/sort-pagination#sorting) +- [pagination](https://docs.strapi.io/dev-docs/api/rest/sort-pagination#pagination) ### Get Comments (flat structure) @@ -666,7 +668,7 @@ query { "id": "123456", "name": "Joe Doe" } - }, + } // ... ] } @@ -937,34 +939,32 @@ Lifecycle hooks can be register either in `register()` or `bootstrap()` methods Listeners can by sync and `async`. ->Be aware that lifecycle hooks registered in `register()` may be fired by plugin's bootstrapping. If you want listen to events triggered after server's startup use `bootstrap()`. +> Be aware that lifecycle hooks registered in `register()` may be fired by plugin's bootstrapping. If you want listen to events triggered after server's startup use `bootstrap()`. Example: ```ts - const commentsCommonService = strapi - .plugin("comments") - .service("common"); +const commentsCommonService = strapi.plugin("comments").service("common"); - commentsCommonService.registerLifecycleHook({ - callback: async ({ action, result }) => { - const saveResult = await logIntoSystem(action, result); +commentsCommonService.registerLifecycleHook({ + callback: async ({ action, result }) => { + const saveResult = await logIntoSystem(action, result); - console.log(saveResult); - }, - contentTypeName: "comment", - hookName: "afterCreate", - }); + console.log(saveResult); + }, + contentTypeName: "comment", + hookName: "afterCreate", +}); - commentsCommonService.registerLifecycleHook({ - callback: async ({ action, result }) => { - const saveResult = await logIntoSystem(action, result); +commentsCommonService.registerLifecycleHook({ + callback: async ({ action, result }) => { + const saveResult = await logIntoSystem(action, result); - console.log(saveResult); - }, - contentTypeName: "report", - hookName: "afterCreate", - }); + console.log(saveResult); + }, + contentTypeName: "report", + hookName: "afterCreate", +}); ``` ## 💬 FAQ @@ -977,14 +977,13 @@ Example: ```ts module.exports = { - 'comments': { enabled: true }, - 'graphql': { enabled: true }, + comments: { enabled: true }, + graphql: { enabled: true }, }; ``` If you already got it, make sure that `comments` plugin is inserted before `graphql`. That should do the job. - ## 🤝 Contributing to the plugin Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome! diff --git a/server/src/services/__tests__/common.service.test.ts b/server/src/services/__tests__/common.service.test.ts index 807fdd8..99f3486 100644 --- a/server/src/services/__tests__/common.service.test.ts +++ b/server/src/services/__tests__/common.service.test.ts @@ -1,31 +1,31 @@ -import { isProfane, replaceProfanities } from 'no-profanity'; -import { StrapiContext } from '../../@types'; -import { CommentsPluginConfig } from '../../config'; -import { getCommentRepository, getStoreRepository } from '../../repositories'; -import { getOrderBy } from '../../repositories/utils'; -import { caster } from '../../test/utils'; -import PluginError from '../../utils/PluginError'; -import { Comment } from '../../validators/repositories'; -import commonService from '../common.service'; +import { isProfane, replaceProfanities } from "no-profanity"; +import { StrapiContext } from "../../@types"; +import { CommentsPluginConfig } from "../../config"; +import { getCommentRepository, getStoreRepository } from "../../repositories"; +import { getOrderBy } from "../../repositories/utils"; +import { caster } from "../../test/utils"; +import PluginError from "../../utils/PluginError"; +import { Comment } from "../../validators/repositories"; +import commonService from "../common.service"; type CommentWithChildren = Comment & { children?: CommentWithChildren[]; }; -jest.mock('../../repositories', () => ({ +jest.mock("../../repositories", () => ({ getCommentRepository: jest.fn(), getStoreRepository: jest.fn(), })); -jest.mock('../../repositories/utils', () => ({ +jest.mock("../../repositories/utils", () => ({ getOrderBy: jest.fn(), })); -jest.mock('no-profanity', () => ({ +jest.mock("no-profanity", () => ({ isProfane: jest.fn(), replaceProfanities: jest.fn(), })); -describe('common.service', () => { +describe("common.service", () => { const mockCommentRepository = { findOne: jest.fn(), findMany: jest.fn(), @@ -44,29 +44,32 @@ describe('common.service', () => { beforeEach(() => { jest.clearAllMocks(); - caster(getCommentRepository).mockReturnValue(mockCommentRepository); + caster(getCommentRepository).mockReturnValue( + mockCommentRepository, + ); caster(getStoreRepository).mockReturnValue(mockStoreRepository); }); - const getStrapi = () => caster({ - strapi: { - documents: () => ({ - findOne: mockFindOne, - findMany: mockFindMany, - }), - plugin: () => null - } - }); + const getStrapi = () => + caster({ + strapi: { + documents: () => ({ + findOne: mockFindOne, + findMany: mockFindMany, + }), + plugin: () => null, + }, + }); const getService = (strapi: StrapiContext) => commonService(strapi); - describe('getConfig', () => { - it('should return full config when no prop is specified', async () => { + describe("getConfig", () => { + it("should return full config when no prop is specified", async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockConfig: Partial = { + const mockConfig: Partial = { isValidationEnabled: true, - moderatorRoles: ['admin'], + moderatorRoles: ["admin"], }; mockStoreRepository.getConfig.mockResolvedValue(mockConfig); @@ -76,50 +79,54 @@ describe('common.service', () => { expect(result).toEqual(mockConfig); }); - it('should return specific config prop when specified', async () => { + it("should return specific config prop when specified", async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockConfig: Partial = { - moderatorRoles: ['admin'], + const mockConfig: Partial = { + moderatorRoles: ["admin"], }; mockStoreRepository.getConfig.mockResolvedValue(mockConfig); - const result = await service.getConfig('moderatorRoles'); + const result = await service.getConfig("moderatorRoles"); - expect(result).toEqual(['admin']); + expect(result).toEqual(["admin"]); }); - it('should return local config when useLocal is true', async () => { + it("should return local config when useLocal is true", async () => { const strapi = getStrapi(); const service = getService(strapi); - const defaultValue = ['admin']; + const defaultValue = ["admin"]; mockStoreRepository.getLocalConfig.mockReturnValue(defaultValue); - const result = await service.getConfig('moderatorRoles', defaultValue, true); + const result = await service.getConfig( + "moderatorRoles", + defaultValue, + true, + ); expect(result).toEqual(defaultValue); }); }); - describe('parseRelationString', () => { - it('should correctly parse relation string', () => { + describe("parseRelationString", () => { + it("should correctly parse relation string", () => { const strapi = getStrapi(); const service = getService(strapi); - const relation = 'api::test.test:1'; + const relation = "api::test.test:1"; const result = service.parseRelationString(relation); expect(result).toEqual({ - uid: 'api::test.test', - relatedId: '1', + uid: "api::test.test", + relatedId: "1", }); }); }); - describe('isValidUserContext', () => { - it('should return true for valid user context', () => { + describe("isValidUserContext", () => { + it("should return true for valid user context", () => { const strapi = getStrapi(); const service = getService(strapi); const user = { id: 1 }; @@ -129,7 +136,7 @@ describe('common.service', () => { expect(result).toBe(true); }); - it('should return false for invalid user context', () => { + it("should return false for invalid user context", () => { const strapi = getStrapi(); const service = getService(strapi); const user = {}; @@ -140,11 +147,11 @@ describe('common.service', () => { }); }); - describe('findOne', () => { - it('should find and return a comment', async () => { + describe("findOne", () => { + it("should find and return a comment", async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockComment = { id: 1, content: 'Test comment' }; + const mockComment = { id: 1, content: "Test comment" }; mockCommentRepository.findOne.mockResolvedValue(mockComment); mockStoreRepository.getConfig.mockResolvedValue([]); @@ -161,7 +168,7 @@ describe('common.service', () => { }); }); - it('should throw error when comment does not exist', async () => { + it("should throw error when comment does not exist", async () => { const strapi = getStrapi(); const service = getService(strapi); @@ -171,11 +178,11 @@ describe('common.service', () => { }); }); - describe('checkBadWords', () => { - it('should pass clean content', async () => { + describe("checkBadWords", () => { + it("should pass clean content", async () => { const strapi = getStrapi(); const service = getService(strapi); - const content = 'Clean content'; + const content = "Clean content"; mockStoreRepository.getConfig.mockResolvedValue(true); caster(isProfane).mockReturnValue(false); @@ -185,38 +192,38 @@ describe('common.service', () => { expect(result).toBe(content); }); - it('should throw error for profane content', async () => { + it("should throw error for profane content", async () => { const strapi = getStrapi(); const service = getService(strapi); - const content = 'Bad content'; + const content = "Bad content"; mockStoreRepository.getConfig.mockResolvedValue(true); caster(isProfane).mockReturnValue(true); - caster(replaceProfanities).mockReturnValue('Filtered content'); + caster(replaceProfanities).mockReturnValue("Filtered content"); await expect(service.checkBadWords(content)).rejects.toThrow(PluginError); }); }); - describe('findAllFlat', () => { - it('should return flat list of comments', async () => { + describe("findAllFlat", () => { + it("should return flat list of comments", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: 'Comment 1' }, - { id: 2, content: 'Comment 2' }, + { id: 1, content: "Comment 1" }, + { id: 2, content: "Comment 2" }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ results: mockComments, pagination: { total: 2 }, }); - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); - + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllFlat({ - fields: ['id', 'content'], + fields: ["id", "content"], limit: 10, skip: 0, }); @@ -226,81 +233,100 @@ describe('common.service', () => { }); }); - describe('modifiedNestedNestedComments', () => { - describe('when nested entries don\'t have relation', () => { - it('should modify nested comments recursively', async () => { - const strapi = getStrapi(); - const service = getService(strapi); - const mockComments = [ - { id: 2, threadOf: 1 }, - { id: 3, threadOf: 1 }, - ]; - - mockCommentRepository.findMany - .mockResolvedValue(mockComments) - .mockResolvedValueOnce([]) - mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - - const result = await service.modifiedNestedNestedComments(1, 'removed', true); - - expect(result).toBe(true); - expect(mockCommentRepository.updateMany).toHaveBeenCalled(); - }); - }) - - describe('when nested entries have relation', () => { - it('should change entries to the deepLimit', async () => { - const strapi = getStrapi(); - const service = getService(strapi); - const mockComments = [ - { id: 2, threadOf: 1 }, - { id: 3, threadOf: 1 }, - ]; - - mockCommentRepository.findMany.mockResolvedValue(mockComments) - mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - - const result = await service.modifiedNestedNestedComments(1, 'removed', true); - - expect(result).toBe(true); - expect(mockCommentRepository.updateMany).toHaveBeenCalled(); - }); - }) - - it('should return false on update failure', async () => { + describe("modifiedNestedNestedComments", () => { + describe("when nested entries don't have relation", () => { + it("should modify nested comments recursively", async () => { + const strapi = getStrapi(); + const service = getService(strapi); + const mockComments = [ + { id: 2, threadOf: 1 }, + { id: 3, threadOf: 1 }, + ]; + + mockCommentRepository.findMany + .mockResolvedValue(mockComments) + .mockResolvedValueOnce([]); + mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); + + const result = await service.modifiedNestedNestedComments( + 1, + "removed", + true, + ); + + expect(result).toBe(true); + expect(mockCommentRepository.updateMany).toHaveBeenCalled(); + }); + }); + + describe("when nested entries have relation", () => { + it("should change entries to the deepLimit", async () => { + const strapi = getStrapi(); + const service = getService(strapi); + const mockComments = [ + { id: 2, threadOf: 1 }, + { id: 3, threadOf: 1 }, + ]; + + mockCommentRepository.findMany.mockResolvedValue(mockComments); + mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); + + const result = await service.modifiedNestedNestedComments( + 1, + "removed", + true, + ); + + expect(result).toBe(true); + expect(mockCommentRepository.updateMany).toHaveBeenCalled(); + }); + }); + + it("should return false on update failure", async () => { const strapi = getStrapi(); const service = getService(strapi); - mockCommentRepository.findMany.mockRejectedValue(new Error('Update failed')); + mockCommentRepository.findMany.mockRejectedValue( + new Error("Update failed"), + ); - const result = await service.modifiedNestedNestedComments(1, 'removed', true); + const result = await service.modifiedNestedNestedComments( + 1, + "removed", + true, + ); expect(result).toBe(false); }); }); - describe('findAllInHierarchy', () => { - it('should return comments in hierarchical structure', async () => { + describe("findAllInHierarchy", () => { + it("should return comments in hierarchical structure", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: 'Parent 1', threadOf: null }, - { id: 2, content: 'Child 1', threadOf: 1 }, - { id: 3, content: 'Child 2', threadOf: 1 }, - { id: 4, content: 'Grandchild 1', threadOf: 2 }, + { id: 1, content: "Parent 1", threadOf: null }, + { id: 2, content: "Child 1", threadOf: "1" }, + { id: 3, content: "Child 2", threadOf: "1" }, + { id: 4, content: "Grandchild 1", threadOf: "2" }, ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); - mockCommentRepository.findWithCount.mockResolvedValue({ - results: mockComments, - pagination: { total: 4 }, + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + mockCommentRepository.findWithCount.mockImplementation(async (args) => { + const threadOf = args?.where?.threadOf?.$eq ?? null; + const filtered = mockComments.filter((c) => c.threadOf === threadOf); + return { + results: filtered, + pagination: { total: filtered.length }, + }; }); + mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ['id', 'content', 'threadOf'], - sort: 'createdAt:desc', + fields: ["id", "content", "threadOf"], + sort: "createdAt:desc", }); const typedResult = result as CommentWithChildren[]; @@ -311,12 +337,12 @@ describe('common.service', () => { expect(typedResult[0].children![0].children).toHaveLength(1); // One grandchild }); - it('should handle empty comments list', async () => { + it("should handle empty comments list", async () => { const strapi = getStrapi(); const service = getService(strapi); mockCommentRepository.findMany.mockResolvedValue([]); - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); mockCommentRepository.findWithCount.mockResolvedValue({ results: [], pagination: { total: 0 }, @@ -324,33 +350,41 @@ describe('common.service', () => { mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ['id', 'content', 'threadOf'], + fields: ["id", "content", "threadOf"], }); expect(result).toHaveLength(0); }); - it('should start from specific comment when startingFromId is provided', async () => { + it("should start from specific comment when startingFromId is provided", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 2, content: 'Child 1', threadOf: 1 }, - { id: 3, content: 'Child 2', threadOf: 1 }, - { id: 4, content: 'Grandchild 1', threadOf: 2 }, - { id: 5, content: 'Grandchild 2', threadOf: 2 }, - { id: 6, content: 'Grandchild 3', threadOf: 4 }, + { id: 2, content: "Child 1", threadOf: "1" }, + { id: 3, content: "Child 2", threadOf: "1" }, + { id: 4, content: "Grandchild 1", threadOf: "2" }, + { id: 5, content: "Grandchild 2", threadOf: "2" }, + { id: 6, content: "Grandchild 3", threadOf: "4" }, ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); - mockCommentRepository.findWithCount.mockResolvedValue({ - results: mockComments, - pagination: { total: 3 }, + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + mockCommentRepository.findWithCount.mockImplementation(async (args) => { + const threadOf = + args?.where?.threadOf?.$eq ?? + args?.where?.threadOf.toString() ?? + null; + const filtered = mockComments.filter((c) => c.threadOf === threadOf); + console.log("FIND WITH COUNT", args, threadOf, filtered); + return { + results: filtered, + pagination: { total: filtered.length }, + }; }); mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ['id', 'content', 'threadOf'], + fields: ["id", "content", "threadOf"], startingFromId: 2, }); @@ -360,26 +394,41 @@ describe('common.service', () => { expect(typedResult[0].children).toHaveLength(1); }); - it('should handle comments with dropBlockedThreads enabled', async () => { + it("should handle comments with dropBlockedThreads enabled", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: 'Parent 1', threadOf: null, dropBlockedThreads: true, blockedThread: true }, - { id: 2, content: 'Child 1', threadOf: 1, dropBlockedThreads: false }, - { id: 3, content: 'Child 2', threadOf: 1, dropBlockedThreads: false }, - { id: 4, content: 'Grandchild 1', threadOf: 2, dropBlockedThreads: false }, + { + id: 1, + content: "Parent 1", + threadOf: null, + dropBlockedThreads: true, + blockedThread: true, + }, + { id: 2, content: "Child 1", threadOf: "1", dropBlockedThreads: false }, + { id: 3, content: "Child 2", threadOf: "1", dropBlockedThreads: false }, + { + id: 4, + content: "Grandchild 1", + threadOf: "2", + dropBlockedThreads: false, + }, ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); - mockCommentRepository.findWithCount.mockResolvedValue({ - results: mockComments, - pagination: { total: 4 }, + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + mockCommentRepository.findWithCount.mockImplementation(async (args) => { + const threadOf = args?.where?.threadOf?.$eq ?? null; + const filtered = mockComments.filter((c) => c.threadOf === threadOf); + return { + results: filtered, + pagination: { total: filtered.length }, + }; }); mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ['id', 'content', 'threadOf', 'blocked'], + fields: ["id", "content", "threadOf", "blocked"], dropBlockedThreads: true, }); @@ -389,40 +438,52 @@ describe('common.service', () => { }); }); - describe('updateComment', () => { - it('should update a comment successfully', async () => { + describe("updateComment", () => { + it("should update a comment successfully", async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockUpdatedComment = { id: 1, content: 'Updated content' }; + const mockUpdatedComment = { id: 1, content: "Updated content" }; mockCommentRepository.update.mockResolvedValue(mockUpdatedComment); - const result = await service.updateComment({ id: 1 }, { content: 'Updated content' }); + const result = await service.updateComment( + { id: 1 }, + { content: "Updated content" }, + ); expect(result).toEqual(mockUpdatedComment); expect(mockCommentRepository.update).toHaveBeenCalledWith({ where: { id: 1 }, - data: { content: 'Updated content' }, + data: { content: "Updated content" }, }); }); - it('should throw an error if update fails', async () => { + it("should throw an error if update fails", async () => { const strapi = getStrapi(); const service = getService(strapi); - mockCommentRepository.update.mockRejectedValue(new Error('Update failed')); + mockCommentRepository.update.mockRejectedValue( + new Error("Update failed"), + ); - await expect(service.updateComment({ id: 1 }, { content: 'Updated content' })).rejects.toThrow('Update failed'); + await expect( + service.updateComment({ id: 1 }, { content: "Updated content" }), + ).rejects.toThrow("Update failed"); }); }); - describe('mergeRelatedEntityTo', () => { - it('should merge related entity with comment', () => { + describe("mergeRelatedEntityTo", () => { + it("should merge related entity with comment", () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; + const comment = { id: 1, related: "api::test.test:1", locale: "en" }; const relatedEntities = [ - { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title' }, + { + uid: "api::test.test", + documentId: "1", + locale: "en", + title: "Test Title", + }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -433,12 +494,17 @@ describe('common.service', () => { }); }); - it('should not merge if no related entity matches', () => { + it("should not merge if no related entity matches", () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; + const comment = { id: 1, related: "api::test.test:1", locale: "en" }; const relatedEntities = [ - { uid: 'api::test.test', documentId: '2', locale: 'en', title: 'Test Title' }, + { + uid: "api::test.test", + documentId: "2", + locale: "en", + title: "Test Title", + }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -446,22 +512,22 @@ describe('common.service', () => { expect(result).toEqual({ ...comment, related: undefined }); }); - it('should handle empty related entities array', () => { + it("should handle empty related entities array", () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; + const comment = { id: 1, related: "api::test.test:1", locale: "en" }; const result = service.mergeRelatedEntityTo(comment, []); expect(result).toEqual({ ...comment, related: undefined }); }); - it('should merge related entity without locale', () => { + it("should merge related entity without locale", () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: 'api::test.test:1' }; + const comment = { id: 1, related: "api::test.test:1" }; const relatedEntities = [ - { uid: 'api::test.test', documentId: '1', title: 'Test Title' }, + { uid: "api::test.test", documentId: "1", title: "Test Title" }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -473,13 +539,13 @@ describe('common.service', () => { }); }); - describe('findAllPerAuthor', () => { - it('should return comments for a specific author', async () => { + describe("findAllPerAuthor", () => { + it("should return comments for a specific author", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: 'Comment 1', authorId: 1 }, - { id: 2, content: 'Comment 2', authorId: 1 }, + { id: 1, content: "Comment 1", authorId: 1 }, + { id: 2, content: "Comment 2", authorId: 1 }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ @@ -488,43 +554,42 @@ describe('common.service', () => { }); mockStoreRepository.getConfig.mockResolvedValue([]); - - caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); + caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); const result = await service.findAllPerAuthor({ authorId: 1, - fields: ['id', 'content'], + fields: ["id", "content"], }); expect(result.data).toHaveLength(2); - expect(result.data.every(item => !item.authorUser)).toBeTruthy(); + expect(result.data.every((item) => !item.authorUser)).toBeTruthy(); expect(mockCommentRepository.findWithCount).toHaveBeenCalledWith({ - pageSize: 10, + pageSize: 10, page: 1, - populate: { authorUser: true }, + populate: { authorUser: true }, select: ["id", "content", "related"], orderBy: { createdAt: "desc" }, - where: { authorId: 1 } + where: { authorId: 1 }, }); }); - it('should return empty data if authorId is not provided', async () => { + it("should return empty data if authorId is not provided", async () => { const strapi = getStrapi(); const service = getService(strapi); const result = await service.findAllPerAuthor({ - fields: ['id', 'content'], + fields: ["id", "content"], }); expect(result.data).toHaveLength(0); }); - it('should filter comments correctly for Strapi authors', async () => { + it("should filter comments correctly for Strapi authors", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: 'Comment 1', authorUser: { id: 1 } }, - { id: 2, content: 'Comment 2', authorUser: { id: 1 } }, + { id: 1, content: "Comment 1", authorUser: { id: 1 } }, + { id: 2, content: "Comment 2", authorUser: { id: 1 } }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ @@ -534,19 +599,22 @@ describe('common.service', () => { mockStoreRepository.getConfig.mockResolvedValue([]); - const result = await service.findAllPerAuthor({ - authorId: 1, - fields: ['id', 'content'], - }, true); + const result = await service.findAllPerAuthor( + { + authorId: 1, + fields: ["id", "content"], + }, + true, + ); expect(result.data).toHaveLength(2); - expect(result.data.every(item => !item.authorUser)).toBeTruthy(); + expect(result.data.every((item) => !item.authorUser)).toBeTruthy(); expect(mockCommentRepository.findWithCount).toHaveBeenCalledWith({ where: { authorUser: { id: 1 } }, pageSize: 10, page: 1, - select: ['id', 'content', 'related'], - orderBy: { createdAt: 'desc' }, + select: ["id", "content", "related"], + orderBy: { createdAt: "desc" }, populate: { authorUser: true, }, @@ -554,15 +622,20 @@ describe('common.service', () => { }); }); - describe('findRelatedEntitiesFor', () => { - it('should find related entities for given comments', async () => { + describe("findRelatedEntitiesFor", () => { + it("should find related entities for given comments", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: 'api::test.test:1', locale: 'en' }, - { id: 1, related: 'api::test.test:1', locale: 'en' } + { id: 1, related: "api::test.test:1", locale: "en" }, + { id: 1, related: "api::test.test:1", locale: "en" }, ]; - const mockRelatedEntities = { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title 1' }; + const mockRelatedEntities = { + uid: "api::test.test", + documentId: "1", + locale: "en", + title: "Test Title 1", + }; mockFindOne.mockResolvedValue(mockRelatedEntities); @@ -572,11 +645,11 @@ describe('common.service', () => { expect(result).toEqual(expect.arrayContaining([mockRelatedEntities])); }); - it('should return an empty array if no related entities are found', async () => { + it("should return an empty array if no related entities are found", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: 'api::test.test:1', locale: 'en' }, + { id: 1, related: "api::test.test:1", locale: "en" }, ]; mockFindOne.mockResolvedValue(undefined); @@ -587,22 +660,29 @@ describe('common.service', () => { }); }); - describe('Handle entity updates', () => { - it('should mark comments as deleted if related entry is deleted', async () => { + describe("Handle entity updates", () => { + it("should mark comments as deleted if related entry is deleted", async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: 'api::test.test:1', locale: 'en' }, - { id: 1, related: 'api::test.test:1', locale: 'en' } + { id: 1, related: "api::test.test:1", locale: "en" }, + { id: 1, related: "api::test.test:1", locale: "en" }, ]; - const mockRelatedEntities = { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title 1' }; + const mockRelatedEntities = { + uid: "api::test.test", + documentId: "1", + locale: "en", + title: "Test Title 1", + }; - mockCommentRepository.findMany.mockResolvedValue(mockComments) + mockCommentRepository.findMany.mockResolvedValue(mockComments); mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - const result = await service.perRemove([mockRelatedEntities.uid, mockRelatedEntities.documentId].join(':')); + const result = await service.perRemove( + [mockRelatedEntities.uid, mockRelatedEntities.documentId].join(":"), + ); - expect(result).toEqual({ count: 2}); + expect(result).toEqual({ count: 2 }); expect(mockCommentRepository.updateMany).toHaveBeenCalled(); }); }); diff --git a/server/src/services/common.service.ts b/server/src/services/common.service.ts index 06d54f6..8687f69 100644 --- a/server/src/services/common.service.ts +++ b/server/src/services/common.service.ts @@ -21,7 +21,7 @@ import PluginError from '../utils/PluginError'; import { client as clientValidator } from '../validators/api'; import { Comment, CommentRelated, CommentWithRelated } from '../validators/repositories'; import { Pagination } from '../validators/repositories/utils'; -import { buildAuthorModel, buildNestedStructure, filterOurResolvedReports, getRelatedGroups } from './utils/functions'; +import { buildAuthorModel, filterOurResolvedReports, getRelatedGroups } from './utils/functions'; const PAGE_SIZE = 10; @@ -33,7 +33,6 @@ type ParsedRelation = { relatedId: string; }; - type Created = PathTo; const commonService = ({ strapi }: StrapiContext) => ({ @@ -71,18 +70,24 @@ const commonService = ({ strapi }: StrapiContext) => ({ }, // Find comments in the flat structure - async findAllFlat({ - fields, - limit, - skip, - sort, - populate, - omit: baseOmit = [], - isAdmin = false, - pagination, - filters = {}, - locale, - }: clientValidator.FindAllFlatSchema, relatedEntity?: any): Promise<{ data: Array, pagination?: Pagination }> { + async findAllFlat( + { + fields, + limit, + skip, + sort, + populate, + omit: baseOmit = [], + isAdmin = false, + pagination, + filters = {}, + locale, + }: clientValidator.FindAllFlatSchema, + relatedEntity?: any, + ): Promise<{ + data: Array; + pagination?: Pagination; + }> { const omit = baseOmit.filter((field) => !REQUIRED_FIELDS.includes(field)); const defaultSelect = (['id', 'related'] as const).filter((field) => !omit.includes(field)); @@ -160,6 +165,81 @@ const commonService = ({ strapi }: StrapiContext) => ({ }; }, + async getCommentsChildren( + { + filters, + populate, + sort, + fields, + isAdmin = false, + omit = [], + locale, + limit, + }: clientValidator.FindAllInHierarchyValidatorSchema, + entry: Comment | CommentWithRelated, + relatedEntity?: any, + dropBlockedThreads = false, + blockNestedThreads = false, + ) { + if (!entry.gotThread) { + return { + ...entry, + threadOf: undefined, + related: undefined, + blockedThread: blockNestedThreads || entry.blockedThread, + children: [], + }; + } + + const children = await this.findAllFlat( + { + filters: { + threadOf: { $eq: entry.id.toString() }, + ...filters, + }, + populate, + sort, + fields, + isAdmin, + omit, + locale, + limit: Infinity, + }, + relatedEntity, + ); + + const allChildren = + entry.blockedThread && dropBlockedThreads + ? [] + : await Promise.all( + children.data.map((child) => + this.getCommentsChildren( + { + filters, + populate, + sort, + fields, + isAdmin, + omit, + locale, + limit, + }, + child, + relatedEntity, + dropBlockedThreads, + ), + ), + ); + + return { + ...entry, + threadOf: undefined, + related: undefined, + blockedThread: blockNestedThreads || entry.blockedThread, + children: allChildren, + }; + }, + // Find comments and create relations tree structure async findAllInHierarchy( { @@ -173,17 +253,51 @@ const commonService = ({ strapi }: StrapiContext) => ({ omit = [], locale, limit, + pagination, }: clientValidator.FindAllInHierarchyValidatorSchema, relatedEntity?: any, ) { - const entities = await this.findAllFlat({ filters, populate, sort, fields, isAdmin, omit, locale, limit }, relatedEntity); - return buildNestedStructure( - entities?.data, - startingFromId, - 'threadOf', - dropBlockedThreads, - false, + const rootEntries = await this.findAllFlat( + { + filters: { + threadOf: startingFromId + ? { $eq: startingFromId.toString() } + : { $null: true }, + ...filters, + }, + pagination, + populate, + sort, + fields, + isAdmin, + omit, + locale, + limit, + }, + relatedEntity, ); + + const rootEntriesWithChildren = await Promise.all( + rootEntries?.data.map((entry) => + this.getCommentsChildren( + { + filters, + populate, + sort, + fields, + isAdmin, + omit, + locale, + limit, + }, + entry, + relatedEntity, + dropBlockedThreads, + ), + ), + ); + + return rootEntriesWithChildren; }, // Find single comment @@ -340,7 +454,7 @@ const commonService = ({ strapi }: StrapiContext) => ({ if (content && isProfane({ testString: content })) { throw new PluginError( 400, - 'Bad language used! Please polite your comment...', + "Bad language used! Please polite your comment...", { content: { original: content, diff --git a/server/src/services/utils/functions.ts b/server/src/services/utils/functions.ts index daba54e..f1f49bf 100644 --- a/server/src/services/utils/functions.ts +++ b/server/src/services/utils/functions.ts @@ -13,45 +13,6 @@ interface StrapiAuthorUser { [key: string]: unknown; } -export const buildNestedStructure = ( - entities: Array, - id: Id | null = null, - field: string = 'threadOf', - dropBlockedThreads = false, - blockNestedThreads = false, -): Array => - entities - .filter((entity: Comment) => { - const entityField: any = get(entity, field); - if (entityField === null && id === null) { - return true; - } - let data = entityField; - if (data && typeof id === 'string') { - data = data.toString(); - } - return ( - (data && data == id) || - (isObject(entityField) && (entityField as any).id === id) - ); - }) - .map((entity: Comment) => ({ - ...entity, - [field]: undefined, - related: undefined, - blockedThread: blockNestedThreads || entity.blockedThread, - children: - entity.blockedThread && dropBlockedThreads - ? [] - : buildNestedStructure( - entities, - entity.id, - field, - dropBlockedThreads, - entity.blockedThread, - ), - })); - export const getRelatedGroups = (related: string): Array => related.split(REGEX.relatedUid).filter((s) => s && s.length > 0); diff --git a/server/src/validators/api/controllers/client.controller.validator.ts b/server/src/validators/api/controllers/client.controller.validator.ts index 655136c..d0bd995 100644 --- a/server/src/validators/api/controllers/client.controller.validator.ts +++ b/server/src/validators/api/controllers/client.controller.validator.ts @@ -84,6 +84,7 @@ const getBaseFindSchema = (enabledCollections: string[]) => { blockedThread: true, approvalStatus: true, isAdminComment: true, + threadOf: true, }); return z .object({ @@ -130,6 +131,7 @@ export const findAllInHierarchyValidator = (enabledCollections: string[], relati skip: true, relation: true, locale: true, + pagination: true, }) .merge(z.object({ startingFromId: z.number().optional(), diff --git a/server/src/validators/utils.ts b/server/src/validators/utils.ts index 55c1ad3..fb09a33 100644 --- a/server/src/validators/utils.ts +++ b/server/src/validators/utils.ts @@ -68,7 +68,7 @@ export const filtersValidator = z.union([ endWithValidators, containsValidators, notContainsValidators, - z.object({ $null: z.string().min(1) }), + z.object({ $null: z.boolean() }), z.object({ $notNull: z.boolean() }), ]); From 6feffe238f9b5eb761ec8e4107067973cf40b52f Mon Sep 17 00:00:00 2001 From: mateusz-kleszcz Date: Thu, 28 Aug 2025 11:19:12 +0200 Subject: [PATCH 2/2] chore: fix formatting in test file --- .../services/__tests__/common.service.test.ts | 427 ++++++++---------- 1 file changed, 181 insertions(+), 246 deletions(-) diff --git a/server/src/services/__tests__/common.service.test.ts b/server/src/services/__tests__/common.service.test.ts index 99f3486..ffc8449 100644 --- a/server/src/services/__tests__/common.service.test.ts +++ b/server/src/services/__tests__/common.service.test.ts @@ -1,31 +1,31 @@ -import { isProfane, replaceProfanities } from "no-profanity"; -import { StrapiContext } from "../../@types"; -import { CommentsPluginConfig } from "../../config"; -import { getCommentRepository, getStoreRepository } from "../../repositories"; -import { getOrderBy } from "../../repositories/utils"; -import { caster } from "../../test/utils"; -import PluginError from "../../utils/PluginError"; -import { Comment } from "../../validators/repositories"; -import commonService from "../common.service"; +import { isProfane, replaceProfanities } from 'no-profanity'; +import { StrapiContext } from '../../@types'; +import { CommentsPluginConfig } from '../../config'; +import { getCommentRepository, getStoreRepository } from '../../repositories'; +import { getOrderBy } from '../../repositories/utils'; +import { caster } from '../../test/utils'; +import PluginError from '../../utils/PluginError'; +import { Comment } from '../../validators/repositories'; +import commonService from '../common.service'; type CommentWithChildren = Comment & { children?: CommentWithChildren[]; }; -jest.mock("../../repositories", () => ({ +jest.mock('../../repositories', () => ({ getCommentRepository: jest.fn(), getStoreRepository: jest.fn(), })); -jest.mock("../../repositories/utils", () => ({ +jest.mock('../../repositories/utils', () => ({ getOrderBy: jest.fn(), })); -jest.mock("no-profanity", () => ({ +jest.mock('no-profanity', () => ({ isProfane: jest.fn(), replaceProfanities: jest.fn(), })); -describe("common.service", () => { +describe('common.service', () => { const mockCommentRepository = { findOne: jest.fn(), findMany: jest.fn(), @@ -44,32 +44,29 @@ describe("common.service", () => { beforeEach(() => { jest.clearAllMocks(); - caster(getCommentRepository).mockReturnValue( - mockCommentRepository, - ); + caster(getCommentRepository).mockReturnValue(mockCommentRepository); caster(getStoreRepository).mockReturnValue(mockStoreRepository); }); - const getStrapi = () => - caster({ - strapi: { - documents: () => ({ - findOne: mockFindOne, - findMany: mockFindMany, - }), - plugin: () => null, - }, - }); + const getStrapi = () => caster({ + strapi: { + documents: () => ({ + findOne: mockFindOne, + findMany: mockFindMany, + }), + plugin: () => null + } + }); const getService = (strapi: StrapiContext) => commonService(strapi); - describe("getConfig", () => { - it("should return full config when no prop is specified", async () => { + describe('getConfig', () => { + it('should return full config when no prop is specified', async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockConfig: Partial = { + const mockConfig: Partial = { isValidationEnabled: true, - moderatorRoles: ["admin"], + moderatorRoles: ['admin'], }; mockStoreRepository.getConfig.mockResolvedValue(mockConfig); @@ -79,54 +76,50 @@ describe("common.service", () => { expect(result).toEqual(mockConfig); }); - it("should return specific config prop when specified", async () => { + it('should return specific config prop when specified', async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockConfig: Partial = { - moderatorRoles: ["admin"], + const mockConfig: Partial = { + moderatorRoles: ['admin'], }; mockStoreRepository.getConfig.mockResolvedValue(mockConfig); - const result = await service.getConfig("moderatorRoles"); + const result = await service.getConfig('moderatorRoles'); - expect(result).toEqual(["admin"]); + expect(result).toEqual(['admin']); }); - it("should return local config when useLocal is true", async () => { + it('should return local config when useLocal is true', async () => { const strapi = getStrapi(); const service = getService(strapi); - const defaultValue = ["admin"]; + const defaultValue = ['admin']; mockStoreRepository.getLocalConfig.mockReturnValue(defaultValue); - const result = await service.getConfig( - "moderatorRoles", - defaultValue, - true, - ); + const result = await service.getConfig('moderatorRoles', defaultValue, true); expect(result).toEqual(defaultValue); }); }); - describe("parseRelationString", () => { - it("should correctly parse relation string", () => { + describe('parseRelationString', () => { + it('should correctly parse relation string', () => { const strapi = getStrapi(); const service = getService(strapi); - const relation = "api::test.test:1"; + const relation = 'api::test.test:1'; const result = service.parseRelationString(relation); expect(result).toEqual({ - uid: "api::test.test", - relatedId: "1", + uid: 'api::test.test', + relatedId: '1', }); }); }); - describe("isValidUserContext", () => { - it("should return true for valid user context", () => { + describe('isValidUserContext', () => { + it('should return true for valid user context', () => { const strapi = getStrapi(); const service = getService(strapi); const user = { id: 1 }; @@ -136,7 +129,7 @@ describe("common.service", () => { expect(result).toBe(true); }); - it("should return false for invalid user context", () => { + it('should return false for invalid user context', () => { const strapi = getStrapi(); const service = getService(strapi); const user = {}; @@ -147,11 +140,11 @@ describe("common.service", () => { }); }); - describe("findOne", () => { - it("should find and return a comment", async () => { + describe('findOne', () => { + it('should find and return a comment', async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockComment = { id: 1, content: "Test comment" }; + const mockComment = { id: 1, content: 'Test comment' }; mockCommentRepository.findOne.mockResolvedValue(mockComment); mockStoreRepository.getConfig.mockResolvedValue([]); @@ -168,7 +161,7 @@ describe("common.service", () => { }); }); - it("should throw error when comment does not exist", async () => { + it('should throw error when comment does not exist', async () => { const strapi = getStrapi(); const service = getService(strapi); @@ -178,11 +171,11 @@ describe("common.service", () => { }); }); - describe("checkBadWords", () => { - it("should pass clean content", async () => { + describe('checkBadWords', () => { + it('should pass clean content', async () => { const strapi = getStrapi(); const service = getService(strapi); - const content = "Clean content"; + const content = 'Clean content'; mockStoreRepository.getConfig.mockResolvedValue(true); caster(isProfane).mockReturnValue(false); @@ -192,38 +185,38 @@ describe("common.service", () => { expect(result).toBe(content); }); - it("should throw error for profane content", async () => { + it('should throw error for profane content', async () => { const strapi = getStrapi(); const service = getService(strapi); - const content = "Bad content"; + const content = 'Bad content'; mockStoreRepository.getConfig.mockResolvedValue(true); caster(isProfane).mockReturnValue(true); - caster(replaceProfanities).mockReturnValue("Filtered content"); + caster(replaceProfanities).mockReturnValue('Filtered content'); await expect(service.checkBadWords(content)).rejects.toThrow(PluginError); }); }); - describe("findAllFlat", () => { - it("should return flat list of comments", async () => { + describe('findAllFlat', () => { + it('should return flat list of comments', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: "Comment 1" }, - { id: 2, content: "Comment 2" }, + { id: 1, content: 'Comment 1' }, + { id: 2, content: 'Comment 2' }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ results: mockComments, pagination: { total: 2 }, }); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); - + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); + mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllFlat({ - fields: ["id", "content"], + fields: ['id', 'content'], limit: 10, skip: 0, }); @@ -233,75 +226,61 @@ describe("common.service", () => { }); }); - describe("modifiedNestedNestedComments", () => { - describe("when nested entries don't have relation", () => { - it("should modify nested comments recursively", async () => { - const strapi = getStrapi(); - const service = getService(strapi); - const mockComments = [ - { id: 2, threadOf: 1 }, - { id: 3, threadOf: 1 }, - ]; - - mockCommentRepository.findMany - .mockResolvedValue(mockComments) - .mockResolvedValueOnce([]); - mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - - const result = await service.modifiedNestedNestedComments( - 1, - "removed", - true, - ); - - expect(result).toBe(true); - expect(mockCommentRepository.updateMany).toHaveBeenCalled(); - }); - }); - - describe("when nested entries have relation", () => { - it("should change entries to the deepLimit", async () => { - const strapi = getStrapi(); - const service = getService(strapi); - const mockComments = [ - { id: 2, threadOf: 1 }, - { id: 3, threadOf: 1 }, - ]; - - mockCommentRepository.findMany.mockResolvedValue(mockComments); - mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - - const result = await service.modifiedNestedNestedComments( - 1, - "removed", - true, - ); - - expect(result).toBe(true); - expect(mockCommentRepository.updateMany).toHaveBeenCalled(); - }); - }); - - it("should return false on update failure", async () => { + describe('modifiedNestedNestedComments', () => { + describe('when nested entries don\'t have relation', () => { + it('should modify nested comments recursively', async () => { + const strapi = getStrapi(); + const service = getService(strapi); + const mockComments = [ + { id: 2, threadOf: 1 }, + { id: 3, threadOf: 1 }, + ]; + + mockCommentRepository.findMany + .mockResolvedValue(mockComments) + .mockResolvedValueOnce([]) + mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); + + const result = await service.modifiedNestedNestedComments(1, 'removed', true); + + expect(result).toBe(true); + expect(mockCommentRepository.updateMany).toHaveBeenCalled(); + }); + }) + + describe('when nested entries have relation', () => { + it('should change entries to the deepLimit', async () => { + const strapi = getStrapi(); + const service = getService(strapi); + const mockComments = [ + { id: 2, threadOf: 1 }, + { id: 3, threadOf: 1 }, + ]; + + mockCommentRepository.findMany.mockResolvedValue(mockComments) + mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); + + const result = await service.modifiedNestedNestedComments(1, 'removed', true); + + expect(result).toBe(true); + expect(mockCommentRepository.updateMany).toHaveBeenCalled(); + }); + }) + + it('should return false on update failure', async () => { const strapi = getStrapi(); const service = getService(strapi); - mockCommentRepository.findMany.mockRejectedValue( - new Error("Update failed"), - ); + mockCommentRepository.findMany.mockRejectedValue(new Error('Update failed')); - const result = await service.modifiedNestedNestedComments( - 1, - "removed", - true, - ); + const result = await service.modifiedNestedNestedComments(1, 'removed', true); expect(result).toBe(false); }); }); - describe("findAllInHierarchy", () => { - it("should return comments in hierarchical structure", async () => { + describe('findAllInHierarchy', () => { + it('should return comments in hierarchical structure', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ @@ -312,7 +291,7 @@ describe("common.service", () => { ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); mockCommentRepository.findWithCount.mockImplementation(async (args) => { const threadOf = args?.where?.threadOf?.$eq ?? null; const filtered = mockComments.filter((c) => c.threadOf === threadOf); @@ -321,12 +300,11 @@ describe("common.service", () => { pagination: { total: filtered.length }, }; }); - mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ["id", "content", "threadOf"], - sort: "createdAt:desc", + fields: ['id', 'content', 'threadOf'], + sort: 'createdAt:desc', }); const typedResult = result as CommentWithChildren[]; @@ -337,12 +315,12 @@ describe("common.service", () => { expect(typedResult[0].children![0].children).toHaveLength(1); // One grandchild }); - it("should handle empty comments list", async () => { + it('should handle empty comments list', async () => { const strapi = getStrapi(); const service = getService(strapi); mockCommentRepository.findMany.mockResolvedValue([]); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); mockCommentRepository.findWithCount.mockResolvedValue({ results: [], pagination: { total: 0 }, @@ -350,13 +328,13 @@ describe("common.service", () => { mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ["id", "content", "threadOf"], + fields: ['id', 'content', 'threadOf'], }); expect(result).toHaveLength(0); }); - it("should start from specific comment when startingFromId is provided", async () => { + it('should start from specific comment when startingFromId is provided', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ @@ -368,14 +346,13 @@ describe("common.service", () => { ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); mockCommentRepository.findWithCount.mockImplementation(async (args) => { const threadOf = args?.where?.threadOf?.$eq ?? args?.where?.threadOf.toString() ?? null; const filtered = mockComments.filter((c) => c.threadOf === threadOf); - console.log("FIND WITH COUNT", args, threadOf, filtered); return { results: filtered, pagination: { total: filtered.length }, @@ -384,7 +361,7 @@ describe("common.service", () => { mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ["id", "content", "threadOf"], + fields: ['id', 'content', 'threadOf'], startingFromId: 2, }); @@ -394,29 +371,18 @@ describe("common.service", () => { expect(typedResult[0].children).toHaveLength(1); }); - it("should handle comments with dropBlockedThreads enabled", async () => { + it('should handle comments with dropBlockedThreads enabled', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { - id: 1, - content: "Parent 1", - threadOf: null, - dropBlockedThreads: true, - blockedThread: true, - }, - { id: 2, content: "Child 1", threadOf: "1", dropBlockedThreads: false }, - { id: 3, content: "Child 2", threadOf: "1", dropBlockedThreads: false }, - { - id: 4, - content: "Grandchild 1", - threadOf: "2", - dropBlockedThreads: false, - }, + { id: 1, content: 'Parent 1', threadOf: null, dropBlockedThreads: true, blockedThread: true }, + { id: 2, content: 'Child 1', threadOf: '1', dropBlockedThreads: false }, + { id: 3, content: 'Child 2', threadOf: '1', dropBlockedThreads: false }, + { id: 4, content: 'Grandchild 1', threadOf: '2', dropBlockedThreads: false }, ]; mockCommentRepository.findMany.mockResolvedValue(mockComments); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); mockCommentRepository.findWithCount.mockImplementation(async (args) => { const threadOf = args?.where?.threadOf?.$eq ?? null; const filtered = mockComments.filter((c) => c.threadOf === threadOf); @@ -428,7 +394,7 @@ describe("common.service", () => { mockStoreRepository.getConfig.mockResolvedValue([]); const result = await service.findAllInHierarchy({ - fields: ["id", "content", "threadOf", "blocked"], + fields: ['id', 'content', 'threadOf', 'blocked'], dropBlockedThreads: true, }); @@ -438,52 +404,40 @@ describe("common.service", () => { }); }); - describe("updateComment", () => { - it("should update a comment successfully", async () => { + describe('updateComment', () => { + it('should update a comment successfully', async () => { const strapi = getStrapi(); const service = getService(strapi); - const mockUpdatedComment = { id: 1, content: "Updated content" }; + const mockUpdatedComment = { id: 1, content: 'Updated content' }; mockCommentRepository.update.mockResolvedValue(mockUpdatedComment); - const result = await service.updateComment( - { id: 1 }, - { content: "Updated content" }, - ); + const result = await service.updateComment({ id: 1 }, { content: 'Updated content' }); expect(result).toEqual(mockUpdatedComment); expect(mockCommentRepository.update).toHaveBeenCalledWith({ where: { id: 1 }, - data: { content: "Updated content" }, + data: { content: 'Updated content' }, }); }); - it("should throw an error if update fails", async () => { + it('should throw an error if update fails', async () => { const strapi = getStrapi(); const service = getService(strapi); - mockCommentRepository.update.mockRejectedValue( - new Error("Update failed"), - ); + mockCommentRepository.update.mockRejectedValue(new Error('Update failed')); - await expect( - service.updateComment({ id: 1 }, { content: "Updated content" }), - ).rejects.toThrow("Update failed"); + await expect(service.updateComment({ id: 1 }, { content: 'Updated content' })).rejects.toThrow('Update failed'); }); }); - describe("mergeRelatedEntityTo", () => { - it("should merge related entity with comment", () => { + describe('mergeRelatedEntityTo', () => { + it('should merge related entity with comment', () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: "api::test.test:1", locale: "en" }; + const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; const relatedEntities = [ - { - uid: "api::test.test", - documentId: "1", - locale: "en", - title: "Test Title", - }, + { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title' }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -494,17 +448,12 @@ describe("common.service", () => { }); }); - it("should not merge if no related entity matches", () => { + it('should not merge if no related entity matches', () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: "api::test.test:1", locale: "en" }; + const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; const relatedEntities = [ - { - uid: "api::test.test", - documentId: "2", - locale: "en", - title: "Test Title", - }, + { uid: 'api::test.test', documentId: '2', locale: 'en', title: 'Test Title' }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -512,22 +461,22 @@ describe("common.service", () => { expect(result).toEqual({ ...comment, related: undefined }); }); - it("should handle empty related entities array", () => { + it('should handle empty related entities array', () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: "api::test.test:1", locale: "en" }; + const comment = { id: 1, related: 'api::test.test:1', locale: 'en' }; const result = service.mergeRelatedEntityTo(comment, []); expect(result).toEqual({ ...comment, related: undefined }); }); - it("should merge related entity without locale", () => { + it('should merge related entity without locale', () => { const strapi = getStrapi(); const service = getService(strapi); - const comment = { id: 1, related: "api::test.test:1" }; + const comment = { id: 1, related: 'api::test.test:1' }; const relatedEntities = [ - { uid: "api::test.test", documentId: "1", title: "Test Title" }, + { uid: 'api::test.test', documentId: '1', title: 'Test Title' }, ]; const result = service.mergeRelatedEntityTo(comment, relatedEntities); @@ -539,13 +488,13 @@ describe("common.service", () => { }); }); - describe("findAllPerAuthor", () => { - it("should return comments for a specific author", async () => { + describe('findAllPerAuthor', () => { + it('should return comments for a specific author', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: "Comment 1", authorId: 1 }, - { id: 2, content: "Comment 2", authorId: 1 }, + { id: 1, content: 'Comment 1', authorId: 1 }, + { id: 2, content: 'Comment 2', authorId: 1 }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ @@ -554,42 +503,43 @@ describe("common.service", () => { }); mockStoreRepository.getConfig.mockResolvedValue([]); - caster(getOrderBy).mockReturnValue(["createdAt", "desc"]); + + caster(getOrderBy).mockReturnValue(['createdAt', 'desc']); const result = await service.findAllPerAuthor({ authorId: 1, - fields: ["id", "content"], + fields: ['id', 'content'], }); expect(result.data).toHaveLength(2); - expect(result.data.every((item) => !item.authorUser)).toBeTruthy(); + expect(result.data.every(item => !item.authorUser)).toBeTruthy(); expect(mockCommentRepository.findWithCount).toHaveBeenCalledWith({ - pageSize: 10, + pageSize: 10, page: 1, - populate: { authorUser: true }, + populate: { authorUser: true }, select: ["id", "content", "related"], orderBy: { createdAt: "desc" }, - where: { authorId: 1 }, + where: { authorId: 1 } }); }); - it("should return empty data if authorId is not provided", async () => { + it('should return empty data if authorId is not provided', async () => { const strapi = getStrapi(); const service = getService(strapi); const result = await service.findAllPerAuthor({ - fields: ["id", "content"], + fields: ['id', 'content'], }); expect(result.data).toHaveLength(0); }); - it("should filter comments correctly for Strapi authors", async () => { + it('should filter comments correctly for Strapi authors', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, content: "Comment 1", authorUser: { id: 1 } }, - { id: 2, content: "Comment 2", authorUser: { id: 1 } }, + { id: 1, content: 'Comment 1', authorUser: { id: 1 } }, + { id: 2, content: 'Comment 2', authorUser: { id: 1 } }, ]; mockCommentRepository.findWithCount.mockResolvedValue({ @@ -599,22 +549,19 @@ describe("common.service", () => { mockStoreRepository.getConfig.mockResolvedValue([]); - const result = await service.findAllPerAuthor( - { - authorId: 1, - fields: ["id", "content"], - }, - true, - ); + const result = await service.findAllPerAuthor({ + authorId: 1, + fields: ['id', 'content'], + }, true); expect(result.data).toHaveLength(2); - expect(result.data.every((item) => !item.authorUser)).toBeTruthy(); + expect(result.data.every(item => !item.authorUser)).toBeTruthy(); expect(mockCommentRepository.findWithCount).toHaveBeenCalledWith({ where: { authorUser: { id: 1 } }, pageSize: 10, page: 1, - select: ["id", "content", "related"], - orderBy: { createdAt: "desc" }, + select: ['id', 'content', 'related'], + orderBy: { createdAt: 'desc' }, populate: { authorUser: true, }, @@ -622,20 +569,15 @@ describe("common.service", () => { }); }); - describe("findRelatedEntitiesFor", () => { - it("should find related entities for given comments", async () => { + describe('findRelatedEntitiesFor', () => { + it('should find related entities for given comments', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: "api::test.test:1", locale: "en" }, - { id: 1, related: "api::test.test:1", locale: "en" }, + { id: 1, related: 'api::test.test:1', locale: 'en' }, + { id: 1, related: 'api::test.test:1', locale: 'en' } ]; - const mockRelatedEntities = { - uid: "api::test.test", - documentId: "1", - locale: "en", - title: "Test Title 1", - }; + const mockRelatedEntities = { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title 1' }; mockFindOne.mockResolvedValue(mockRelatedEntities); @@ -645,11 +587,11 @@ describe("common.service", () => { expect(result).toEqual(expect.arrayContaining([mockRelatedEntities])); }); - it("should return an empty array if no related entities are found", async () => { + it('should return an empty array if no related entities are found', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: "api::test.test:1", locale: "en" }, + { id: 1, related: 'api::test.test:1', locale: 'en' }, ]; mockFindOne.mockResolvedValue(undefined); @@ -660,30 +602,23 @@ describe("common.service", () => { }); }); - describe("Handle entity updates", () => { - it("should mark comments as deleted if related entry is deleted", async () => { + describe('Handle entity updates', () => { + it('should mark comments as deleted if related entry is deleted', async () => { const strapi = getStrapi(); const service = getService(strapi); const mockComments = [ - { id: 1, related: "api::test.test:1", locale: "en" }, - { id: 1, related: "api::test.test:1", locale: "en" }, + { id: 1, related: 'api::test.test:1', locale: 'en' }, + { id: 1, related: 'api::test.test:1', locale: 'en' } ]; - const mockRelatedEntities = { - uid: "api::test.test", - documentId: "1", - locale: "en", - title: "Test Title 1", - }; + const mockRelatedEntities = { uid: 'api::test.test', documentId: '1', locale: 'en', title: 'Test Title 1' }; - mockCommentRepository.findMany.mockResolvedValue(mockComments); + mockCommentRepository.findMany.mockResolvedValue(mockComments) mockCommentRepository.updateMany.mockResolvedValue({ count: 2 }); - const result = await service.perRemove( - [mockRelatedEntities.uid, mockRelatedEntities.documentId].join(":"), - ); + const result = await service.perRemove([mockRelatedEntities.uid, mockRelatedEntities.documentId].join(':')); - expect(result).toEqual({ count: 2 }); + expect(result).toEqual({ count: 2}); expect(mockCommentRepository.updateMany).toHaveBeenCalled(); }); }); -}); +}); \ No newline at end of file