# 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 programmatic `strapi.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.ts` and you're done

## ⚙️ Installation

```bash
# 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`):

```ts
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)

```ts
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):

```ts
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:

```ts
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 `maxLocale` budget remains
- **Media** fields are populated as `true`
- **`admin::user`** and the `related` field on `plugin::upload.file` are 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:

1. Creating issues for bugs or feature requests
2. Submitting pull requests for improvements
3. Providing feedback and suggestions

## 📄 License

MIT License - Copyright (c) Jorge Pizzati
