mirror of
https://github.com/ComfortablyCoding/strapi-plugin-slugify.git
synced 2025-08-16 00:02:44 -04:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f7843ee318 | ||
|
65c3a4e5f0 | ||
|
66b9d2789c | ||
|
68ea256e5b | ||
|
5752ccfc93 | ||
|
f7ba4cce37 | ||
|
430f90b992 | ||
|
e45d80d826 | ||
|
eead698ec7 | ||
|
78229365a5 | ||
|
b668ceed26 | ||
|
ddf130a110 | ||
|
7a41a004e1 | ||
|
691e39feb0 | ||
|
0d446655ec | ||
|
8f8f375fb0 | ||
|
77a1c81978 | ||
|
38292fedd3 | ||
|
8943c3fcbe | ||
|
2d53829e94 | ||
|
9c336ebb1d | ||
|
ccb31f7389 | ||
|
8381b32a20 | ||
|
d4a991be6f | ||
|
78ca3f6d5d | ||
|
f1ff9339dc | ||
|
99e63342e2 | ||
|
5cbaea6ab0 | ||
|
27b69ff063 | ||
|
c48e13fb0d | ||
|
d44577e9c4 | ||
|
d0ff250a21 | ||
|
9ca09c76f0 | ||
|
1b20016f4c | ||
|
6cd9b79e27 | ||
|
6b28c77d3a | ||
|
991af08b6b | ||
|
906188608c | ||
|
09a279d9d9 | ||
|
c630ef7abd | ||
|
b0aaa5d52b | ||
|
32e5d0aebd | ||
|
3fb1b0b9b5 | ||
|
1bf0670815 | ||
|
5fcfae3b85 | ||
|
6ca80869b4 | ||
|
1463e672e6 | ||
|
f27ce29e8e | ||
|
2afe7117a2 |
4
.github/workflows/npm-publish.yml
vendored
4
.github/workflows/npm-publish.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
|||||||
- name: Checkout branch
|
- name: Checkout branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Node v14
|
- name: Install Node v18
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: '18.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
|
40
README.md
40
README.md
@ -20,11 +20,9 @@ The installation requirements are the same as Strapi itself and can be found in
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install strapi-plugin-slugify
|
npm install strapi-plugin-slugify
|
||||||
```
|
|
||||||
|
|
||||||
**or**
|
# or
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn add strapi-plugin-slugify
|
yarn add strapi-plugin-slugify
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -56,18 +54,27 @@ module.exports = ({ env }) => ({
|
|||||||
|
|
||||||
This will listen for any record created or updated in the article content type and set a slugified value for the slug field automatically based on the title field.
|
This will listen for any record created or updated in the article content type and set a slugified value for the slug field automatically based on the title field.
|
||||||
|
|
||||||
> Note that if you want to rewrite the same field (so `title` is both a reference and a slug) then you just put `title` for both the `field` and `references` properties.
|
> Note: To rewrite the same field (e.g. `title` is both a reference and a slug) use `title` as the `field` and `references` value.
|
||||||
|
|
||||||
|
> Note: Compound slugs (basing the slug on multiple fields) can be achieved by passing an array of fields to the `references` property (e.g. `references: ['date','title']`).
|
||||||
|
|
||||||
**IMPORTANT NOTE**: Make sure any sensitive data is stored in env files.
|
**IMPORTANT NOTE**: Make sure any sensitive data is stored in env files.
|
||||||
|
|
||||||
### The Complete Plugin Configuration Object
|
### Additional Requirement for GraphQL
|
||||||
|
|
||||||
|
Per [#35](https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues/35) please ensure that the slugify plugin configuration is placed **before** the graphql plugin configuration.
|
||||||
|
|
||||||
|
## The Complete Plugin Configuration Object
|
||||||
|
|
||||||
| Property | Description | Type | Default | Required |
|
| Property | Description | Type | Default | Required |
|
||||||
| -------- | ----------- | ---- | ------- | -------- |
|
| -------- | ----------- | ---- | ------- | -------- |
|
||||||
| contentTypes | The Content Types to add auto slugification and search findOne by slug search utility to | Object | {} | No |
|
| contentTypes | The Content Types to add auto slugification and search findOne by slug search utility to | Object | {} | No |
|
||||||
| contentTypes[modelName] | The model name of the content type (it is the `singularName` in the [model schema](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#model-schema)) | String | N/A | Yes |
|
| contentTypes[modelName] | The model name of the content type (it is the `singularName` in the [model schema](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#model-schema)) | String | N/A | Yes |
|
||||||
| contentTypes[modelName]field | The name of the field to add the slug | String | N/A | Yes |
|
| contentTypes[modelName]field | The name of the field to add the slug | String | N/A | Yes |
|
||||||
| contentTypes[modelName]references | The name of the field that is used to build the slug | String | N/A | Yes |
|
| contentTypes[modelName]references | The name(s) of the field(s) used to build the slug. If an array of fields is set it will result in a compound slug | String or Array | N/A | Yes |
|
||||||
|
| slugifyWithCount | Duplicate strings will have their occurrence appended to the end of the slug | Boolean | false | No |
|
||||||
|
| shouldUpdateSlug | Allow the slug to be updated after initial generation. | Boolean | false | No |
|
||||||
|
| skipUndefinedReferences | Skip reference fields that have no data. Mostly applicable to compound slug | Boolean | false | No |
|
||||||
| slugifyOptions | The options to pass the the slugify function. All options can be found in the [slugify docs](https://github.com/sindresorhus/slugify#api) | Object | {} | No |
|
| slugifyOptions | The options to pass the the slugify function. All options can be found in the [slugify docs](https://github.com/sindresorhus/slugify#api) | Object | {} | No |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -80,7 +87,7 @@ Any time the respective content types have an entity created or updated the slug
|
|||||||
|
|
||||||
### Find One by Slug
|
### Find One by Slug
|
||||||
|
|
||||||
Hitting the `/api/slugify/slugs/:modelName/:slug` endpoint for any configured content types will return the entity type that matches the slug in the url.
|
Hitting the `/api/slugify/slugs/:modelName/:slug` endpoint for any configured content types will return the entity type that matches the slug in the url. Additionally the endpoint accepts any of the parameters that can be added to the routes built into Strapi.
|
||||||
|
|
||||||
**IMPORTANT** The modelName is case sensitive and must match exactly with the name defined in the configuration.
|
**IMPORTANT** The modelName is case sensitive and must match exactly with the name defined in the configuration.
|
||||||
|
|
||||||
@ -118,6 +125,25 @@ await fetch(`${API_URL}/api/slugify/slugs/article/lorem-ipsum-dolor`);
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additionally if `draftAndPublish` is enabled for the content-type a `publicationState` arg can be passed to the GraphQL query that accepts either `preview` or `live` as input.
|
||||||
|
|
||||||
|
**IMPORTANT** Please beware that the request for an entry in `preview` will return both draft entries & published entries as per Strapi default.
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
findSlug(modelName:"article",slug:"lorem-ipsum-dolor",publicationState:"preview"){
|
||||||
|
... on ArticleEntityResponse{
|
||||||
|
data{
|
||||||
|
id
|
||||||
|
attributes{
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Example Response
|
### Example Response
|
||||||
|
|
||||||
If an article with the slug of `lorem-ipsum-dolor` exists the response will look the same as a single entity response
|
If an article with the slug of `lorem-ipsum-dolor` exists the response will look the same as a single entity response
|
||||||
|
24
package.json
24
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "strapi-plugin-slugify",
|
"name": "strapi-plugin-slugify",
|
||||||
"version": "2.0.0",
|
"version": "2.3.8",
|
||||||
"description": "A plugin for Strapi Headless CMS that provides the ability to auto slugify a field for any content type.",
|
"description": "A plugin for Strapi Headless CMS that provides the ability to auto slugify a field for any content type.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
@ -26,20 +26,18 @@
|
|||||||
"url": "https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues"
|
"url": "https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/slugify": "1.1.0"
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
},
|
"@strapi/strapi": "^4.14.0",
|
||||||
"devDependencies": {
|
"@strapi/utils": "^4.14.0",
|
||||||
"eslint": "^8.8.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"prettier": "^2.5.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@strapi/strapi": "^4.0.7",
|
|
||||||
"@strapi/utils": "^4.0.7",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"yup": "^0.32.9"
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.53.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"prettier": "^3.1.0"
|
||||||
|
},
|
||||||
"strapi": {
|
"strapi": {
|
||||||
"displayName": "Slugify",
|
"displayName": "Slugify",
|
||||||
"name": "slugify",
|
"name": "slugify",
|
||||||
@ -47,7 +45,7 @@
|
|||||||
"kind": "plugin"
|
"kind": "plugin"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.x.x <=16.x.x",
|
"node": ">=18.0.0 <=20.x.x",
|
||||||
"npm": ">=6.0.0"
|
"npm": ">=6.0.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
34
server/bootstrap.js
vendored
34
server/bootstrap.js
vendored
@ -1,34 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { SUPPORTED_LIFECYCLES } = require('./utils/constants');
|
|
||||||
const { getPluginService } = require('./utils/getPluginService');
|
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
|
||||||
const settingsService = getPluginService(strapi, 'settingsService');
|
|
||||||
const settings = settingsService.get();
|
|
||||||
|
|
||||||
const { contentTypes, slugifyOptions } = settings;
|
|
||||||
|
|
||||||
// build settings structure
|
|
||||||
const models = settingsService.build(contentTypes);
|
|
||||||
|
|
||||||
// reset plugin settings
|
|
||||||
settingsService.set({
|
|
||||||
models,
|
|
||||||
slugifyOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set up lifecycles
|
|
||||||
const subscribe = {
|
|
||||||
models: _.map(models, (m) => m.uid),
|
|
||||||
};
|
|
||||||
|
|
||||||
SUPPORTED_LIFECYCLES.forEach((lifecycle) => {
|
|
||||||
subscribe[lifecycle] = (ctx) => {
|
|
||||||
getPluginService(strapi, 'slugService').slugify(ctx);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
strapi.db.lifecycles.subscribe(subscribe);
|
|
||||||
};
|
|
17
server/bootstrap/buildSettings.js
Normal file
17
server/bootstrap/buildSettings.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const { getPluginService } = require('../utils/getPluginService');
|
||||||
|
const buildSettings = async () => {
|
||||||
|
const settingsService = getPluginService('settingsService');
|
||||||
|
const settings = await settingsService.get();
|
||||||
|
|
||||||
|
// build settings structure
|
||||||
|
const normalizedSettings = settingsService.build(settings);
|
||||||
|
|
||||||
|
// reset plugin settings
|
||||||
|
await settingsService.set(normalizedSettings);
|
||||||
|
|
||||||
|
return normalizedSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildSettings,
|
||||||
|
};
|
16
server/bootstrap/index.js
Normal file
16
server/bootstrap/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { buildSettings } = require('./buildSettings');
|
||||||
|
const { setupLifecycles } = require('./setupLifecycles');
|
||||||
|
const { syncSlugCount } = require('./syncSlugCount');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
const settings = await buildSettings();
|
||||||
|
|
||||||
|
if (settings.slugifyWithCount) {
|
||||||
|
// Ensure correct count used for old plugin versions and projects with existing slugs.
|
||||||
|
await syncSlugCount(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLifecycles(settings);
|
||||||
|
};
|
18
server/bootstrap/setupLifecycles.js
Normal file
18
server/bootstrap/setupLifecycles.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const { getPluginService } = require('../utils/getPluginService');
|
||||||
|
const setupLifecycles = (settings) => {
|
||||||
|
// set up lifecycles
|
||||||
|
const subscribe = {
|
||||||
|
models: _.map(settings.modelsByUID, (model) => model.uid),
|
||||||
|
};
|
||||||
|
|
||||||
|
['beforeCreate', 'afterCreate', 'beforeUpdate'].forEach((lifecycle) => {
|
||||||
|
subscribe[lifecycle] = (ctx) => getPluginService('slugService').slugify(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
strapi.db.lifecycles.subscribe(subscribe);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setupLifecycles,
|
||||||
|
};
|
75
server/bootstrap/syncSlugCount.js
Normal file
75
server/bootstrap/syncSlugCount.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const syncSlugCount = async (settings) => {
|
||||||
|
const entries = await strapi.entityService.findMany('plugin::slugify.slug', {
|
||||||
|
filters: { createdAt: { $gt: 1 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// if entries aready present we can skip sync
|
||||||
|
if (entries && entries.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strapi.log.info('[slugify] syncing slug count for registered content types');
|
||||||
|
|
||||||
|
const slugs = new Map();
|
||||||
|
|
||||||
|
// chec slugs in each reigistered model
|
||||||
|
for (const uid in settings.modelsByUID) {
|
||||||
|
if (!Object.hasOwnProperty.call(settings.modelsByUID, uid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = settings.modelsByUID[uid];
|
||||||
|
|
||||||
|
// using db query to avoid the need to check if CT has draftAndPublish enabled
|
||||||
|
const modelEntries = await strapi.db.query(model.uid).findMany({
|
||||||
|
filters: { createdAt: { $gt: 1 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
strapi.log.info(`[slugify] syncing slug count for ${model.uid}`);
|
||||||
|
for (const entry of modelEntries) {
|
||||||
|
const slug = entry[model.field];
|
||||||
|
if (!slug) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = slugs.get(getNonAppendedSlug(slug));
|
||||||
|
if (!record) {
|
||||||
|
slugs.set(slug, { slug, count: 1 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
slugs.set(record.slug, { slug: record.slug, count: record.count + 1 });
|
||||||
|
}
|
||||||
|
strapi.log.info(`[slugify] sync for ${model.uid} completed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slugs.size) {
|
||||||
|
// create all required records
|
||||||
|
const createResponse = await strapi.db.query('plugin::slugify.slug').createMany({
|
||||||
|
data: [...slugs.values()],
|
||||||
|
});
|
||||||
|
|
||||||
|
strapi.log.info(
|
||||||
|
`[slugify] ${createResponse.count} out of ${slugs.size} slugs synced successfully`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
strapi.log.info('[slugify] No syncable slugs found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// removes any appended number from a slug/string if found
|
||||||
|
const getNonAppendedSlug = (slug) => {
|
||||||
|
const match = slug.match('[\\-]{1}[\\d]+$');
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug.replace(match[0], '');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
syncSlugCount,
|
||||||
|
};
|
@ -1,15 +1,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { pluginConfigSchema } = require('./schema');
|
const schema = require('./schema');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
default() {
|
default: () => ({
|
||||||
return {
|
|
||||||
contentTypes: {},
|
contentTypes: {},
|
||||||
slugifyOptions: {},
|
slugifyOptions: {},
|
||||||
};
|
slugifyWithCount: false,
|
||||||
},
|
shouldUpdateSlug: false,
|
||||||
async validator(config) {
|
skipUndefinedReferences: false,
|
||||||
await pluginConfigSchema.validate(config);
|
}),
|
||||||
},
|
validator: (config) => schema.validateSync(config),
|
||||||
};
|
};
|
||||||
|
@ -3,21 +3,23 @@
|
|||||||
const yup = require('yup');
|
const yup = require('yup');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const pluginConfigSchema = yup.object().shape({
|
const schema = yup.object().shape({
|
||||||
slugifyOptions: yup.object(),
|
slugifyOptions: yup.object(),
|
||||||
contentTypes: yup.lazy((obj) => {
|
contentTypes: yup.lazy((obj) => {
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
let shape = {};
|
let shape = {};
|
||||||
_.each(obj, (_, key) => {
|
_.each(obj, (_value, key) => {
|
||||||
shape[key] = yup.object().shape({
|
shape[key] = yup.object().shape({
|
||||||
field: yup.string().required(),
|
field: yup.string().required(),
|
||||||
references: yup.string().required(),
|
references: yup.lazy((v) =>
|
||||||
|
_.isArray(v) ? yup.array().of(yup.string()).required() : yup.string().required()
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return yup.object().shape(shape);
|
return yup.object().shape(shape);
|
||||||
}),
|
}),
|
||||||
|
slugifyWithCount: yup.bool(),
|
||||||
|
shouldUpdateSlug: yup.bool(),
|
||||||
|
skipUndefinedReferences: yup.bool(),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = schema;
|
||||||
pluginConfigSchema,
|
|
||||||
};
|
|
||||||
|
9
server/content-types/index.js
Normal file
9
server/content-types/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const slugSchema = require('./slug/schema.json');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slug: {
|
||||||
|
schema: slugSchema,
|
||||||
|
},
|
||||||
|
};
|
29
server/content-types/slug/schema.json
Normal file
29
server/content-types/slug/schema.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "slugs",
|
||||||
|
"info": {
|
||||||
|
"singularName": "slug",
|
||||||
|
"pluralName": "slugs",
|
||||||
|
"displayName": "slug"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false,
|
||||||
|
"comment": ""
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"content-manager": {
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
|
"content-type-builder": {
|
||||||
|
"visible": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"slug": {
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,32 +2,35 @@
|
|||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { getPluginService } = require('../utils/getPluginService');
|
const { getPluginService } = require('../utils/getPluginService');
|
||||||
const { transformResponse } = require('../utils/transformEntry');
|
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams');
|
||||||
|
const { sanitizeOutput } = require('../utils/sanitizeOutput');
|
||||||
|
const { hasRequiredModelScopes } = require('../utils/hasRequiredModelScopes');
|
||||||
|
const transform = require('../utils/transform');
|
||||||
|
|
||||||
module.exports = ({ strapi }) => ({
|
module.exports = ({ strapi }) => ({
|
||||||
async findSlug(ctx) {
|
async findSlug(ctx) {
|
||||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
const { modelsByName } = getPluginService('settingsService').get();
|
||||||
const { params } = ctx.request;
|
const { modelName, slug } = ctx.request.params;
|
||||||
const { modelName, slug } = params;
|
const { auth } = ctx.state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!modelName) {
|
isValidFindSlugParams({
|
||||||
throw Error('A model name path variable is required.');
|
modelName,
|
||||||
|
slug,
|
||||||
|
modelsByName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return ctx.badRequest(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!slug) {
|
const { uid, field, contentType } = modelsByName[modelName];
|
||||||
throw Error('A slug path variable is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = models[modelName];
|
try {
|
||||||
if (!model) {
|
await hasRequiredModelScopes(strapi, uid, auth);
|
||||||
throw Error(
|
} catch (error) {
|
||||||
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
|
return ctx.forbidden();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uid, field, contentType } = model;
|
|
||||||
|
|
||||||
// add slug filter to any already existing query restrictions
|
// add slug filter to any already existing query restrictions
|
||||||
let query = ctx.query || {};
|
let query = ctx.query || {};
|
||||||
if (!query.filters) {
|
if (!query.filters) {
|
||||||
@ -40,15 +43,13 @@ module.exports = ({ strapi }) => ({
|
|||||||
query.publicationState = 'live';
|
query.publicationState = 'live';
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
|
const data = await getPluginService('slugService').findOne(uid, query);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
ctx.body = transformResponse(data);
|
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
|
||||||
|
ctx.body = transform.response({ data: sanitizedEntity, schema: contentType });
|
||||||
} else {
|
} else {
|
||||||
ctx.notFound();
|
ctx.notFound();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
ctx.badRequest(error.message);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,7 @@ const registerGraphlQLQuery = (strapi) => {
|
|||||||
resolversConfig: getResolversConfig(),
|
resolversConfig: getResolversConfig(),
|
||||||
});
|
});
|
||||||
|
|
||||||
getPluginService(strapi, 'extension', 'graphql').use(extension);
|
getPluginService('extension', 'graphql').use(extension);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { getPluginService } = require('../utils/getPluginService');
|
const { getPluginService } = require('../utils/getPluginService');
|
||||||
const { ValidationError } = require('@strapi/utils').errors;
|
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams');
|
||||||
|
const { hasRequiredModelScopes } = require('../utils/hasRequiredModelScopes');
|
||||||
|
const { sanitizeOutput } = require('../utils/sanitizeOutput');
|
||||||
|
const { ForbiddenError, ValidationError } = require('@strapi/utils').errors;
|
||||||
|
|
||||||
const getCustomTypes = (strapi, nexus) => {
|
const getCustomTypes = (strapi, nexus) => {
|
||||||
const { naming } = getPluginService(strapi, 'utils', 'graphql');
|
const { naming } = getPluginService('utils', 'graphql');
|
||||||
const { toEntityResponse } = getPluginService(strapi, 'format', 'graphql').returnTypes;
|
const { toEntityResponse } = getPluginService('format', 'graphql').returnTypes;
|
||||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
const { modelsByUID } = getPluginService('settingsService').get();
|
||||||
const { getEntityResponseName } = naming;
|
const { getEntityResponseName } = naming;
|
||||||
|
|
||||||
// get all types required for findSlug query
|
// get all types required for findSlug query
|
||||||
let findSlugTypes = {
|
let findSlugTypes = {
|
||||||
response: [],
|
response: [],
|
||||||
};
|
};
|
||||||
_.forEach(strapi.contentTypes, (value, key) => {
|
_.forEach(strapi.contentTypes, (contentType, uid) => {
|
||||||
if (models[key]) {
|
if (modelsByUID[uid]) {
|
||||||
findSlugTypes.response.push(getEntityResponseName(value));
|
findSlugTypes.response.push(getEntityResponseName(contentType));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ const getCustomTypes = (strapi, nexus) => {
|
|||||||
t.members(...findSlugTypes.response);
|
t.members(...findSlugTypes.response);
|
||||||
},
|
},
|
||||||
resolveType: (ctx) => {
|
resolveType: (ctx) => {
|
||||||
return getEntityResponseName(models[ctx.info.resourceUID].contentType);
|
return getEntityResponseName(modelsByUID[ctx.info.resourceUID].contentType);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,34 +48,46 @@ const getCustomTypes = (strapi, nexus) => {
|
|||||||
args: {
|
args: {
|
||||||
modelName: nexus.stringArg('The model name of the content type'),
|
modelName: nexus.stringArg('The model name of the content type'),
|
||||||
slug: nexus.stringArg('The slug to query for'),
|
slug: nexus.stringArg('The slug to query for'),
|
||||||
|
publicationState: nexus.stringArg('The publication state of the entry'),
|
||||||
},
|
},
|
||||||
resolve: async (_parent, args) => {
|
resolve: async (_parent, args, ctx) => {
|
||||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
const { modelsByName } = getPluginService('settingsService').get();
|
||||||
const { modelName, slug } = args;
|
const { modelName, slug, publicationState } = args;
|
||||||
|
const { auth } = ctx.state;
|
||||||
|
|
||||||
const model = models[modelName];
|
try {
|
||||||
|
isValidFindSlugParams({
|
||||||
|
modelName,
|
||||||
|
slug,
|
||||||
|
modelsByName,
|
||||||
|
publicationState,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new ValidationError(error.message);
|
||||||
|
}
|
||||||
|
const { uid, field, contentType } = modelsByName[modelName];
|
||||||
|
|
||||||
// ensure valid model is passed
|
try {
|
||||||
if (!model) {
|
await hasRequiredModelScopes(strapi, uid, auth);
|
||||||
throw new ValidationError(
|
} catch (error) {
|
||||||
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
|
throw new ForbiddenError();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uid, field, contentType } = model;
|
// build query
|
||||||
let query = {
|
let query = {
|
||||||
filters: {
|
filters: {
|
||||||
[field]: slug,
|
[field]: slug,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// only return published entries
|
// only return published entries by default if content type has draftAndPublish enabled
|
||||||
if (_.get(contentType, ['options', 'draftAndPublish'], false)) {
|
if (_.get(contentType, ['options', 'draftAndPublish'], false)) {
|
||||||
query.publicationState = 'live';
|
query.publicationState = publicationState || 'live';
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
|
const data = await getPluginService('slugService').findOne(uid, query);
|
||||||
return toEntityResponse(data, { resourceUID: uid });
|
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
|
||||||
|
return toEntityResponse(sanitizedEntity, { resourceUID: uid });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const bootstrap = require('./bootstrap');
|
const bootstrap = require('./bootstrap');
|
||||||
const register = require('./register');
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
const contentTypes = require('./content-types');
|
||||||
const controllers = require('./controllers');
|
const controllers = require('./controllers');
|
||||||
|
const register = require('./register');
|
||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
const services = require('./services');
|
const services = require('./services');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
bootstrap,
|
bootstrap,
|
||||||
register,
|
|
||||||
config,
|
config,
|
||||||
|
contentTypes,
|
||||||
controllers,
|
controllers,
|
||||||
|
register,
|
||||||
routes,
|
routes,
|
||||||
services,
|
services,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { registerGraphlQLQuery } = require('./graphql');
|
const { registerGraphlQLQuery } = require('./graphql');
|
||||||
|
const { getPluginService } = require('./utils/getPluginService');
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
module.exports = ({ strapi }) => {
|
||||||
|
const { contentTypes } = getPluginService('settingsService').get();
|
||||||
|
|
||||||
|
// ensure we have at least one model before attempting registration
|
||||||
|
if (!Object.keys(contentTypes).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// add graphql query if present
|
// add graphql query if present
|
||||||
if (strapi.plugin('graphql')) {
|
if (strapi.plugin('graphql')) {
|
||||||
strapi.log.info('[slugify] graphql detected, registering queries');
|
strapi.log.info('[slugify] graphql detected, registering queries');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const slugService = require('./slug-service');
|
|
||||||
const settingsService = require('./settings-service');
|
const settingsService = require('./settings-service');
|
||||||
|
const slugService = require('./slug-service');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
slugService,
|
slugService,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const { isValidModelField } = require('../utils/isValidModelField');
|
||||||
const { pluginId } = require('../utils/pluginId');
|
const { pluginId } = require('../utils/pluginId');
|
||||||
|
|
||||||
module.exports = ({ strapi }) => ({
|
module.exports = ({ strapi }) => ({
|
||||||
@ -10,34 +11,48 @@ module.exports = ({ strapi }) => ({
|
|||||||
set(settings) {
|
set(settings) {
|
||||||
return strapi.config.set(`plugin.${pluginId}`, settings);
|
return strapi.config.set(`plugin.${pluginId}`, settings);
|
||||||
},
|
},
|
||||||
build(contentTypes) {
|
build(settings) {
|
||||||
let models = {};
|
// build models
|
||||||
|
settings.modelsByUID = {};
|
||||||
_.filter(strapi.contentTypes, (value, uid) => {
|
settings.modelsByName = {};
|
||||||
const model = contentTypes[value.modelName];
|
_.each(strapi.contentTypes, (contentType, uid) => {
|
||||||
|
const model = settings.contentTypes[contentType.modelName];
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure provided fields are present on the model
|
// ensure provided fields are present on the model
|
||||||
const hasField = _.get(value, ['attributes', model.field], false);
|
const hasField = isValidModelField(contentType, model.field);
|
||||||
const hasReference = _.get(value, ['attributes', model.references], false);
|
if (!hasField) {
|
||||||
if (!hasField || !hasReference) {
|
|
||||||
strapi.log.warn(
|
strapi.log.warn(
|
||||||
`[slugify] skipping ${value.info.singularName} registration, invalid field and/or reference provided.`
|
`[slugify] skipping ${contentType.info.singularName} registration, invalid field provided.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let references = _.isArray(model.references) ? model.references : [model.references];
|
||||||
|
const hasReferences = references.every((referenceField) =>
|
||||||
|
isValidModelField(contentType, referenceField)
|
||||||
|
);
|
||||||
|
if (!hasReferences) {
|
||||||
|
strapi.log.warn(
|
||||||
|
`[slugify] skipping ${contentType.info.singularName} registration, invalid reference field provided.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
uid,
|
uid,
|
||||||
...contentTypes[value.modelName],
|
...model,
|
||||||
contentType: value,
|
contentType,
|
||||||
|
references,
|
||||||
};
|
};
|
||||||
models[uid] = data;
|
settings.modelsByUID[uid] = data;
|
||||||
models[value.modelName] = data;
|
settings.modelsByName[contentType.modelName] = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
return models;
|
_.omit(settings, ['contentTypes']);
|
||||||
|
|
||||||
|
return settings;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { getPluginService } = require('../utils/getPluginService');
|
|
||||||
const { stringToSlug } = require('../utils/stringToSlug');
|
|
||||||
|
|
||||||
module.exports = ({ strapi }) => ({
|
|
||||||
slugify(ctx) {
|
|
||||||
const { models, slugifyOptions } = getPluginService(strapi, 'settingsService').get();
|
|
||||||
|
|
||||||
const { params, model: entityModel } = ctx;
|
|
||||||
const model = models[entityModel.uid];
|
|
||||||
const { data } = params;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { field, references } = model;
|
|
||||||
const referenceFieldValue = data[references];
|
|
||||||
|
|
||||||
// ensure the reference field has data
|
|
||||||
if (typeof referenceFieldValue === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data[field] = stringToSlug(referenceFieldValue, slugifyOptions);
|
|
||||||
},
|
|
||||||
|
|
||||||
async findOne(uid, query) {
|
|
||||||
const slugs = await strapi.entityService.findMany(uid, query);
|
|
||||||
|
|
||||||
// single
|
|
||||||
if (slugs && _.isPlainObject(slugs)) {
|
|
||||||
return slugs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// collection
|
|
||||||
if (slugs && _.isArray(slugs) && slugs.length) {
|
|
||||||
return slugs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// no result
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
41
server/services/slug-service/buildSlug.js
Normal file
41
server/services/slug-service/buildSlug.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const slugify = require('@sindresorhus/slugify');
|
||||||
|
|
||||||
|
const buildSlug = async (string, settings) => {
|
||||||
|
let slug = slugify(string, settings.slugifyOptions);
|
||||||
|
|
||||||
|
// slugify with count
|
||||||
|
if (!settings.slugifyWithCount) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugEntry = await strapi.db.query('plugin::slugify.slug').findOne({
|
||||||
|
select: ['id', 'count'],
|
||||||
|
where: { slug },
|
||||||
|
});
|
||||||
|
|
||||||
|
// if no result then count is 1 and base slug is returned
|
||||||
|
if (!slugEntry) {
|
||||||
|
await strapi.entityService.create('plugin::slugify.slug', {
|
||||||
|
data: {
|
||||||
|
slug,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = slugEntry.count + 1;
|
||||||
|
await strapi.entityService.update('plugin::slugify.slug', slugEntry.id, {
|
||||||
|
data: {
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const separator = settings.slugifyOptions.separator || '-';
|
||||||
|
return `${slug}${separator}${count}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildSlug,
|
||||||
|
};
|
40
server/services/slug-service/getReferenceFieldValues.js
Normal file
40
server/services/slug-service/getReferenceFieldValues.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const getReferenceFieldValues = (ctx, data, references) => {
|
||||||
|
return references
|
||||||
|
.filter((referenceField) => {
|
||||||
|
// check action specific fields
|
||||||
|
if (
|
||||||
|
referenceField === 'id' &&
|
||||||
|
(ctx.action === 'afterCreate' || ctx.action === 'beforeUpdate')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check general data fields
|
||||||
|
if (typeof data[referenceField] === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[referenceField] === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data[referenceField] === 'string' && data[referenceField].length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((referenceField) => {
|
||||||
|
if (referenceField === 'id') {
|
||||||
|
return ctx.result ? ctx.result.id : ctx.params.where.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[referenceField];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getReferenceFieldValues,
|
||||||
|
};
|
74
server/services/slug-service/index.js
Normal file
74
server/services/slug-service/index.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const { getPluginService } = require('../../utils/getPluginService');
|
||||||
|
const { shouldUpdateSlug } = require('./shoudUpdateSlug');
|
||||||
|
const { getReferenceFieldValues } = require('./getReferenceFieldValues');
|
||||||
|
const { buildSlug } = require('./buildSlug');
|
||||||
|
|
||||||
|
module.exports = ({ strapi }) => ({
|
||||||
|
async slugify(ctx) {
|
||||||
|
const { params, model: entityModel } = ctx;
|
||||||
|
const settings = getPluginService('settingsService').get();
|
||||||
|
const { data } = params;
|
||||||
|
|
||||||
|
const model = settings.modelsByUID[entityModel.uid];
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field, references } = model;
|
||||||
|
|
||||||
|
// do not add/update slug if it already has a value unless settings specify otherwise
|
||||||
|
if (!settings.shouldUpdateSlug && data[field]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the reference field has data
|
||||||
|
let referenceFieldValues = getReferenceFieldValues(ctx, data, references);
|
||||||
|
|
||||||
|
// respect skip undefined fields setting
|
||||||
|
const hasUndefinedFields = referenceFieldValues.length < references.length;
|
||||||
|
if ((!settings.skipUndefinedReferences && hasUndefinedFields) || !referenceFieldValues.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the slug should be updated based on the action type
|
||||||
|
let shouldUpdateSlugByAction = await shouldUpdateSlug(strapi, ctx, references);
|
||||||
|
if (!shouldUpdateSlugByAction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceFieldValues = referenceFieldValues.join(' ');
|
||||||
|
|
||||||
|
// update slug field based on action type
|
||||||
|
const slug = await buildSlug(referenceFieldValues, settings);
|
||||||
|
|
||||||
|
if (ctx.action === 'beforeCreate' || ctx.action === 'beforeUpdate') {
|
||||||
|
data[field] = slug;
|
||||||
|
} else if (ctx.action === 'afterCreate') {
|
||||||
|
strapi.entityService.update(model.uid, ctx.result.id, {
|
||||||
|
data: {
|
||||||
|
slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async findOne(uid, query) {
|
||||||
|
const slugs = await strapi.entityService.findMany(uid, query);
|
||||||
|
|
||||||
|
// single
|
||||||
|
if (slugs && _.isPlainObject(slugs)) {
|
||||||
|
return slugs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// collection
|
||||||
|
if (slugs && _.isArray(slugs) && slugs.length) {
|
||||||
|
return slugs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// no result
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
50
server/services/slug-service/shoudUpdateSlug.js
Normal file
50
server/services/slug-service/shoudUpdateSlug.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getReferenceFieldValues } = require('./getReferenceFieldValues');
|
||||||
|
|
||||||
|
const shouldUpdateSlugInAfterCreate = (strapi, ctx, references) => {
|
||||||
|
if (!references.includes('id')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldUpdateSlugInBeforeCreate = (strapi, ctx, references) => {
|
||||||
|
if (references.includes('id')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldUpdateSlugInBeforeUpdate = async (strapi, ctx, references) => {
|
||||||
|
const record = await strapi.entityService.findOne(ctx.model.uid, ctx.params.where.id);
|
||||||
|
|
||||||
|
let currentReferenceFieldValues = getReferenceFieldValues(ctx, record, references);
|
||||||
|
let referenceFieldValues = getReferenceFieldValues(ctx, ctx.params.data, references);
|
||||||
|
|
||||||
|
// only update if reference a reference field has changed
|
||||||
|
if (JSON.stringify(referenceFieldValues) == JSON.stringify(currentReferenceFieldValues)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldUpdateSlug = async (strapi, ctx, references) => {
|
||||||
|
let shouldUpdate = false;
|
||||||
|
if (ctx.action === 'beforeCreate') {
|
||||||
|
shouldUpdate = shouldUpdateSlugInBeforeCreate(strapi, ctx, references);
|
||||||
|
} else if (ctx.action === 'afterCreate') {
|
||||||
|
shouldUpdate = shouldUpdateSlugInAfterCreate(strapi, ctx, references);
|
||||||
|
} else if (ctx.action === 'beforeUpdate') {
|
||||||
|
shouldUpdate = shouldUpdateSlugInBeforeUpdate(strapi, ctx, references);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldUpdate;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
shouldUpdateSlug,
|
||||||
|
};
|
@ -1,7 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const SUPPORTED_LIFECYCLES = ['beforeCreate', 'beforeUpdate'];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
SUPPORTED_LIFECYCLES,
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ const { pluginId } = require('./pluginId');
|
|||||||
*
|
*
|
||||||
* @return service
|
* @return service
|
||||||
*/
|
*/
|
||||||
const getPluginService = (strapi, name, plugin = pluginId) => strapi.plugin(plugin).service(name);
|
const getPluginService = (name, plugin = pluginId) => strapi.plugin(plugin).service(name);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getPluginService,
|
getPluginService,
|
||||||
|
6
server/utils/hasRequiredModelScopes.js
Normal file
6
server/utils/hasRequiredModelScopes.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const hasRequiredModelScopes = (strapi, uid, auth) =>
|
||||||
|
strapi.auth.verify(auth, { scope: `${uid}.find` });
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hasRequiredModelScopes,
|
||||||
|
};
|
36
server/utils/isValidFindSlugParams.js
Normal file
36
server/utils/isValidFindSlugParams.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { ValidationError } = require('@strapi/utils').errors;
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const isValidFindSlugParams = (params) => {
|
||||||
|
if (!params) {
|
||||||
|
throw new ValidationError('A model and slug must be provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { modelName, slug, modelsByName, publicationState } = params;
|
||||||
|
const model = modelsByName[modelName];
|
||||||
|
|
||||||
|
if (!modelName) {
|
||||||
|
throw new ValidationError('A model name path variable is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw new ValidationError('A slug path variable is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.get(model, ['contentType', 'options', 'draftAndPublish'], false) && publicationState) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'Filtering by publication state is only supported for content types that have Draft and Publish enabled.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure valid model is passed
|
||||||
|
if (!model) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isValidFindSlugParams,
|
||||||
|
};
|
10
server/utils/isValidModelField.js
Normal file
10
server/utils/isValidModelField.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const isValidModelField = (model, field) =>
|
||||||
|
_.get(model, ['attributes', field], false) || field === 'id';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isValidModelField,
|
||||||
|
};
|
8
server/utils/sanitizeOutput.js
Normal file
8
server/utils/sanitizeOutput.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const { sanitize } = require('@strapi/utils');
|
||||||
|
|
||||||
|
const sanitizeOutput = (data, contentType, auth) =>
|
||||||
|
sanitize.contentAPI.output(data, contentType, { auth });
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sanitizeOutput,
|
||||||
|
};
|
@ -1,11 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const slugify = require('@sindresorhus/slugify');
|
|
||||||
|
|
||||||
const stringToSlug = (string, options) => {
|
|
||||||
return slugify(string, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stringToSlug,
|
|
||||||
};
|
|
@ -1,21 +1,31 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// copied from: https://raw.githubusercontent.com/strapi/strapi/master/packages/core/strapi/lib/core-api/controller/transform.js
|
|
||||||
|
|
||||||
const { isNil, isPlainObject } = require('lodash/fp');
|
const { isNil, isPlainObject } = require('lodash/fp');
|
||||||
|
|
||||||
const transformResponse = (resource, meta = {}, { contentType } = {}) => {
|
function response({ data, schema }) {
|
||||||
|
return transformResponse(data, {}, { contentType: schema });
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://github.com/strapi/strapi/blob/main/packages/core/strapi/src/core-api/controller/transform.ts
|
||||||
|
function isEntry(property) {
|
||||||
|
return property === null || isPlainObject(property) || Array.isArray(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDZEntries(property) {
|
||||||
|
return Array.isArray(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformResponse(resource, meta = {}, opts = {}) {
|
||||||
if (isNil(resource)) {
|
if (isNil(resource)) {
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: transformEntry(resource, contentType),
|
data: transformEntry(resource, opts?.contentType),
|
||||||
meta,
|
meta,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const transformComponent = (data, component) => {
|
function transformComponent(data, component) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data.map((datum) => transformComponent(datum, component));
|
return data.map((datum) => transformComponent(datum, component));
|
||||||
}
|
}
|
||||||
@ -28,9 +38,9 @@ const transformComponent = (data, component) => {
|
|||||||
|
|
||||||
const { id, attributes } = res;
|
const { id, attributes } = res;
|
||||||
return { id, ...attributes };
|
return { id, ...attributes };
|
||||||
};
|
}
|
||||||
|
|
||||||
const transformEntry = (entry, type) => {
|
function transformEntry(entry, type) {
|
||||||
if (isNil(entry)) {
|
if (isNil(entry)) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@ -47,17 +57,17 @@ const transformEntry = (entry, type) => {
|
|||||||
|
|
||||||
const attributeValues = {};
|
const attributeValues = {};
|
||||||
|
|
||||||
for (const key in properties) {
|
for (const key of Object.keys(properties)) {
|
||||||
const property = properties[key];
|
const property = properties[key];
|
||||||
const attribute = type && type.attributes[key];
|
const attribute = type && type.attributes[key];
|
||||||
|
|
||||||
if (attribute && attribute.type === 'relation') {
|
if (attribute && attribute.type === 'relation' && isEntry(property) && 'target' in attribute) {
|
||||||
const data = transformEntry(property, strapi.contentType(attribute.target));
|
const data = transformEntry(property, strapi.contentType(attribute.target));
|
||||||
|
|
||||||
attributeValues[key] = { data };
|
attributeValues[key] = { data };
|
||||||
} else if (attribute && attribute.type === 'component') {
|
} else if (attribute && attribute.type === 'component' && isEntry(property)) {
|
||||||
attributeValues[key] = transformComponent(property, strapi.components[attribute.component]);
|
attributeValues[key] = transformComponent(property, strapi.components[attribute.component]);
|
||||||
} else if (attribute && attribute.type === 'dynamiczone') {
|
} else if (attribute && attribute.type === 'dynamiczone' && isDZEntries(property)) {
|
||||||
if (isNil(property)) {
|
if (isNil(property)) {
|
||||||
attributeValues[key] = property;
|
attributeValues[key] = property;
|
||||||
}
|
}
|
||||||
@ -65,7 +75,7 @@ const transformEntry = (entry, type) => {
|
|||||||
attributeValues[key] = property.map((subProperty) => {
|
attributeValues[key] = property.map((subProperty) => {
|
||||||
return transformComponent(subProperty, strapi.components[subProperty.__component]);
|
return transformComponent(subProperty, strapi.components[subProperty.__component]);
|
||||||
});
|
});
|
||||||
} else if (attribute && attribute.type === 'media') {
|
} else if (attribute && attribute.type === 'media' && isEntry(property)) {
|
||||||
const data = transformEntry(property, strapi.contentType('plugin::upload.file'));
|
const data = transformEntry(property, strapi.contentType('plugin::upload.file'));
|
||||||
|
|
||||||
attributeValues[key] = { data };
|
attributeValues[key] = { data };
|
||||||
@ -77,9 +87,11 @@ const transformEntry = (entry, type) => {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
attributes: attributeValues,
|
attributes: attributeValues,
|
||||||
|
// NOTE: not necessary for now
|
||||||
|
// meta: {},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformResponse,
|
response,
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user