mirror of
https://github.com/ComfortablyCoding/strapi-plugin-slugify.git
synced 2025-08-16 00:02:44 -04:00
Compare commits
36 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 |
4
.github/workflows/npm-publish.yml
vendored
4
.github/workflows/npm-publish.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v14
|
||||
- name: Install Node v18
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
node-version: '18.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Yarn
|
||||
|
@ -54,13 +54,15 @@ 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.
|
||||
|
||||
> 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.
|
||||
|
||||
### Additional Requirement for GraphQL
|
||||
|
||||
Per [#35](https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues/35) please ensure that the slugify plugin configuration is placed **after** the graphql plugin configuration.
|
||||
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
|
||||
|
||||
@ -71,6 +73,7 @@ Per [#35](https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues/35)
|
||||
| contentTypes[modelName]field | The name of the field to add 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 |
|
||||
|
||||
|
24
package.json
24
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "strapi-plugin-slugify",
|
||||
"version": "2.2.1",
|
||||
"version": "2.3.8",
|
||||
"description": "A plugin for Strapi Headless CMS that provides the ability to auto slugify a field for any content type.",
|
||||
"scripts": {
|
||||
"lint": "eslint . --fix",
|
||||
@ -26,20 +26,18 @@
|
||||
"url": "https://github.com/ComfortablyCoding/strapi-plugin-slugify/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/slugify": "1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@strapi/strapi": "^4.14.0",
|
||||
"@strapi/utils": "^4.14.0",
|
||||
"lodash": "^4.17.21",
|
||||
"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": {
|
||||
"displayName": "Slugify",
|
||||
"name": "slugify",
|
||||
@ -47,7 +45,7 @@
|
||||
"kind": "plugin"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"node": ">=18.0.0 <=20.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
29
server/bootstrap.js
vendored
29
server/bootstrap.js
vendored
@ -1,29 +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();
|
||||
|
||||
// build settings structure
|
||||
const normalizedSettings = settingsService.build(settings);
|
||||
|
||||
// reset plugin settings
|
||||
settingsService.set(normalizedSettings);
|
||||
|
||||
// set up lifecycles
|
||||
const subscribe = {
|
||||
models: _.map(normalizedSettings.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,17 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const { pluginConfigSchema } = require('./schema');
|
||||
const schema = require('./schema');
|
||||
|
||||
module.exports = {
|
||||
default() {
|
||||
return {
|
||||
contentTypes: {},
|
||||
slugifyOptions: {},
|
||||
slugifyWithCount: false,
|
||||
skipUndefinedReferences: false,
|
||||
};
|
||||
},
|
||||
async validator(config) {
|
||||
await pluginConfigSchema.validate(config);
|
||||
},
|
||||
default: () => ({
|
||||
contentTypes: {},
|
||||
slugifyOptions: {},
|
||||
slugifyWithCount: false,
|
||||
shouldUpdateSlug: false,
|
||||
skipUndefinedReferences: false,
|
||||
}),
|
||||
validator: (config) => schema.validateSync(config),
|
||||
};
|
||||
|
@ -3,7 +3,7 @@
|
||||
const yup = require('yup');
|
||||
const _ = require('lodash');
|
||||
|
||||
const pluginConfigSchema = yup.object().shape({
|
||||
const schema = yup.object().shape({
|
||||
slugifyOptions: yup.object(),
|
||||
contentTypes: yup.lazy((obj) => {
|
||||
let shape = {};
|
||||
@ -18,9 +18,8 @@ const pluginConfigSchema = yup.object().shape({
|
||||
return yup.object().shape(shape);
|
||||
}),
|
||||
slugifyWithCount: yup.bool(),
|
||||
shouldUpdateSlug: yup.bool(),
|
||||
skipUndefinedReferences: yup.bool(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
pluginConfigSchema,
|
||||
};
|
||||
module.exports = schema;
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const { NotFoundError } = require('@strapi/utils/lib/errors');
|
||||
const { getPluginService } = require('../utils/getPluginService');
|
||||
const { transformResponse } = require('@strapi/strapi/lib/core-api/controller/transform');
|
||||
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams');
|
||||
const { sanitizeOutput } = require('../utils/sanitizeOutput');
|
||||
const { hasRequiredModelScopes } = require('../utils/hasRequiredModelScopes');
|
||||
const transform = require('../utils/transform');
|
||||
|
||||
module.exports = ({ strapi }) => ({
|
||||
async findSlug(ctx) {
|
||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
||||
const { modelsByName } = getPluginService('settingsService').get();
|
||||
const { modelName, slug } = ctx.request.params;
|
||||
const { auth } = ctx.state;
|
||||
|
||||
isValidFindSlugParams({
|
||||
modelName,
|
||||
slug,
|
||||
models,
|
||||
});
|
||||
try {
|
||||
isValidFindSlugParams({
|
||||
modelName,
|
||||
slug,
|
||||
modelsByName,
|
||||
});
|
||||
} catch (error) {
|
||||
return ctx.badRequest(error.message);
|
||||
}
|
||||
|
||||
const { uid, field, contentType } = models[modelName];
|
||||
const { uid, field, contentType } = modelsByName[modelName];
|
||||
|
||||
await hasRequiredModelScopes(strapi, uid, auth);
|
||||
try {
|
||||
await hasRequiredModelScopes(strapi, uid, auth);
|
||||
} catch (error) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
// add slug filter to any already existing query restrictions
|
||||
let query = ctx.query || {};
|
||||
@ -36,13 +43,13 @@ module.exports = ({ strapi }) => ({
|
||||
query.publicationState = 'live';
|
||||
}
|
||||
|
||||
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
|
||||
const data = await getPluginService('slugService').findOne(uid, query);
|
||||
|
||||
if (data) {
|
||||
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
|
||||
ctx.body = transformResponse(sanitizedEntity, {}, { contentType });
|
||||
ctx.body = transform.response({ data: sanitizedEntity, schema: contentType });
|
||||
} else {
|
||||
throw new NotFoundError();
|
||||
ctx.notFound();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ const registerGraphlQLQuery = (strapi) => {
|
||||
resolversConfig: getResolversConfig(),
|
||||
});
|
||||
|
||||
getPluginService(strapi, 'extension', 'graphql').use(extension);
|
||||
getPluginService('extension', 'graphql').use(extension);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@ -3,20 +3,21 @@ const { getPluginService } = require('../utils/getPluginService');
|
||||
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 { naming } = getPluginService(strapi, 'utils', 'graphql');
|
||||
const { toEntityResponse } = getPluginService(strapi, 'format', 'graphql').returnTypes;
|
||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
||||
const { naming } = getPluginService('utils', 'graphql');
|
||||
const { toEntityResponse } = getPluginService('format', 'graphql').returnTypes;
|
||||
const { modelsByUID } = getPluginService('settingsService').get();
|
||||
const { getEntityResponseName } = naming;
|
||||
|
||||
// get all types required for findSlug query
|
||||
let findSlugTypes = {
|
||||
response: [],
|
||||
};
|
||||
_.forEach(strapi.contentTypes, (value, key) => {
|
||||
if (models[key]) {
|
||||
findSlugTypes.response.push(getEntityResponseName(value));
|
||||
_.forEach(strapi.contentTypes, (contentType, uid) => {
|
||||
if (modelsByUID[uid]) {
|
||||
findSlugTypes.response.push(getEntityResponseName(contentType));
|
||||
}
|
||||
});
|
||||
|
||||
@ -33,7 +34,7 @@ const getCustomTypes = (strapi, nexus) => {
|
||||
t.members(...findSlugTypes.response);
|
||||
},
|
||||
resolveType: (ctx) => {
|
||||
return getEntityResponseName(models[ctx.info.resourceUID].contentType);
|
||||
return getEntityResponseName(modelsByUID[ctx.info.resourceUID].contentType);
|
||||
},
|
||||
});
|
||||
|
||||
@ -47,23 +48,30 @@ const getCustomTypes = (strapi, nexus) => {
|
||||
args: {
|
||||
modelName: nexus.stringArg('The model name of the content type'),
|
||||
slug: nexus.stringArg('The slug to query for'),
|
||||
publicationState: nexus.stringArg('The publication state of the entry')
|
||||
publicationState: nexus.stringArg('The publication state of the entry'),
|
||||
},
|
||||
resolve: async (_parent, args, ctx) => {
|
||||
const { models } = getPluginService(strapi, 'settingsService').get();
|
||||
const { modelsByName } = getPluginService('settingsService').get();
|
||||
const { modelName, slug, publicationState } = args;
|
||||
const { auth } = ctx.state;
|
||||
|
||||
isValidFindSlugParams({
|
||||
modelName,
|
||||
slug,
|
||||
models,
|
||||
publicationState
|
||||
});
|
||||
try {
|
||||
isValidFindSlugParams({
|
||||
modelName,
|
||||
slug,
|
||||
modelsByName,
|
||||
publicationState,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ValidationError(error.message);
|
||||
}
|
||||
const { uid, field, contentType } = modelsByName[modelName];
|
||||
|
||||
const { uid, field, contentType } = models[modelName];
|
||||
|
||||
await hasRequiredModelScopes(strapi, uid, auth);
|
||||
try {
|
||||
await hasRequiredModelScopes(strapi, uid, auth);
|
||||
} catch (error) {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
|
||||
// build query
|
||||
let query = {
|
||||
@ -75,9 +83,9 @@ const getCustomTypes = (strapi, nexus) => {
|
||||
// only return published entries by default if content type has draftAndPublish enabled
|
||||
if (_.get(contentType, ['options', 'draftAndPublish'], false)) {
|
||||
query.publicationState = publicationState || 'live';
|
||||
}
|
||||
}
|
||||
|
||||
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
|
||||
const data = await getPluginService('slugService').findOne(uid, query);
|
||||
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
|
||||
return toEntityResponse(sanitizedEntity, { resourceUID: uid });
|
||||
},
|
||||
|
@ -1,17 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('./bootstrap');
|
||||
const register = require('./register');
|
||||
const config = require('./config');
|
||||
const contentTypes = require('./content-types');
|
||||
const controllers = require('./controllers');
|
||||
const register = require('./register');
|
||||
const routes = require('./routes');
|
||||
const services = require('./services');
|
||||
|
||||
module.exports = {
|
||||
bootstrap,
|
||||
register,
|
||||
config,
|
||||
contentTypes,
|
||||
controllers,
|
||||
register,
|
||||
routes,
|
||||
services,
|
||||
};
|
||||
|
@ -1,8 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const { registerGraphlQLQuery } = require('./graphql');
|
||||
const { getPluginService } = require('./utils/getPluginService');
|
||||
|
||||
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
|
||||
if (strapi.plugin('graphql')) {
|
||||
strapi.log.info('[slugify] graphql detected, registering queries');
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const slugService = require('./slug-service');
|
||||
const settingsService = require('./settings-service');
|
||||
const slugService = require('./slug-service');
|
||||
|
||||
module.exports = {
|
||||
slugService,
|
||||
|
@ -13,7 +13,8 @@ module.exports = ({ strapi }) => ({
|
||||
},
|
||||
build(settings) {
|
||||
// build models
|
||||
settings.models = {};
|
||||
settings.modelsByUID = {};
|
||||
settings.modelsByName = {};
|
||||
_.each(strapi.contentTypes, (contentType, uid) => {
|
||||
const model = settings.contentTypes[contentType.modelName];
|
||||
if (!model) {
|
||||
@ -30,7 +31,9 @@ module.exports = ({ strapi }) => ({
|
||||
}
|
||||
|
||||
let references = _.isArray(model.references) ? model.references : [model.references];
|
||||
const hasReferences = references.every((r) => isValidModelField(contentType, r));
|
||||
const hasReferences = references.every((referenceField) =>
|
||||
isValidModelField(contentType, referenceField)
|
||||
);
|
||||
if (!hasReferences) {
|
||||
strapi.log.warn(
|
||||
`[slugify] skipping ${contentType.info.singularName} registration, invalid reference field provided.`
|
||||
@ -44,8 +47,8 @@ module.exports = ({ strapi }) => ({
|
||||
contentType,
|
||||
references,
|
||||
};
|
||||
settings.models[uid] = data;
|
||||
settings.models[contentType.modelName] = data;
|
||||
settings.modelsByUID[uid] = data;
|
||||
settings.modelsByName[contentType.modelName] = data;
|
||||
});
|
||||
|
||||
_.omit(settings, ['contentTypes']);
|
||||
|
@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const { getPluginService } = require('../utils/getPluginService');
|
||||
const { toSlug, toSlugWithCount } = require('../utils/slugification');
|
||||
|
||||
module.exports = ({ strapi }) => ({
|
||||
slugify(ctx) {
|
||||
const settings = getPluginService(strapi, 'settingsService').get();
|
||||
const { params, model: entityModel } = ctx;
|
||||
const { data } = params;
|
||||
|
||||
const model = settings.models[entityModel.uid];
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { field, references } = model;
|
||||
|
||||
// ensure the reference field has data
|
||||
let referenceFieldValues = references
|
||||
.filter((r) => typeof data[r] !== 'undefined' && data[r].length)
|
||||
.map((r) => data[r]);
|
||||
|
||||
const hasUndefinedFields = referenceFieldValues.length < references.length;
|
||||
if ((!settings.skipUndefinedReferences && hasUndefinedFields) || !referenceFieldValues.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
referenceFieldValues = referenceFieldValues.join(' ');
|
||||
if (settings.slugifyWithCount) {
|
||||
data[field] = toSlugWithCount(referenceFieldValues, settings.slugifyOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
data[field] = toSlug(referenceFieldValues, settings.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
|
||||
*/
|
||||
const getPluginService = (strapi, name, plugin = pluginId) => strapi.plugin(plugin).service(name);
|
||||
const getPluginService = (name, plugin = pluginId) => strapi.plugin(plugin).service(name);
|
||||
|
||||
module.exports = {
|
||||
getPluginService,
|
||||
|
@ -1,12 +1,5 @@
|
||||
const { ForbiddenError } = require('@strapi/utils/lib/errors');
|
||||
|
||||
const hasRequiredModelScopes = async (strapi, uid, auth) => {
|
||||
try {
|
||||
await strapi.auth.verify(auth, { scope: `${uid}.find` });
|
||||
} catch (e) {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
};
|
||||
const hasRequiredModelScopes = (strapi, uid, auth) =>
|
||||
strapi.auth.verify(auth, { scope: `${uid}.find` });
|
||||
|
||||
module.exports = {
|
||||
hasRequiredModelScopes,
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { ValidationError } = require('@strapi/utils/lib/errors');
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
const _ = require('lodash');
|
||||
|
||||
const isValidFindSlugParams = (params) => {
|
||||
@ -6,8 +6,8 @@ const isValidFindSlugParams = (params) => {
|
||||
throw new ValidationError('A model and slug must be provided.');
|
||||
}
|
||||
|
||||
const { modelName, slug, models, publicationState } = params;
|
||||
const model = models[modelName];
|
||||
const { modelName, slug, modelsByName, publicationState } = params;
|
||||
const model = modelsByName[modelName];
|
||||
|
||||
if (!modelName) {
|
||||
throw new ValidationError('A model name path variable is required.');
|
||||
@ -18,7 +18,9 @@ const isValidFindSlugParams = (params) => {
|
||||
}
|
||||
|
||||
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.')
|
||||
throw new ValidationError(
|
||||
'Filtering by publication state is only supported for content types that have Draft and Publish enabled.'
|
||||
);
|
||||
}
|
||||
|
||||
// ensure valid model is passed
|
||||
|
@ -2,7 +2,8 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const isValidModelField = (model, field) => _.get(model, ['attributes', field], false);
|
||||
const isValidModelField = (model, field) =>
|
||||
_.get(model, ['attributes', field], false) || field === 'id';
|
||||
|
||||
module.exports = {
|
||||
isValidModelField,
|
||||
|
@ -1,6 +1,7 @@
|
||||
const { contentAPI } = require('@strapi/utils/lib/sanitize');
|
||||
const { sanitize } = require('@strapi/utils');
|
||||
|
||||
const sanitizeOutput = (data, contentType, auth) => contentAPI.output(data, contentType, { auth });
|
||||
const sanitizeOutput = (data, contentType, auth) =>
|
||||
sanitize.contentAPI.output(data, contentType, { auth });
|
||||
|
||||
module.exports = {
|
||||
sanitizeOutput,
|
||||
|
@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const slugify = require('@sindresorhus/slugify');
|
||||
const slugifyWithCount = slugify.counter();
|
||||
|
||||
const toSlug = (string, options) => slugify(string, options);
|
||||
const toSlugWithCount = (string, options) => slugifyWithCount(string, options);
|
||||
|
||||
module.exports = {
|
||||
toSlug,
|
||||
toSlugWithCount,
|
||||
};
|
97
server/utils/transform.js
Normal file
97
server/utils/transform.js
Normal file
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
const { isNil, isPlainObject } = require('lodash/fp');
|
||||
|
||||
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)) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
return {
|
||||
data: transformEntry(resource, opts?.contentType),
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
function transformComponent(data, component) {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((datum) => transformComponent(datum, component));
|
||||
}
|
||||
|
||||
const res = transformEntry(data, component);
|
||||
|
||||
if (isNil(res)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const { id, attributes } = res;
|
||||
return { id, ...attributes };
|
||||
}
|
||||
|
||||
function transformEntry(entry, type) {
|
||||
if (isNil(entry)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (Array.isArray(entry)) {
|
||||
return entry.map((singleEntry) => transformEntry(singleEntry, type));
|
||||
}
|
||||
|
||||
if (!isPlainObject(entry)) {
|
||||
throw new Error('Entry must be an object');
|
||||
}
|
||||
|
||||
const { id, ...properties } = entry;
|
||||
|
||||
const attributeValues = {};
|
||||
|
||||
for (const key of Object.keys(properties)) {
|
||||
const property = properties[key];
|
||||
const attribute = type && type.attributes[key];
|
||||
|
||||
if (attribute && attribute.type === 'relation' && isEntry(property) && 'target' in attribute) {
|
||||
const data = transformEntry(property, strapi.contentType(attribute.target));
|
||||
|
||||
attributeValues[key] = { data };
|
||||
} else if (attribute && attribute.type === 'component' && isEntry(property)) {
|
||||
attributeValues[key] = transformComponent(property, strapi.components[attribute.component]);
|
||||
} else if (attribute && attribute.type === 'dynamiczone' && isDZEntries(property)) {
|
||||
if (isNil(property)) {
|
||||
attributeValues[key] = property;
|
||||
}
|
||||
|
||||
attributeValues[key] = property.map((subProperty) => {
|
||||
return transformComponent(subProperty, strapi.components[subProperty.__component]);
|
||||
});
|
||||
} else if (attribute && attribute.type === 'media' && isEntry(property)) {
|
||||
const data = transformEntry(property, strapi.contentType('plugin::upload.file'));
|
||||
|
||||
attributeValues[key] = { data };
|
||||
} else {
|
||||
attributeValues[key] = property;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
attributes: attributeValues,
|
||||
// NOTE: not necessary for now
|
||||
// meta: {},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
response,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user