diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..47dfc96 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,38 @@ +name: Publish to npm + +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org/ + + - name: Upgrade npm + run: npm install -g npm@latest + + - name: Check if version already on npm + id: check + run: | + VERSION=$(node -p "require('./package.json').version") + if npm view strapi-v5-plugin-populate-deep@$VERSION version 2>/dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Publish + if: steps.check.outputs.exists == 'false' + run: npm publish --provenance --access public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..981635f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Create Release + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Bump version and create tag + id: tag + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + default_bump: patch + release_branches: main + + - name: Update package.json version + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + pkg.version = '${{ steps.tag.outputs.new_version }}'; + fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add package.json + git commit -m "chore: release v${{ steps.tag.outputs.new_version }}" + git push + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ steps.tag.outputs.new_tag }} \ + --generate-notes \ + --title "${{ steps.tag.outputs.new_tag }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index a2e74ba..7045922 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,92 @@ -# Strapi plugin populate-deep -This plugin allows for easier population of deep content structures using the rest API. - -# Installation - -`npm install strapi-plugin-populate-deep` - -`yarn add strapi-plugin-populate-deep` - - -# Usages - -## Examples +# strapi-v5-plugin-populate-deep + +A Strapi v5 plugin that enables deep population of nested content structures via a simple query parameter. + +[![npm version](https://img.shields.io/npm/v/strapi-v5-plugin-populate-deep?label=version&color=blue)](https://www.npmjs.com/package/strapi-v5-plugin-populate-deep) +[![npm downloads](https://img.shields.io/npm/dm/strapi-v5-plugin-populate-deep?color=brightgreen)](https://www.npmjs.com/package/strapi-v5-plugin-populate-deep) +[![npm total downloads](https://img.shields.io/npm/dt/strapi-v5-plugin-populate-deep)](https://www.npmjs.com/package/strapi-v5-plugin-populate-deep) +[![license](https://img.shields.io/npm/l/strapi-v5-plugin-populate-deep)](./LICENSE) +[![node](https://img.shields.io/node/v/strapi-v5-plugin-populate-deep)](https://www.npmjs.com/package/strapi-v5-plugin-populate-deep) +[![install size](https://packagephobia.com/badge?p=strapi-v5-plugin-populate-deep)](https://packagephobia.com/result?p=strapi-v5-plugin-populate-deep) +[![GitHub stars](https://img.shields.io/github/stars/NEDDL/strapi-v5-plugin-populate-deep?style=social)](https://github.com/NEDDL/strapi-v5-plugin-populate-deep/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/NEDDL/strapi-v5-plugin-populate-deep?style=social)](https://github.com/NEDDL/strapi-v5-plugin-populate-deep/network/members) +[![GitHub issues](https://img.shields.io/github/issues/NEDDL/strapi-v5-plugin-populate-deep)](https://github.com/NEDDL/strapi-v5-plugin-populate-deep/issues) +[![GitHub last commit](https://img.shields.io/github/last-commit/NEDDL/strapi-v5-plugin-populate-deep)](https://github.com/NEDDL/strapi-v5-plugin-populate-deep/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/NEDDL/strapi-v5-plugin-populate-deep)](https://github.com/NEDDL/strapi-v5-plugin-populate-deep/graphs/contributors) +[![Strapi v5](https://img.shields.io/badge/Strapi-v5-8C4BFF?logo=strapi)](https://strapi.io) + +## Installation + +```bash +npm install strapi-v5-plugin-populate-deep +# or +yarn add strapi-v5-plugin-populate-deep +``` -Populate a request with the default max depth. +## Usage -`/api/articles?populate=deep` +Add `pLevel` to any API request to deeply populate the response. -Populate a request with the a custom depth +| Parameter | Type | Description | +|-----------|------|-------------| +| `pLevel` | `number` (optional) | Depth of population. Omit the value to use the default depth. | +| `pIgnore` | `string` or `string[]` (optional) | Fields or collection names to exclude from population. Accepts a comma-separated string or array. | -`/api/articles?populate=deep,10` +### Examples -Populate a request with the a custom depth +``` +# Use default depth (5) +GET /api/articles?pLevel -`/api/articles/1?populate=deep,10` +# Use custom depth +GET /api/articles?pLevel=10 -## Good to know +# Ignore specific fields (comma-separated string) +GET /api/articles?pLevel=5&pIgnore=author,tags -The default max depth is 5 levels deep. +# Ignore specific fields (array syntax) +GET /api/articles?pLevel=5&pIgnore[0]=author&pIgnore[1]=tags +``` -The populate deep option is available for all collections and single types using the findOne and findMany methods. +## Configuration -# Configuration +Customize the default depth globally via `config/plugins.js` (or `.ts`): -The default depth can be customized via the plugin config. To do so create or edit you plugins.js file. +```js +// config/plugins.js +module.exports = ({ env }) => ({ + 'strapi-v5-plugin-populate-deep': { + config: { + defaultDepth: 3, // default: 5 + }, + }, +}); +``` -## Example configuration +## Good to Know -`config/plugins.js` +- Default depth is **5** unless configured otherwise. +- Works for all collections and single types (`findOne` and `findMany`). +- `pIgnore` prevents circular population and can significantly reduce response size and query time. +- Increasing depth may result in longer response times — use `pIgnore` to offset this. +- `plugin::upload.file` related field is always excluded to avoid bloated responses. +- `admin::user` (creator fields) can be excluded via `skipCreatorFields` config: -``` +```js module.exports = ({ env }) => ({ - 'strapi-plugin-populate-deep': { + 'strapi-v5-plugin-populate-deep': { config: { - defaultDepth: 3, // Default is 5 - } + defaultDepth: 5, + skipCreatorFields: true, + }, }, }); ``` -# Contributions -The original idea for getting the populate structure was created by [tomnovotny7](https://github.com/tomnovotny7) and can be found in [this](https://github.com/strapi/strapi/issues/11836) github thread +## Contributors + + + + + +Based on the original work by [Barelydead](https://github.com/Barelydead/strapi-plugin-populate-deep). Original populate concept by [tomnovotny7](https://github.com/tomnovotny7) ([thread](https://github.com/strapi/strapi/issues/11836)). Dynamic zone fix by [tooonuch](https://github.com/tooonuch). diff --git a/package-lock.json b/package-lock.json index 8d9da43..48ec0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,17 @@ { - "name": "strapi-plugin-populate-deep", - "version": "3.0.1", - "lockfileVersion": 1 -} \ No newline at end of file + "name": "strapi-v5-plugin-populate-deep", + "version": "4.3.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "strapi-v5-plugin-populate-deep", + "version": "4.3.1", + "license": "MIT", + "engines": { + "node": ">=18.0.0 <=24.x.x", + "npm": ">=6.0.0" + } + } + } +} diff --git a/package.json b/package.json index 4859498..58de880 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,32 @@ { - "name": "strapi-plugin-populate-deep", - "version": "3.0.1", - "description": "Strapi plugin that populates nested content.", + "name": "strapi-v5-plugin-populate-deep", + "version": "4.3.1", + "description": "Strapi v5 plugin that populates nested content.", "strapi": { - "name": "strapi-plugin-populate-deep", - "description": "Api helper to populate deep content structures.", + "name": "strapi-v5-plugin-populate-deep", + "description": "API helper to populate deep content structures for Strapi v5.", "kind": "plugin" }, - "dependencies": {}, "author": { - "name": "Christofer Jungberg" + "name": "Mustafa ONAL" }, + "contributors": [ + "Christofer Jungberg", + "Mustafa ONAL", + "chohner" + ], "maintainers": [ { - "name": "Christofer Jungberg" + "name": "Mustafa ONAL" } ], "repository": { "type": "git", - "url": "https://github.com/Barelydead/strapi-plugin-deep-populate" + "url": "https://github.com/NEDDL/strapi-v5-plugin-populate-deep" }, "engines": { - "node": ">=14.19.1 <=20.x.x", + "node": ">=18.0.0 <=24.x.x", "npm": ">=6.0.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/server/bootstrap.js b/server/bootstrap.js index cb357c6..12c1b9e 100644 --- a/server/bootstrap.js +++ b/server/bootstrap.js @@ -1,18 +1,30 @@ -'use strict'; -const { getFullPopulateObject } = require('./helpers') +"use strict"; +const { getFullPopulateObject } = require("./helpers"); module.exports = ({ strapi }) => { - // Subscribe to the lifecycles that we are intrested in. + const defaultDepth = + strapi.plugin("strapi-v5-plugin-populate-deep")?.config("defaultDepth") || + 5; + strapi.db.lifecycles.subscribe((event) => { - if (event.action === 'beforeFindMany' || event.action === 'beforeFindOne') { - const populate = event.params?.populate; - const defaultDepth = strapi.plugin('strapi-plugin-populate-deep')?.config('defaultDepth') || 5 + const { action, model, params } = event; + + if (!["beforeFindMany", "beforeFindOne"].includes(action)) return; + if (!model.uid.startsWith("api::")) return; + + const ctx = strapi.requestContext.get(); + if (!ctx?.request?.url?.startsWith("/api/")) return; + + const pLevel = params?.pLevel ?? ctx.query?.pLevel; + if (pLevel === undefined) return; + + const pIgnore = params?.pIgnore ?? ctx.query?.pIgnore ?? []; + const ignore = typeof pIgnore === 'string' ? pIgnore.split(',').map(s => s.trim()) : Array.isArray(pIgnore) ? pIgnore : [pIgnore]; - if (populate && populate[0] === 'deep') { - const depth = populate[1] ?? defaultDepth - const modelObject = getFullPopulateObject(event.model.uid, depth, []); - event.params.populate = modelObject.populate - } + const depth = pLevel ? parseInt(pLevel, 10) : defaultDepth; + const populateObj = getFullPopulateObject(model.uid, depth, ignore); + if (populateObj && populateObj !== true) { + params.populate = populateObj.populate; } }); }; diff --git a/server/helpers/index.js b/server/helpers/index.js index 7f026b8..2dca138 100644 --- a/server/helpers/index.js +++ b/server/helpers/index.js @@ -1,4 +1,28 @@ -const { isEmpty, merge } = require("lodash/fp"); +const isEmpty = (obj) => Object.keys(obj).length === 0; + +const deepAssign = (target, source) => { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + if (typeof source[key] === "object" && source[key] !== null) { + if ( + !target[key] || + typeof target[key] !== "object" || + target[key] === null + ) { + target[key] = source[key]; + } + deepAssign(target[key], source[key]); + } else if ( + !target[key] || + typeof target[key] !== "object" || + target[key] === null + ) { + target[key] = source[key]; + } + } + } + return target; +}; const getModelPopulationAttributes = (model) => { if (model.uid === "plugin::upload.file") { @@ -10,39 +34,46 @@ const getModelPopulationAttributes = (model) => { }; const getFullPopulateObject = (modelUid, maxDepth = 20, ignore) => { - const skipCreatorFields = strapi.plugin('strapi-plugin-populate-deep')?.config('skipCreatorFields'); - if (maxDepth <= 1) { return true; } - if (modelUid === "admin::user" && skipCreatorFields) { + + if (modelUid === "admin::user" && strapi + .plugin("strapi-v5-plugin-populate-deep") + ?.config("skipCreatorFields")) { return undefined; } const populate = {}; const model = strapi.getModel(modelUid); - if (ignore && !ignore.includes(model.collectionName)) ignore.push(model.collectionName) + if (ignore && !ignore.includes(model.collectionName)) + ignore.push(model.collectionName); for (const [key, value] of Object.entries( getModelPopulationAttributes(model) )) { - if (ignore?.includes(key)) continue + if (ignore?.includes(key) || value.private === true) continue; if (value) { if (value.type === "component") { - populate[key] = getFullPopulateObject(value.component, maxDepth - 1); + populate[key] = getFullPopulateObject(value.component, maxDepth - 1, [...ignore]); } else if (value.type === "dynamiczone") { const dynamicPopulate = value.components.reduce((prev, cur) => { - const curPopulate = getFullPopulateObject(cur, maxDepth - 1); - return curPopulate === true ? prev : merge(prev, curPopulate); + const curPopulate = getFullPopulateObject(cur, maxDepth - 1, [...ignore]); + return curPopulate === undefined ? prev : deepAssign(prev, { [cur]: curPopulate }); }, {}); - populate[key] = isEmpty(dynamicPopulate) ? true : dynamicPopulate; + populate[key] = isEmpty(dynamicPopulate) ? true : { on: dynamicPopulate }; } else if (value.type === "relation") { - const relationPopulate = getFullPopulateObject( - value.target, - (key === 'localizations') && maxDepth > 2 ? 1 : maxDepth - 1, - ignore - ); - if (relationPopulate) { - populate[key] = relationPopulate; + if (key === "localizations") { + populate[key] = true; + } else { + if (ignore?.includes(strapi.getModel(value.target).collectionName)) continue; + const relationPopulate = getFullPopulateObject( + value.target, + maxDepth - 1, + [...ignore] + ); + if (relationPopulate) { + populate[key] = relationPopulate; + } } } else if (value.type === "media") { populate[key] = true; @@ -53,5 +84,5 @@ const getFullPopulateObject = (modelUid, maxDepth = 20, ignore) => { }; module.exports = { - getFullPopulateObject -} + getFullPopulateObject, +}; diff --git a/server/index.js b/server/index.js index cd752b6..960a686 100644 --- a/server/index.js +++ b/server/index.js @@ -5,5 +5,14 @@ const config = require('./config') module.exports = { bootstrap, - config + config, + register: ({ strapi }) => { + // addQueryParams was introduced in Strapi 5.37 — skip gracefully on older versions + if (typeof strapi.contentAPI?.addQueryParams === 'function') { + strapi.contentAPI.addQueryParams({ + pLevel: { schema: (z) => z.string().max(3).optional() }, + pIgnore: { schema: (z) => z.string().optional() }, + }); + } + }, }; \ No newline at end of file