From cb9f9c4daa23f4b404731c06c7c45ea047c30f29 Mon Sep 17 00:00:00 2001 From: shaadcode Date: Sat, 18 Jul 2026 15:36:24 +0330 Subject: [PATCH 1/2] test: add tests --- .github/workflows/CI.yml | 19 +- .vscode/extensions.json | 3 - .vscode/launch.json | 24 -- .vscode/settings.json | 128 --------- bun.lock | 7 +- dev/payload.config.ts | 11 +- dev/tsconfig.json | 10 +- package.json | 1 + .../automation/tasks/cleanup/cleanup.test.ts | 189 +++++++++++++ .../automation/tasks/{ => cleanup}/cleanup.ts | 20 +- src/core/buffer/bufferManager.test.ts | 100 +++++-- src/core/buffer/bufferManager.ts | 23 +- .../helpers/handleBufferDebugMode.test.ts | 263 ++++++++++++++++++ .../buffer/helpers/handleBufferDebugMode.ts | 3 +- .../helpers/{ => emitWrapper}/emitWrapper.ts | 6 +- src/core/log-builders/collections/hooks.ts | 2 +- src/core/log-builders/collections/shared.ts | 3 +- src/index.ts | 2 +- src/pluginUtils/configHelpers.ts | 2 +- 19 files changed, 594 insertions(+), 222 deletions(-) delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json create mode 100644 src/core/automation/tasks/cleanup/cleanup.test.ts rename src/core/automation/tasks/{ => cleanup}/cleanup.ts (67%) create mode 100644 src/core/buffer/helpers/handleBufferDebugMode.test.ts rename src/core/log-builders/collections/helpers/{ => emitWrapper}/emitWrapper.ts (89%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 701f3dd..943e7f9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,9 +2,7 @@ name: CI on: push: - branches: [main] pull_request: - branches: [main] jobs: build-and-test: @@ -15,23 +13,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "npm" + - name: Setup Bun + uses: oven-sh/setup-bun@v2 - name: Install dependencies - run: npm ci - - # - name: Lint code - # run: npm run lint - - # - name: Run tests - # run: npm test + run: bun --frozen-lockfile - name: Build project - run: npm run build:safe + run: bun build:safe - name: Job summary run: echo "✅ CI checks passed successfully!" diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 940260d..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 572ee15..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Next.js: debug full stack", - "type": "node", - "request": "launch", - "program": "${workspaceFolder}/node_modules/next/dist/bin/next", - "runtimeArgs": ["--inspect"], - "skipFiles": ["/**"], - "serverReadyAction": { - "action": "debugWithChrome", - "killOnServerStop": true, - "pattern": "- Local:.+(https?://.+)", - "uriFormat": "%s", - "webRoot": "${workspaceFolder}" - }, - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index eac4b18..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "editor.tabSize": 2, - // Disable the default formatter, use eslint instead - "prettier.enable": false, - // Auto fix - "editor.formatOnSave": true, - "editor.fontWeight": "500", - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": { - "source.organizeImports": "never", - "source.fixAll.eslint": "always", - "source.removeUnusedImports": "always" - }, - "js/ts.experimental.useTsgo": true, - "eslint.enable": true, - "eslint.probe": [ - "astro", - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "html", - "mdx", - "vue", - "markdown", - "json", - "jsonc" - ], - "[typescriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[typescript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "[json]": { - "editor.quickSuggestions": { - "strings": true - }, - "editor.defaultFormatter": "vscode.json-language-features" - }, - // Silent the stylistic rules in your IDE, but still auto fix them - "eslint.rules.customizations": [ - { - "rule": "style/*", - "severity": "off", - "fixable": true - }, - { - "rule": "format/*", - "severity": "off", - "fixable": true - }, - { - "rule": "*-indent", - "severity": "off", - "fixable": true - }, - { - "rule": "*-spacing", - "severity": "off", - "fixable": true - }, - { - "rule": "*-spaces", - "severity": "off", - "fixable": true - }, - { - "rule": "*-order", - "severity": "off", - "fixable": true - }, - { - "rule": "*-dangle", - "severity": "off", - "fixable": true - }, - { - "rule": "*-newline", - "severity": "off", - "fixable": true - }, - { - "rule": "*quotes", - "severity": "off", - "fixable": true - }, - { - "rule": "*semi", - "severity": "off", - "fixable": true - } - ], - // Enable eslint for all supported languages - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - "html", - "markdown", - "json", - "jsonc", - "yaml", - "toml", - "xml", - "gql", - "graphql", - "astro", - "svelte", - "css", - "less", - "scss", - "pcss", - "postcss" - ], - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "typescript.native-preview.tsdk": "node_modules\\@typescript\\native-preview", -} diff --git a/bun.lock b/bun.lock index 0d3fc7b..f3aa118 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,7 @@ "release-it": "^19.0.6", "rimraf": "^6.1.2", "sharp": "^0.34.5", + "type-fest": "^5.8.0", "typescript": "^5.9.3", "vitest": "^4.0.18", }, @@ -2158,6 +2159,8 @@ "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], @@ -2206,7 +2209,7 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], @@ -2492,6 +2495,8 @@ "mongoose-lean-virtuals/mpath": ["mpath@0.8.4", "", {}, "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g=="], + "new-github-release-url/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "noms/readable-stream": ["readable-stream@1.0.34", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], diff --git a/dev/payload.config.ts b/dev/payload.config.ts index 31b974a..d9c7ddf 100644 --- a/dev/payload.config.ts +++ b/dev/payload.config.ts @@ -3,12 +3,12 @@ import sharp from 'sharp'; import path from 'node:path'; import { buildConfig } from 'payload'; import { fileURLToPath } from 'node:url'; -import { media } from 'collections/Media.js'; -import { users } from 'collections/Users.js'; // import { auditorPlugin } from 'payload-auditor'; import { mongooseAdapter } from '@payloadcms/db-mongodb'; import { lexicalEditor } from '@payloadcms/richtext-lexical'; +import { media } from './collections/Media.js'; +import { users } from './collections/Users.js'; // import { auditorPlugin } from './../dist/index.js'; import { auditorPlugin } from '../src/index.js'; import { testEmailAdapter } from './helpers/testEmailAdapter.js'; @@ -49,7 +49,12 @@ export default buildConfig({ slug: 'media', hooks: { afterOperation: { - updateByID: { enabled: true }, + updateByID: { + enabled: true, + modes: { + debug: {}, + }, + }, }, }, }, diff --git a/dev/tsconfig.json b/dev/tsconfig.json index a462007..0edda57 100644 --- a/dev/tsconfig.json +++ b/dev/tsconfig.json @@ -2,17 +2,19 @@ "extends": "../tsconfig.json", "compilerOptions": { "target": "ES2022", - "baseUrl": "./", "module": "NodeNext", "moduleResolution": "NodeNext", "paths": { - "@payload-config": ["./payload.config.ts"], - "payload-auditor/*": ["../dist/*"] + "@payload-config": [ + "./payload.config.ts" + ], + "payload-auditor/*": [ + "../dist/*" + ] }, "emitDeclarationOnly": false, "noEmit": true }, - "include": [ "**/*.js", "**/*.jsx", diff --git a/package.json b/package.json index 1c7ca03..da58b80 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "release-it": "^19.0.6", "rimraf": "^6.1.2", "sharp": "^0.34.5", + "type-fest": "^5.8.0", "typescript": "^5.9.3", "vitest": "^4.0.18" }, diff --git a/src/core/automation/tasks/cleanup/cleanup.test.ts b/src/core/automation/tasks/cleanup/cleanup.test.ts new file mode 100644 index 0000000..cd6e026 --- /dev/null +++ b/src/core/automation/tasks/cleanup/cleanup.test.ts @@ -0,0 +1,189 @@ +import type { TaskConfig } from 'payload'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { PluginOptions } from '../../../../types/pluginOptions.js'; +import { CLEANUP_TASK_LABEL, CLEANUP_TASK_SLUG, cleanupLogsTask, DEFAULT_CRON_TIME, DEFAULT_OLDER_THAN, DEFAULT_QUEUE_NAME } from './cleanup.js'; + +const mockPluginConfig = { + automation: { + logCleanup: { + cronTime: DEFAULT_CRON_TIME, + olderThan: DEFAULT_OLDER_THAN, + queueName: DEFAULT_QUEUE_NAME, + }, + }, +} as const satisfies PluginOptions; + +const mockReq = { + payload: { + delete: vi.fn(), + logger: { + error: vi.fn(), + info: vi.fn(), + }, + }, +}; + +describe('cleanupLogsTask', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return expected task', () => { + const result = cleanupLogsTask(mockPluginConfig); + + const expectedResultInstance = { + handler: expect.any(Function), + slug: expect.any(String), + label: expect.any(String), + schedule: expect.any(Array), + } as TaskConfig<'cleanup-payload-auditor-log'>; + + const expectedResult = { + handler: expect.any(Function), + slug: CLEANUP_TASK_SLUG, + label: CLEANUP_TASK_LABEL, + schedule: [{ cron: DEFAULT_CRON_TIME, queue: DEFAULT_QUEUE_NAME }], + } as TaskConfig<'cleanup-payload-auditor-log'>; + + expect(result).toEqual(expectedResultInstance); + expect(result).toEqual(expectedResult); + }); + + describe('handler task', () => { + it('should call payload.delete with correct parameters', async () => { + const pluginOptions: PluginOptions = {}; + const task = cleanupLogsTask(pluginOptions); + + typeof task.handler === 'function' && await task + .handler({ + // @ts-expect-error + req: mockReq, + }); + + expect(mockReq.payload.delete) + .toHaveBeenCalledWith({ + collection: 'Audit-log', + where: expect.objectContaining({ + createdAt: { + less_than: expect.any(String), + }, + }), + }); + }); + + it('should use configureRootCollection result when provided', async () => { + const configuredSlug = 'configured-audit-logs'; + const pluginOptions: PluginOptions = { + collection: { + configureRootCollection: vi.fn().mockReturnValue({ slug: configuredSlug }), + trackCollections: [], + }, + }; + + const task = cleanupLogsTask(pluginOptions); + typeof task.handler === 'function' && await task + .handler({ + // @ts-expect-error + req: mockReq, + }); + + expect(mockReq.payload.delete).toHaveBeenCalledWith({ + collection: configuredSlug, + where: expect.any(Object), + }); + }); + + it('should use custom olderThan value', async () => { + const customOlderThan = 86400000; // 1 day + const pluginOptions: PluginOptions = { + automation: { + logCleanup: { + olderThan: customOlderThan, + }, + }, + }; + + const task = cleanupLogsTask(pluginOptions); + typeof task.handler === 'function' && await task.handler({ + // @ts-expect-error + req: mockReq, + }); + + const expectedDate = new Date(Date.now() - customOlderThan).toISOString(); + + expect(mockReq.payload.delete).toHaveBeenCalledWith({ + collection: 'Audit-log', + where: { + createdAt: { + less_than: expect.any(String), + }, + }, + }); + + const callArgs = mockReq.payload.delete.mock.calls[0][0]; + const lessThanValue = callArgs.where.createdAt.less_than; + expect(lessThanValue).toBe(expectedDate); + }); + + it('should return empty output object', async () => { + const pluginOptions: PluginOptions = {}; + const task = cleanupLogsTask(pluginOptions); + + const result = typeof task.handler === 'function' && await task.handler({ + // @ts-expect-error + req: mockReq, + }); + + expect(result).toEqual({ output: {} }); + }); + + it('should log error when payload.delete fails', async () => { + const mockError = new Error('Database connection failed'); + mockReq.payload.delete.mockRejectedValueOnce(mockError); + + const pluginOptions: PluginOptions = {}; + const task = cleanupLogsTask(pluginOptions); + + typeof task.handler === 'function' && await task.handler({ + // @ts-expect-error + req: mockReq, + }); + + expect(mockReq.payload.logger.error).toHaveBeenCalledWith( + `Error while cleaning old logs — task: ${CLEANUP_TASK_SLUG}`, + ); + expect(mockReq.payload.delete).toHaveBeenCalled(); + }); + + it('should not throw error when payload.delete fails', async () => { + mockReq.payload.delete.mockRejectedValueOnce(new Error('Database error')); + + const pluginOptions: PluginOptions = {}; + const task = cleanupLogsTask(pluginOptions); + + await expect( + typeof task.handler === 'function' && task.handler({ + // @ts-expect-error + req: mockReq, + }), + ).resolves.not.toThrow(); + }); + + it('should log error with correct message', async () => { + mockReq.payload.delete.mockRejectedValueOnce(new Error('Any error')); + + const pluginOptions: PluginOptions = {}; + const task = cleanupLogsTask(pluginOptions); + + typeof task.handler === 'function' && await task.handler({ + // @ts-expect-error + req: mockReq, + }); + + expect(mockReq.payload.logger.error).toHaveBeenCalledWith( + `Error while cleaning old logs — task: ${CLEANUP_TASK_SLUG}`, + ); + }); + }); +}); diff --git a/src/core/automation/tasks/cleanup.ts b/src/core/automation/tasks/cleanup/cleanup.ts similarity index 67% rename from src/core/automation/tasks/cleanup.ts rename to src/core/automation/tasks/cleanup/cleanup.ts index 927c438..6c682c0 100644 --- a/src/core/automation/tasks/cleanup.ts +++ b/src/core/automation/tasks/cleanup/cleanup.ts @@ -1,21 +1,23 @@ import type { TaskConfig } from 'payload'; -import auditor from './../../../collections/auditor.js'; -import type { PluginOptions } from './../../../types/pluginOptions.js'; +import auditor from '../../../../collections/auditor.js'; +import type { PluginOptions } from '../../../../types/pluginOptions.js'; -const DEFAULT_OLDER_THAN = 604800000; // 1 week -const DEFAULT_CRON = '0 3 * * *'; // At 03:00 AM +export const DEFAULT_OLDER_THAN = 604800000; // 1 week +export const DEFAULT_CRON_TIME = '0 3 * * *'; // At 03:00 AM export const DEFAULT_QUEUE_NAME = 'payload-auditor-queue'; // default queue name +export const CLEANUP_TASK_SLUG = 'cleanup-payload-auditor-log'; +export const CLEANUP_TASK_LABEL = 'payload auditor - cleanup logs'; -export const cleanupLogsTask = (pluginOptions: PluginOptions): TaskConfig<'cleanup-payload-auditor-log'> => { - const cronTime = pluginOptions.automation?.logCleanup?.cronTime ?? DEFAULT_CRON; +export const cleanupLogsTask = (pluginOptions: PluginOptions): TaskConfig => { + const cronTime = pluginOptions.automation?.logCleanup?.cronTime ?? DEFAULT_CRON_TIME; const queueName = pluginOptions.automation?.logCleanup?.queueName ?? DEFAULT_QUEUE_NAME; const olderThan = pluginOptions.automation?.logCleanup?.olderThan ?? DEFAULT_OLDER_THAN; const collectionSlug = pluginOptions.collection?.slug ?? pluginOptions.collection?.configureRootCollection?.(auditor).slug ?? 'Audit-log'; return { - slug: 'cleanup-payload-auditor-log', - label: 'payload auditor - cleanup logs', + slug: CLEANUP_TASK_SLUG, + label: CLEANUP_TASK_LABEL, schedule: [{ cron: cronTime, queue: queueName }], handler: async ({ req }) => { const millisecondsAgo = new Date(Date.now() - olderThan); @@ -27,7 +29,7 @@ export const cleanupLogsTask = (pluginOptions: PluginOptions): TaskConfig<'clean } // eslint-disable-next-line unused-imports/no-unused-vars catch (error) { - req.payload.logger.error(`Error while cleaning old logs — task: "cleanup-payload-auditor-log"`); + req.payload.logger.error(`Error while cleaning old logs — task: ${CLEANUP_TASK_SLUG}`); } return { output: {} }; diff --git a/src/core/buffer/bufferManager.test.ts b/src/core/buffer/bufferManager.test.ts index 8d4b037..22c1010 100644 --- a/src/core/buffer/bufferManager.test.ts +++ b/src/core/buffer/bufferManager.test.ts @@ -1,23 +1,26 @@ +import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { onEventLog } from './../../core/events/emitter.js'; import type { PluginOptions } from '../../types/pluginOptions.js'; -import { bufferManager } from './../../core/buffer/bufferManager.js'; import { defaultCollectionValues } from '../../Constant/Constant.js'; +import { bufferManager, bufferStore } from './../../core/buffer/bufferManager.js'; vi.mock('../../core/events/emitter.ts', () => ({ onEventLog: vi.fn(), })); -const createMock = vi.fn(); const mockPayload = { - create: createMock, + create: vi.fn(), }; +const mockOnEventLog = onEventLog as Mock; + const sampleLog = { message: 'test log', timestamp: Date.now() }; beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); + bufferStore.length = 0; }); afterEach(() => { @@ -38,13 +41,13 @@ describe('bufferManager', () => { bufferManager(mockPayload as any, pluginOptions); // simulate event listener - const handler = (onEventLog as any).mock.calls[0][1]; + const handler = mockOnEventLog.mock.calls[0][1]; await handler(sampleLog); - expect(createMock).not.toHaveBeenCalled(); + expect(mockPayload.create).not.toHaveBeenCalled(); await handler(sampleLog); - expect(createMock).toHaveBeenCalledTimes(2); + expect(mockPayload.create).toHaveBeenCalledTimes(2); }); it('should flush immediately in realtime mode', async () => { @@ -57,10 +60,10 @@ describe('bufferManager', () => { } as PluginOptions; bufferManager(mockPayload as any, pluginOptions); - const handler = (onEventLog as any).mock.calls[0][1]; + const handler = mockOnEventLog.mock.calls[0][1]; await handler(sampleLog); - expect(createMock).toHaveBeenCalledWith({ + expect(mockPayload.create).toHaveBeenCalledWith({ collection: defaultCollectionValues.slug, data: sampleLog, }); @@ -71,26 +74,25 @@ describe('bufferManager', () => { collection: { buffer: { flushStrategy: 'time', - time: '2s', + time: 2000, }, }, } as unknown as PluginOptions; bufferManager(mockPayload as any, pluginOptions); - const handler = (onEventLog as any).mock.calls[0][1]; + const handler = mockOnEventLog.mock.calls[0][1]; await handler(sampleLog); - expect(createMock).not.toHaveBeenCalled(); + expect(mockPayload.create).not.toHaveBeenCalled(); vi.advanceTimersByTime(2000); - await Promise.resolve(); // allow flushBuffer to run - expect(createMock).toHaveBeenCalledWith({ + expect(mockPayload.create).toHaveBeenCalledWith({ collection: defaultCollectionValues.slug, data: sampleLog, }); }); - it('should clear buffer after flushing', async () => { + it('should flushing logs after create log when size value is 1', async () => { const pluginOptions = { collection: { buffer: { @@ -101,13 +103,77 @@ describe('bufferManager', () => { } as PluginOptions; bufferManager(mockPayload as any, pluginOptions); - const handler = (onEventLog as any).mock.calls[0][1]; + const handler = mockOnEventLog.mock.calls[0][1]; await handler(sampleLog); - expect(createMock).toHaveBeenCalledTimes(1); + expect(mockPayload.create).toHaveBeenCalledTimes(1); // now push another and check it's flushed separately (i.e., buffer reset) await handler(sampleLog); - expect(createMock).toHaveBeenCalledTimes(2); + expect(mockPayload.create).toHaveBeenCalledTimes(2); + }); + + it('should clear buffer store after flushing by size', async () => { + const pluginOptions = { + collection: { + buffer: { + flushStrategy: 'size', + size: 2, + }, + }, + } as unknown as PluginOptions; + + bufferManager(mockPayload as any, pluginOptions); + + const handler = mockOnEventLog.mock.calls[0][1]; + + await handler({ ...sampleLog, id: 'log-1' }); + await handler({ ...sampleLog, id: 'log-2' }); + + expect(mockPayload.create).toHaveBeenCalledTimes(2); + expect(bufferStore).toHaveLength(0); + }); + + it('should clear buffer store after flushing by time', async () => { + const pluginOptions = { + collection: { + buffer: { + flushStrategy: 'time', + time: 2000, + }, + }, + } as PluginOptions; + + bufferManager(mockPayload as any, pluginOptions); + + const handler = mockOnEventLog.mock.calls[0][1]; + + await handler({ ...sampleLog, id: 'log-1' }); + await handler({ ...sampleLog, id: 'log-2' }); + + vi.advanceTimersByTime(2000); + + expect(mockPayload.create).toHaveBeenCalledTimes(2); + expect(bufferStore).toHaveLength(0); + }); + + it('should clear buffer store after flushing by realtime', async () => { + const pluginOptions = { + collection: { + buffer: { + flushStrategy: 'realtime', + }, + }, + } as PluginOptions; + + bufferManager(mockPayload as any, pluginOptions); + + const handler = mockOnEventLog.mock.calls[0][1]; + + await handler({ ...sampleLog, id: 'log-1' }); + await handler({ ...sampleLog, id: 'log-2' }); + + expect(mockPayload.create).toHaveBeenCalledTimes(2); + expect(bufferStore).toHaveLength(0); }); }); diff --git a/src/core/buffer/bufferManager.ts b/src/core/buffer/bufferManager.ts index ae0e0f1..c9e1d4d 100644 --- a/src/core/buffer/bufferManager.ts +++ b/src/core/buffer/bufferManager.ts @@ -2,18 +2,21 @@ import type { Payload } from 'payload'; import { onEventLog } from './../../core/events/emitter.js'; import type { AuditorLog } from '../../collections/auditor.js'; -import type { PluginOptions } from './../../types/pluginOptions.js'; import { defaultCollectionValues } from './../../Constant/Constant.js'; +import type { BufferConfig, PluginOptions } from './../../types/pluginOptions.js'; import { handleBufferDebugMode } from './../../core/buffer/helpers/handleBufferDebugMode.js'; -const DEFAULT_INTERVAL_BUFFER = 10000; -const store: AuditorLog[] = []; +export const DEFAULT_INTERVAL_BUFFER = 10000 as NonNullable; +export const DEFAULT_BUFFER_SIZE = 10 as NonNullable; +export const DEFAULT_BUFFER_STRATEGY = 'time' as NonNullable; + +export const bufferStore: AuditorLog[] = []; let payloadInstance: Payload; const flushBuffer = async (pluginOptions: PluginOptions) => { - const logsToInsert = [...store]; - store.length = 0; + const logsToInsert = [...bufferStore]; + bufferStore.length = 0; await Promise.all( logsToInsert.map(log => payloadInstance.create({ @@ -28,18 +31,18 @@ const flushBuffer = async (pluginOptions: PluginOptions) => { export const bufferManager = (payload: Payload, pluginOptions: PluginOptions) => { const bufferConfig = pluginOptions.collection?.buffer; - const size = bufferConfig?.size ?? 10; + const size = bufferConfig?.size ?? DEFAULT_BUFFER_SIZE; const interval = bufferConfig?.time ?? DEFAULT_INTERVAL_BUFFER; - const flushStrategy = bufferConfig?.flushStrategy ?? 'time'; + const flushStrategy = bufferConfig?.flushStrategy ?? DEFAULT_BUFFER_STRATEGY; payloadInstance = payload; // When the log is generated, add it to the buffer. onEventLog('logGenerated', async (log: AuditorLog) => { handleBufferDebugMode({ flushStrategy, interval, size }, bufferConfig); - store.push(log); + bufferStore.push(log); if (flushStrategy === 'size') { - if (store.length >= size) { + if (bufferStore.length >= size) { await flushBuffer(pluginOptions); } } @@ -51,7 +54,7 @@ export const bufferManager = (payload: Payload, pluginOptions: PluginOptions) => if (flushStrategy === 'time') { // Every few seconds, empty the buffer (even if it's not full) setInterval(async () => { - if (store.length > 0) { + if (bufferStore.length > 0) { await flushBuffer(pluginOptions); } }, interval); diff --git a/src/core/buffer/helpers/handleBufferDebugMode.test.ts b/src/core/buffer/helpers/handleBufferDebugMode.test.ts new file mode 100644 index 0000000..8205771 --- /dev/null +++ b/src/core/buffer/helpers/handleBufferDebugMode.test.ts @@ -0,0 +1,263 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { prettyDebugLog } from '../../../utils/prettyDebugLog.js'; +import { handleBufferDebugMode } from './handleBufferDebugMode.js'; +import type { BufferConfig, BufferDebugFields } from '../../../types/pluginOptions.js'; +import { DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_STRATEGY, DEFAULT_INTERVAL_BUFFER } from '../bufferManager.js'; + +vi.mock('../../../utils/prettyDebugLog.js'); + +const mockFields = { + flushStrategy: DEFAULT_BUFFER_STRATEGY, + size: DEFAULT_BUFFER_SIZE, + interval: DEFAULT_INTERVAL_BUFFER, +} satisfies Record; + +describe('handleBufferDebugMode', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('when debug is disabled', () => { + it('should return null and not call prettyDebugLog', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: false, + }, + }, + }; + + const result = handleBufferDebugMode(mockFields, bufferConfig); + + expect(result).toBeNull(); + expect(prettyDebugLog).not.toHaveBeenCalled(); + }); + + it('should return null when bufferConfig is undefined', () => { + const result = handleBufferDebugMode(mockFields, undefined); + + expect(result).toBeNull(); + expect(prettyDebugLog).not.toHaveBeenCalled(); + }); + + it('should return null when bufferConfig.modes is undefined', () => { + const bufferConfig: BufferConfig = {}; + + const result = handleBufferDebugMode(mockFields, bufferConfig); + + expect(result).toBeNull(); + expect(prettyDebugLog).not.toHaveBeenCalled(); + }); + }); + + describe('when debug is enabled', () => { + it('should call prettyDebugLog with all fields when debugFields is not specified', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledTimes(1); + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + mockFields, + 'table', + ); + }); + + it('should filter fields based on debugFields configuration', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + fields: { + size: true, + }, + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + const expectedFiltered = { + size: 10, + }; + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + expectedFiltered, + 'table', + ); + }); + + it('should use custom displayType when provided', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + displayType: 'manual', + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + mockFields, + 'manual', + ); + }); + + it('should use custom fields when provided in debug configuration', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + fields: { + size: true, + interval: true, + }, + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + const expectedFiltered = { + size: 10, + interval: 10000, + }; + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + expectedFiltered, + 'table', + ); + }); + }); + + describe('edge cases', () => { + it('should handle empty fields object', () => { + const emptyFields = {} as Record; + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + }, + }, + }; + + handleBufferDebugMode(emptyFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + emptyFields, + 'table', + ); + }); + + it('should handle debugFields with all false values', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + fields: { + size: false, + flushStrategy: false, + interval: false, + }, + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + {}, + 'table', + ); + }); + + it('should handle undefined displayType', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + displayType: undefined, + }, + }, + }; + + handleBufferDebugMode(mockFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledWith( + 'Buffer', + '', + mockFields, + 'table', + ); + }); + }); + + describe('multiple calls', () => { + it('should work correctly with multiple calls', () => { + const bufferConfig: BufferConfig = { + modes: { + debug: { + enabled: true, + fields: { + size: true, + interval: true, + }, + }, + }, + }; + + // فراخوانی اول + handleBufferDebugMode(mockFields, bufferConfig); + + // فراخوانی دوم با داده‌های متفاوت + const otherFields = { + ...mockFields, + size: 2048, + interval: 50000, + }; + handleBufferDebugMode(otherFields, bufferConfig); + + expect(prettyDebugLog).toHaveBeenCalledTimes(2); + + const expectedFirstFilter = { size: 10, interval: 10000 }; + const expectedSecondFilter = { size: 2048, interval: 50000 }; + + expect(prettyDebugLog).toHaveBeenNthCalledWith( + 1, + 'Buffer', + '', + expectedFirstFilter, + 'table', + ); + + expect(prettyDebugLog).toHaveBeenNthCalledWith( + 2, + 'Buffer', + '', + expectedSecondFilter, + 'table', + ); + }); + }); +}); diff --git a/src/core/buffer/helpers/handleBufferDebugMode.ts b/src/core/buffer/helpers/handleBufferDebugMode.ts index 88cc636..4391eb2 100644 --- a/src/core/buffer/helpers/handleBufferDebugMode.ts +++ b/src/core/buffer/helpers/handleBufferDebugMode.ts @@ -11,7 +11,8 @@ export const handleBufferDebugMode = ( return null; } const debugFields = bufferConfig?.modes?.debug?.fields ?? fields; - const displayType = bufferConfig?.modes?.debug?.displayType; + const displayType = bufferConfig?.modes?.debug?.displayType ?? 'table'; + const debugLog = Object.fromEntries( Object.entries(fields).filter(([key]) => debugFields[key as keyof BufferDebugFields]), ); diff --git a/src/core/log-builders/collections/helpers/emitWrapper.ts b/src/core/log-builders/collections/helpers/emitWrapper/emitWrapper.ts similarity index 89% rename from src/core/log-builders/collections/helpers/emitWrapper.ts rename to src/core/log-builders/collections/helpers/emitWrapper/emitWrapper.ts index ff4da30..53e1ead 100644 --- a/src/core/log-builders/collections/helpers/emitWrapper.ts +++ b/src/core/log-builders/collections/helpers/emitWrapper/emitWrapper.ts @@ -1,11 +1,11 @@ -import { emitEvent } from './../../../../core/events/emitter.js'; -import type { AuditorLog } from './../../../../collections/auditor.js'; +import { emitEvent } from '../../../../events/emitter.js'; +import type { AuditorLog } from '../../../../../collections/auditor.js'; import type { AllCollectionHooks, HookOperationConfig, HookTrackingOperationMap, PluginOptions, -} from './../../../../types/pluginOptions.js'; +} from '../../../../../types/pluginOptions.js'; export const emitWrapper = async ( logData: AuditorLog, diff --git a/src/core/log-builders/collections/hooks.ts b/src/core/log-builders/collections/hooks.ts index 31fa1cc..c24d6cf 100644 --- a/src/core/log-builders/collections/hooks.ts +++ b/src/core/log-builders/collections/hooks.ts @@ -20,8 +20,8 @@ import type { } from 'payload'; import type { SharedArgs } from './shared.js'; -import { emitWrapper } from './helpers/emitWrapper.js'; import { handleDebugMode } from './helpers/handleDebugMode.js'; +import { emitWrapper } from './helpers/emitWrapper/emitWrapper.js'; import type { AuditorLog } from './../../../collections/auditor.js'; import type { AllCollectionHooks, diff --git a/src/core/log-builders/collections/shared.ts b/src/core/log-builders/collections/shared.ts index bc30083..234c0d6 100644 --- a/src/core/log-builders/collections/shared.ts +++ b/src/core/log-builders/collections/shared.ts @@ -1,8 +1,8 @@ import type { PayloadRequest, RequestContext, SanitizedCollectionConfig } from 'payload'; import { hookHandlers } from './hooks.js'; -import { emitWrapper } from './helpers/emitWrapper.js'; import { handleDebugMode } from './helpers/handleDebugMode.js'; +import { emitWrapper } from './helpers/emitWrapper/emitWrapper.js'; import type { AuditorLog } from './../../../collections/auditor.js'; import { checkOperationEnabled } from './helpers/isOperationEnabled.js'; import type { hookTypes } from './../../../pluginUtils/configHelpers.js'; @@ -51,6 +51,7 @@ export const sharedLogic = async ( const userHookOperationConfig = ( userHookConfig as Record | undefined> )?.[sharedArgs.operation]; + const isOperationEnabled = checkOperationEnabled( userHookOperationConfig, userHookConfig, diff --git a/src/index.ts b/src/index.ts index 649a908..51d0792 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import type { Config, Plugin } from 'payload'; import { defaultPluginOpts } from './Constant/Constant.js'; import type { PluginOptions } from './types/pluginOptions.js'; -import { cleanupLogsTask } from './core/automation/tasks/cleanup.js'; +import { cleanupLogsTask } from './core/automation/tasks/cleanup/cleanup.js'; import { attachCollectionConfig, buildAccessControl, diff --git a/src/pluginUtils/configHelpers.ts b/src/pluginUtils/configHelpers.ts index 0a20192..8980fd7 100644 --- a/src/pluginUtils/configHelpers.ts +++ b/src/pluginUtils/configHelpers.ts @@ -2,8 +2,8 @@ import type { Access, BasePayload, Config } from 'payload'; import auditor from '../collections/auditor.js'; import { bufferManager } from './../core/buffer/bufferManager.js'; -import { DEFAULT_QUEUE_NAME } from './../core/automation/tasks/cleanup.js'; import { defaultCollectionValues, hookMap } from './../Constant/Constant.js'; +import { DEFAULT_QUEUE_NAME } from '../core/automation/tasks/cleanup/cleanup.js'; import type { AllCollectionHooks, PluginOptions } from './../types/pluginOptions.js'; type AccessOps = 'create' | 'delete' | 'read' | 'update'; From 6bf05758c36237c861cf63eaad896182e745bf88 Mon Sep 17 00:00:00 2001 From: shaadcode Date: Sat, 18 Jul 2026 15:46:13 +0330 Subject: [PATCH 2/2] fix(build): wrong script to install dependencies --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 943e7f9..e813216 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -17,7 +17,7 @@ jobs: uses: oven-sh/setup-bun@v2 - name: Install dependencies - run: bun --frozen-lockfile + run: bun i --frozen-lockfile - name: Build project run: bun build:safe