strapi-plugin-recursive-populate
v1.0.0
Published
Deep recursive population for Strapi content type queries
Maintainers
Readme
Strapi Plugin - Recursive Populate
A Strapi v5 plugin that adds deep recursive population for content type queries. Use the special populate=*.* value to fully populate every relation, component, dynamic zone, and media field — automatically — without hand-writing nested populate objects.
🚀 Features
- 🌳 Deep, recursive populate for components, dynamic zones, relations, and media
- 🎯 Triggered by
populate=*.*— works in REST API requests and in programmaticstrapi.documents()/ service calls - ⚙️ Configurable max recursion depth and max locale-traversal depth
- 🪶 Zero admin UI, zero database tables, zero runtime overhead when not invoked
- 🧩 Headless: enable in
config/plugins.tsand you're done
⚙️ Installation
# Using npm
npm install strapi-plugin-recursive-populate
# Using yarn
yarn add strapi-plugin-recursive-populate🔧 Configuration
Enable and configure the plugin in config/plugins.ts (or config/plugins.js):
export default ({ env }) => ({
'recursive-populate': {
enabled: true,
config: {
maxDepth: 20,
maxLocale: 1,
},
},
});Options
| Option | Type | Default | Description |
| ----------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| maxDepth | number | 20 | Maximum recursion depth when expanding components / relations / dynamic zones. Higher values populate more, at increased query cost. |
| maxLocale | number | 1 | How many levels of relation traversal are allowed before the recursion bottoms out. 0 populates localizations only, no deep relation walk. |
🎯 Usage
REST API
Send populate=*.* as the populate value on any find / findOne request:
GET /api/pages?populate=*.*
GET /api/pages/abc123?populate=*.*The plugin intercepts the query, builds a fully-recursive populate object based on the content type's schema, and hands it to Strapi's core controller.
Programmatic (services / lifecycle hooks)
const result = await strapi.documents('api::page.page').findOne({
documentId,
populate: '*.*',
});The same populate=*.* marker works through the document service. The plugin's document middleware detects it and rewrites params.populate with the full recursive tree.
Building a populate object directly
If you need the populate object without executing a query (for caching, inspection, or composition):
const populateObj = strapi
.plugin('recursive-populate')
.service('population')
.getFullPopulate('api::page.page');
// Optionally override config values per-call:
const shallow = strapi
.plugin('recursive-populate')
.service('population')
.getFullPopulate('api::page.page', 5, 0);Wrapping an existing core service
The plugin exposes find and findOne helpers that wrap a core service call with deep-populate handling and reset localizations arrays on the result:
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::page.page', ({ strapi }) => ({
async findOne(ctx) {
const { id } = ctx.params;
const populationService = strapi.plugin('recursive-populate').service('population');
const coreService = strapi.service('api::page.page').findOne;
const data = await populationService.findOne(coreService, 'api::page.page', id, ctx.query);
return { data };
},
}));🧠 How It Works
The plugin walks the requested model's schema (not its data) to build a populate tree:
- Components are recursively expanded
- Dynamic zones are expanded per-component into a
{ on: { ... } }map - Relations are expanded as long as
maxLocalebudget remains - Media fields are populated as
true admin::userand therelatedfield onplugin::upload.fileare excluded
Recursion is bounded by maxDepth (overall) and maxLocale (relation hops). The schema walk is in-memory only — no database hit. The actual query cost is determined by Strapi's normal populate execution after the populate object is supplied.
The plugin runs for any content type that requests populate=*.*. Trusted internal callers benefit from convenience; if populate=*.* is reachable from untrusted clients, gate it at your API layer (route policies, role permissions) just like any expensive query.
🤝 Contributing
Feel free to contribute to this plugin by:
- Creating issues for bugs or feature requests
- Submitting pull requests for improvements
- Providing feedback and suggestions
📄 License
MIT License - Copyright (c) Jorge Pizzati
