{"_id":"@ace-ams/payload-purge","name":"@ace-ams/payload-purge","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.1":{"name":"@ace-ams/payload-purge","version":"0.1.1","description":"Purge unused documents from your Payload CMS collections","license":"MIT","type":"module","repository":{"type":"git","url":"git+https://github.com/ace-ams/payload-plugin-purge.git"},"bugs":{"url":"https://github.com/ace-ams/payload-plugin-purge/issues"},"homepage":"https://github.com/ace-ams/payload-plugin-purge#readme","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"import":"./dist/exports/client.js","types":"./dist/exports/client.d.ts","default":"./dist/exports/client.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"build":"npm run copyfiles && npm run build:types && npm run build:swc","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","build:types":"tsc --outDir dist --rootDir ./src","clean":"rimraf {dist,*.tsbuildinfo}","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","dev":"next dev dev --turbo","dev:generate-importmap":"npm run dev:payload generate:importmap","dev:generate-types":"npm run dev:payload generate:types","dev:payload":"cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload","generate:importmap":"npm run dev:generate-importmap","generate:types":"npm run dev:generate-types","lint":"eslint","lint:fix":"eslint ./src --fix","test":"npm test:int && npm test:e2e","test:e2e":"playwright test","test:int":"vitest"},"devDependencies":{"@eslint/eslintrc":"^3.2.0","@payloadcms/db-mongodb":"^3.37.0","@payloadcms/db-postgres":"^3.37.0","@payloadcms/db-sqlite":"^3.37.0","@payloadcms/eslint-config":"^3.9.0","@payloadcms/next":"^3.37.0","@payloadcms/richtext-lexical":"^3.37.0","@playwright/test":"^1.58.2","@swc-node/register":"^1.10.9","@swc/cli":"^0.6.0","@types/node":"^22.19.9","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","copyfiles":"^2.4.1","cross-env":"^7.0.3","eslint":"^9.23.0","eslint-config-next":"^16.2.1","graphql":"^16.8.1","mongodb-memory-server":"^10.1.4","next":"^16.2.1","open":"^10.1.0","payload":"^3.37.0","prettier":"^3.4.2","qs-esm":"^8.0.1","react":"^19.2.4","react-dom":"^19.2.4","rimraf":"^3.0.2","sharp":"^0.34.2","sort-package-json":"^2.10.0","typescript":"^5.7.3","vite-tsconfig-paths":"^6.0.5","vitest":"^4.0.18"},"peerDependencies":{"@payloadcms/ui":"^3.37.0","payload":"^3.37.0"},"engines":{"node":"^24.0.0"},"publishConfig":{"access":"public"},"registry":"https://registry.npmjs.org/","dependencies":{},"gitHead":"1332004fa3b2136f8304f283fc24aee4119f694d","_id":"@ace-ams/payload-purge@0.1.1","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-SFsjk77NpX/MZaxNyuZFEi1QdVKNoP33I7qqZdqCfwMt60NeM/KL8dJ+1iCiSLpHOKex4zzEwgRQ7V7cBPzNQA==","shasum":"30dd194697864b9aa7b5e1a11879dfbac632b014","tarball":"https://registry.npmjs.org/@ace-ams/payload-purge/-/payload-purge-0.1.1.tgz","fileCount":14,"unpackedSize":35531,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ace-ams%2fpayload-purge@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAk+9XpAFNDw5i8+AUQjke/bSy09EBQ32zB8w6HzzzjtAiA5BL/8Hxyx5imJ/qB3/+SGlPgo7CouAWxyvAfVZYBorA=="}]},"_npmUser":{"name":"codename-niels","email":"nielsjlwijers@gmail.com"},"directories":{},"maintainers":[{"name":"rens-born05","email":"rens@born05.com"},{"name":"tazio","email":"personal@tazio.nl"},{"name":"codename-niels","email":"nielsjlwijers@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payload-purge_0.1.1_1776174190702_0.4733800048759267"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-14T13:43:10.572Z","0.1.1":"2026-04-14T13:43:10.838Z","modified":"2026-04-14T13:43:11.200Z"},"maintainers":[{"name":"rens-born05","email":"rens@born05.com"},{"name":"tazio","email":"personal@tazio.nl"},{"name":"codename-niels","email":"nielsjlwijers@gmail.com"}],"description":"Purge unused documents from your Payload CMS collections","homepage":"https://github.com/ace-ams/payload-plugin-purge#readme","repository":{"type":"git","url":"git+https://github.com/ace-ams/payload-plugin-purge.git"},"bugs":{"url":"https://github.com/ace-ams/payload-plugin-purge/issues"},"license":"MIT","readme":"# Payload Purge\n\nA [Payload CMS](https://payloadcms.com) plugin that adds a **Purge Unused** button to any collection's list view. When triggered, it scans all other collections and globals for references and permanently deletes any documents that are not referenced anywhere.\n\n## Installation\n\n```sh\nnpm i @ace-ams/payload-purge\n```\n\n## Setup\n\nAdd the plugin to your `payload.config.ts` and pass the slugs of the collections you want to enable purging on:\n\n```ts\nimport { payloadPurge } from '@ace-ams/payload-purge'\nimport { buildConfig } from 'payload'\n\nexport default buildConfig({\n  plugins: [\n    payloadPurge({\n      collections: ['media'],\n    }),\n  ],\n})\n```\n\nThat's it. A **Purge Unused** button will appear in the list view toolbar of every enabled collection.\n\n## Options\n\n| Option        | Type                                                   | Default    | Description                                                                              |\n| ------------- | ------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------- |\n| `collections` | `CollectionSlug[]`                                     | —          | Array of collection slugs to enable the purge feature on.                                |\n| `disabled`    | `boolean`                                              | `false`    | Set to `true` to disable the plugin without removing it from your config.                |\n| `access`      | `(req: PayloadRequest) => boolean \\| Promise<boolean>` | allow all  | Optional access control function. Return `false` to deny the request with a `403`.      |\n| `beforePurge` | `(args: BeforePurgeArgs) => void \\| Promise<void>`     | —          | Hook called before any documents are deleted.                                            |\n| `afterPurge`  | `(args: AfterPurgeArgs) => void \\| Promise<void>`      | —          | Hook called after all unused documents have been deleted.                                |\n\n### `access`\n\nBy default any authenticated user can trigger a purge. Use `access` to restrict it further — for example, to admins only:\n\n```ts\npayloadPurge({\n  collections: ['media'],\n  access: (req) => req.user?.role === 'admin',\n})\n```\n\n### `beforePurge`\n\nCalled after the unused IDs have been identified but before any deletions are performed. Receives the target collection slug and the current request.\n\n```ts\npayloadPurge({\n  collections: ['media'],\n  beforePurge: async ({ collectionSlug, req }) => {\n    req.payload.logger.info(`About to purge unused docs from \"${collectionSlug}\"`)\n  },\n})\n```\n\n### `afterPurge`\n\nCalled after all deletions are complete. Receives the collection slug, total deleted count, the request, and the list of deleted IDs.\n\n```ts\npayloadPurge({\n  collections: ['media'],\n  afterPurge: async ({ collectionSlug, deletedCount, unusedIds, req }) => {\n    req.payload.logger.info(`Purged ${deletedCount} doc(s) from \"${collectionSlug}\": ${unusedIds.join(', ')}`)\n  },\n})\n```\n\n## How it works\n\n1. **Authentication** — Only logged-in users may call the purge endpoint. Unauthenticated requests receive a `401`.\n2. **Rate limiting** — Each user is limited to one purge every 30 seconds. Subsequent requests within that window receive a `429` with the number of seconds remaining.\n3. **Access control** — If an `access` function is provided, it is called next. Returning `false` results in a `403`.\n4. **`beforePurge` hook** — Fired before any database work begins.\n5. **ID collection** — All document IDs in the target collection are fetched in batches of 100 to avoid loading large collections into memory at once.\n6. **Reference scanning** — Every other collection and every global is scanned page-by-page for any occurrence of those IDs. Scanning stops early once all IDs are confirmed as referenced.\n7. **Deletion** — Unreferenced documents are deleted one by one, fully respecting the collection's own `access.delete` rules.\n8. **`afterPurge` hook** — Fired after all deletions complete.\n\n## Security\n\n- The purge endpoint requires an authenticated session (`401` otherwise).\n- A per-user in-memory rate limit of 30 seconds prevents rapid repeated calls.\n- Deletions run with `overrideAccess: false` — if a user does not have delete permission on a document according to the collection's own access rules, that document will not be deleted.\n- Reference scanning runs with elevated access so that no references are missed due to read restrictions on other collections.\n- The plugin logs all purge failures server-side via `payload.logger.error` without leaking internal details to the client.\n\n---\n\n## Development\n\n1. Clone the repository and run `npm install` to install dependencies.\n2. Run `npm dev` to start the local Payload dev environment at `http://localhost:3000`.\n3. Make changes inside `src/` — the dev project in `dev/` picks them up automatically.\n4. Run `npm test:int` to run integration tests and `npm test:e2e` for end-to-end tests.\n5. Run `npm build` to compile the plugin to `dist/` before publishing.\n","readmeFilename":"README.md","_rev":"1-b61021990ea1f87a0ba4a3140b5d6c66"}