Skip to content

Commit 4c6eeab

Browse files
committed
feat: add the populate-deeply plugin
1 parent c51ace2 commit 4c6eeab

File tree

15 files changed

+9061
-0
lines changed

15 files changed

+9061
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public/
2+
coverage/
3+
.circleci/
4+
.github/
5+
.nvmrc
6+
.eslintrc
7+
codecov.yml
8+
*.map
9+
*.spec.*
10+
setup-package.*
11+
**/__tests__/**
12+
**/__mocks__/**
13+
tsconfig.*
14+
.turbo/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Strapi plugin populate-deeply
2+
3+
This plugin allows for easier population of deep content structures using the rest API. It is heavily based on the populate-deep plugin but I have rewritten in Typescript and fixed a number of bugs and security issues.
4+
5+
# Installation
6+
7+
`npm install strapi-plugin-populate-deeply`
8+
9+
`yarn add strapi-plugin-populate-deeply`
10+
11+
# Usages
12+
13+
## Examples
14+
15+
Populate a request with the default depth.
16+
17+
`/api/articles?populate=deep`
18+
19+
Populate a request with the a custom depth
20+
21+
`/api/articles?populate=deep,10`
22+
23+
## Good to know
24+
25+
To guard against DoS attacks, a maximum depth can be configured, defaulting to 5 levels deep.
26+
27+
The populate deep option is available for all collections and single types using the findOne and findMany methods.
28+
29+
# Configuration
30+
31+
The default and max depths can be customized via the plugin config. To do so create or edit you plugins.js file.
32+
33+
## Example configuration
34+
35+
`config/plugins.js`
36+
37+
```
38+
module.exports = ({ env }) => ({
39+
'strapi-plugin-populate-deep': {
40+
config: {
41+
defaultDepth: 4, // Default is 3
42+
maxDepth: 6, // Default is 5
43+
}
44+
},
45+
});
46+
```
47+
48+
# Contributions
49+
50+
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. The plugin is entirely based on the [populate-deep](https://github.com/Barelydead/strapi-plugin-populate-deep) plugin by Christofer Jungberg.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const config = {
2+
default: {},
3+
validator() {},
4+
};
5+
6+
export default config;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module '@strapi/helper-plugin';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Strapi } from '@strapi/strapi';
2+
import { isEmpty, merge } from 'lodash/fp';
3+
4+
const getModelPopulationAttributes = (model) => {
5+
if (model.uid === 'plugin::upload.file') {
6+
const { related, ...attributes } = model.attributes;
7+
return attributes;
8+
}
9+
10+
return model.attributes;
11+
};
12+
13+
export type PopulateObject = {
14+
populate: any;
15+
};
16+
17+
type ModelMetadata = {
18+
type: string;
19+
target: string;
20+
component: string;
21+
components: string[];
22+
};
23+
24+
export const getFullPopulateObject = (
25+
modelUid,
26+
maxDepth = 5,
27+
ignore?: string[]
28+
): PopulateObject | true | undefined => {
29+
const skipCreatorFields = strapi
30+
.plugin('strapi-plugin-populate-deep')
31+
?.config('skipCreatorFields');
32+
33+
if (maxDepth <= 1) {
34+
return true;
35+
}
36+
if (modelUid === 'admin::user' && skipCreatorFields) {
37+
return undefined;
38+
}
39+
40+
const populate = {};
41+
const model = strapi.getModel(modelUid);
42+
if (ignore && !ignore.includes(model.collectionName))
43+
ignore.push(model.collectionName);
44+
for (const [key, value] of Object.entries<ModelMetadata>(
45+
getModelPopulationAttributes(model)
46+
)) {
47+
if (ignore?.includes(key)) continue;
48+
if (value) {
49+
if (value.type === 'component') {
50+
populate[key] = getFullPopulateObject(value.component, maxDepth - 1);
51+
} else if (value.type === 'dynamiczone') {
52+
const dynamicPopulate = value.components.reduce((prev, cur) => {
53+
const curPopulate = getFullPopulateObject(cur, maxDepth - 1);
54+
return merge(prev, { [cur]: curPopulate });
55+
// return curPopulate === true ? prev : merge(prev, curPopulate);
56+
}, {});
57+
populate[key] = isEmpty(dynamicPopulate)
58+
? true
59+
: { on: dynamicPopulate };
60+
} else if (value.type === 'relation') {
61+
const relationPopulate = getFullPopulateObject(
62+
value.target,
63+
key === 'localizations' && maxDepth > 2 ? 1 : maxDepth - 1,
64+
ignore
65+
);
66+
if (relationPopulate) {
67+
populate[key] = relationPopulate;
68+
}
69+
} else if (value.type === 'media') {
70+
populate[key] = true;
71+
}
72+
}
73+
}
74+
return isEmpty(populate) ? true : { populate };
75+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "strapi-plugin-populate-deeply",
3+
"version": "0.9.0",
4+
"description": "An updated, Typescript-based version of populate-deep",
5+
"homepage": "https://github.com/rixw/strapi-utils/strapi-plugin-populate-deeply",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/rixw/strapi-utils",
9+
"directory": "packages/strapi-plugin-populate-deeply"
10+
},
11+
"license": "MIT",
12+
"author": {
13+
"name": "Richard Weaver",
14+
"email": "[email protected]",
15+
"url": "https://github.com/rixw"
16+
},
17+
"maintainers": [
18+
{
19+
"name": "Richard Weaver",
20+
"email": "[email protected]",
21+
"url": "https://github.com/rixw"
22+
}
23+
],
24+
"main": "./dist/index.js",
25+
"types": "./dist/index.d.ts",
26+
"files": [
27+
"./dist"
28+
],
29+
"scripts": {
30+
"build": "yarn clean && tsc -p tsconfig.json && node dist/setup-package.js",
31+
"clean": "rm -rf dist",
32+
"develop": "tsc -p tsconfig.server.json -w"
33+
},
34+
"dependencies": {
35+
"@strapi/helper-plugin": "^4.13.1",
36+
"lodash": "4.17.21",
37+
"prop-types": "^15.7.2"
38+
},
39+
"devDependencies": {
40+
"@strapi/typescript-utils": "^4.13.1",
41+
"typescript": "^5.2.2"
42+
},
43+
"peerDependencies": {
44+
"@strapi/strapi": "^4.13.1"
45+
},
46+
"publishConfig": {
47+
"access": "public",
48+
"directory": "dist"
49+
},
50+
"strapi": {
51+
"name": "populate-deeply",
52+
"description": "An updated, Typescript-based version of populate-deep",
53+
"kind": "plugin"
54+
}
55+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PopulateObject, getFullPopulateObject } from './helpers';
2+
3+
const bootstrap = ({ strapi }) => {
4+
const defaultDepth =
5+
strapi.plugin('strapi-plugin-populate-deep')?.config('defaultDepth') || 3;
6+
const maxDepth =
7+
strapi.plugin('strapi-plugin-populate-deep')?.config('maxDepth') || 5;
8+
// Subscribe to the lifecycles that we are interested in.
9+
strapi.db.lifecycles.subscribe((event) => {
10+
if (event.action === 'beforeFindMany' || event.action === 'beforeFindOne') {
11+
let populate = event.params?.populate;
12+
if (typeof populate === 'string') {
13+
populate = [populate];
14+
}
15+
if (populate && populate[0] === 'deep') {
16+
const depth = populate[1] ?? defaultDepth;
17+
const modelObject = getFullPopulateObject(
18+
event.model.uid,
19+
depth > maxDepth ? maxDepth : depth,
20+
[]
21+
);
22+
event.params.populate = (modelObject as PopulateObject).populate;
23+
}
24+
}
25+
});
26+
};
27+
28+
export default bootstrap;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const config = {
2+
default: {},
3+
validator() {},
4+
};
5+
6+
export default config;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Strapi } from '@strapi/strapi';
2+
import { isEmpty, merge } from 'lodash/fp';
3+
4+
const getModelPopulationAttributes = (model) => {
5+
if (model.uid === 'plugin::upload.file') {
6+
const { related, ...attributes } = model.attributes;
7+
return attributes;
8+
}
9+
10+
return model.attributes;
11+
};
12+
13+
export type PopulateObject = {
14+
populate: any;
15+
};
16+
17+
type ModelMetadata = {
18+
type: string;
19+
target: string;
20+
component: string;
21+
components: string[];
22+
};
23+
24+
export const getFullPopulateObject = (
25+
modelUid,
26+
maxDepth = 5,
27+
ignore?: string[]
28+
): PopulateObject | true | undefined => {
29+
const skipCreatorFields = strapi
30+
.plugin('strapi-plugin-populate-deep')
31+
?.config('skipCreatorFields');
32+
33+
if (maxDepth <= 1) {
34+
return true;
35+
}
36+
if (modelUid === 'admin::user' && skipCreatorFields) {
37+
return undefined;
38+
}
39+
40+
const populate = {};
41+
const model = strapi.getModel(modelUid);
42+
if (ignore && !ignore.includes(model.collectionName))
43+
ignore.push(model.collectionName);
44+
for (const [key, value] of Object.entries<ModelMetadata>(
45+
getModelPopulationAttributes(model)
46+
)) {
47+
if (ignore?.includes(key)) continue;
48+
if (value) {
49+
if (value.type === 'component') {
50+
populate[key] = getFullPopulateObject(value.component, maxDepth - 1);
51+
} else if (value.type === 'dynamiczone') {
52+
const dynamicPopulate = value.components.reduce((prev, cur) => {
53+
const curPopulate = getFullPopulateObject(cur, maxDepth - 1);
54+
return merge(prev, { [cur]: curPopulate });
55+
// return curPopulate === true ? prev : merge(prev, curPopulate);
56+
}, {});
57+
populate[key] = isEmpty(dynamicPopulate)
58+
? true
59+
: { on: dynamicPopulate };
60+
} else if (value.type === 'relation') {
61+
const relationPopulate = getFullPopulateObject(
62+
value.target,
63+
key === 'localizations' && maxDepth > 2 ? 1 : maxDepth - 1,
64+
ignore
65+
);
66+
if (relationPopulate) {
67+
populate[key] = relationPopulate;
68+
}
69+
} else if (value.type === 'media') {
70+
populate[key] = true;
71+
}
72+
}
73+
}
74+
return isEmpty(populate) ? true : { populate };
75+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import bootstrap from './bootstrap';
2+
import config from './config';
3+
4+
const server = {
5+
bootstrap,
6+
config,
7+
};
8+
9+
export default server;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
*
3+
* setup-package.js
4+
*
5+
* This file is used by publish system to build a clean npm package with the compiled js files in the root of the package.
6+
* It will not be included in the npm package.
7+
*
8+
**/
9+
10+
import fs from 'fs';
11+
12+
// This script is called from within the build folder. It is important to include it in .npmignore, so it will not get published.
13+
const sourceDirectory = __dirname + '/..';
14+
const destinationDirectory = __dirname;
15+
16+
function main() {
17+
// Generate publish-ready package.json
18+
const source = fs.readFileSync(__dirname + '/../package.json').toString('utf-8');
19+
const sourceObj = JSON.parse(source);
20+
sourceObj.scripts = {};
21+
sourceObj.devDependencies = {};
22+
sourceObj.main = undefined;
23+
sourceObj.files = undefined;
24+
sourceObj.types = undefined;
25+
fs.writeFileSync(
26+
`${destinationDirectory}/package.json`,
27+
Buffer.from(JSON.stringify(sourceObj, null, 2), 'utf-8'),
28+
);
29+
fs.copyFileSync(`${sourceDirectory}/README.md`, `${destinationDirectory}/README.md`);
30+
fs.copyFileSync(`${sourceDirectory}/.npmignore`, `${destinationDirectory}/.npmignore`);
31+
}
32+
33+
main();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import server from './server';
2+
export = server;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"extends": "@strapi/typescript-utils/tsconfigs/server",
3+
"compilerOptions": {
4+
"resolveJsonModule": true,
5+
"outDir": "./dist",
6+
"declaration": true,
7+
"declarationMap": true
8+
},
9+
"include": [
10+
"./server/**/*",
11+
"./strapi-server.*",
12+
"./setup-package.ts",
13+
"./types/**/*",
14+
],
15+
"exclude": [
16+
"dist/**/*",
17+
"build",
18+
"node_modules",
19+
"**/*.spec.ts"
20+
]
21+
}

0 commit comments

Comments
 (0)