Compare commits

..

No commits in common. "master" and "2.1.0" have entirely different histories.

33 changed files with 403 additions and 9564 deletions

View File

@ -13,10 +13,10 @@ jobs:
- name: Checkout branch - name: Checkout branch
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v18 - name: Install Node v14
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '18.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Yarn - name: Install Yarn

View File

@ -20,9 +20,11 @@ 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
``` ```
@ -54,17 +56,11 @@ 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: To rewrite the same field (e.g. `title` is both a reference and a slug) use `title` as the `field` and `references` value. > 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: 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.
### Additional Requirement for GraphQL ### The Complete Plugin Configuration Object
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 |
| -------- | ----------- | ---- | ------- | -------- | | -------- | ----------- | ---- | ------- | -------- |
@ -73,7 +69,6 @@ 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]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 | | 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 | | 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 | | 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 |
@ -87,7 +82,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. Additionally the endpoint accepts any of the parameters that can be added to the routes built into Strapi. 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.
**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.
@ -125,25 +120,6 @@ 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

View File

@ -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.3.8", "version": "2.1.0",
"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,17 +26,19 @@
"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",
"@strapi/utils": "^4.14.0",
"lodash": "^4.17.21",
"yup": "^0.32.9"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.53.0", "eslint": "^8.8.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"prettier": "^3.1.0" "prettier": "^2.5.1"
},
"peerDependencies": {
"@strapi/strapi": "^4.0.7",
"@strapi/utils": "^4.0.7",
"lodash": "^4.17.21",
"yup": "^0.32.9"
}, },
"strapi": { "strapi": {
"displayName": "Slugify", "displayName": "Slugify",
@ -45,7 +47,7 @@
"kind": "plugin" "kind": "plugin"
}, },
"engines": { "engines": {
"node": ">=18.0.0 <=20.x.x", "node": ">=12.x.x <=16.x.x",
"npm": ">=6.0.0" "npm": ">=6.0.0"
}, },
"keywords": [ "keywords": [

29
server/bootstrap.js vendored Normal file
View File

@ -0,0 +1,29 @@
'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);
};

View File

@ -1,17 +0,0 @@
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,
};

View File

@ -1,16 +0,0 @@
'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);
};

View File

@ -1,18 +0,0 @@
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,
};

View File

@ -1,75 +0,0 @@
'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,
};

View File

@ -1,14 +1,17 @@
'use strict'; 'use strict';
const schema = require('./schema'); const { pluginConfigSchema } = require('./schema');
module.exports = { module.exports = {
default: () => ({ default() {
contentTypes: {}, return {
slugifyOptions: {}, contentTypes: {},
slugifyWithCount: false, slugifyOptions: {},
shouldUpdateSlug: false, slugifyWithCount: false,
skipUndefinedReferences: false, skipUndefinedReferences: false,
}), };
validator: (config) => schema.validateSync(config), },
async validator(config) {
await pluginConfigSchema.validate(config);
},
}; };

View File

@ -3,7 +3,7 @@
const yup = require('yup'); const yup = require('yup');
const _ = require('lodash'); const _ = require('lodash');
const schema = yup.object().shape({ const pluginConfigSchema = yup.object().shape({
slugifyOptions: yup.object(), slugifyOptions: yup.object(),
contentTypes: yup.lazy((obj) => { contentTypes: yup.lazy((obj) => {
let shape = {}; let shape = {};
@ -18,8 +18,9 @@ const schema = yup.object().shape({
return yup.object().shape(shape); return yup.object().shape(shape);
}), }),
slugifyWithCount: yup.bool(), slugifyWithCount: yup.bool(),
shouldUpdateSlug: yup.bool(),
skipUndefinedReferences: yup.bool(), skipUndefinedReferences: yup.bool(),
}); });
module.exports = schema; module.exports = {
pluginConfigSchema,
};

View File

@ -1,9 +0,0 @@
'use strict';
const slugSchema = require('./slug/schema.json');
module.exports = {
slug: {
schema: slugSchema,
},
};

View File

@ -1,29 +0,0 @@
{
"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"
}
}
}

View File

@ -1,55 +1,56 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const { sanitize } = require('@strapi/utils');
const { getPluginService } = require('../utils/getPluginService'); const { getPluginService } = require('../utils/getPluginService');
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams'); const { transformResponse } = require('@strapi/strapi/lib/core-api/controller/transform');
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 { modelsByName } = getPluginService('settingsService').get(); const { models } = getPluginService(strapi, 'settingsService').get();
const { modelName, slug } = ctx.request.params; const { params } = ctx.request;
const { auth } = ctx.state; const { modelName, slug } = params;
try { try {
isValidFindSlugParams({ if (!modelName) {
modelName, throw Error('A model name path variable is required.');
slug, }
modelsByName,
}); if (!slug) {
throw Error('A slug path variable is required.');
}
const model = models[modelName];
if (!model) {
throw Error(
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
);
}
const { uid, field, contentType } = model;
// add slug filter to any already existing query restrictions
let query = ctx.query || {};
if (!query.filters) {
query.filters = {};
}
query.filters[field] = slug;
// only return published entries by default if content type has draftAndPublish enabled
if (_.get(contentType, ['options', 'draftAndPublish'], false) && !query.publicationState) {
query.publicationState = 'live';
}
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
if (data) {
const sanitizedEntity = await sanitize.contentAPI.output(data, contentType);
ctx.body = transformResponse(sanitizedEntity);
} else {
ctx.notFound();
}
} catch (error) { } catch (error) {
return ctx.badRequest(error.message); ctx.badRequest(error.message);
}
const { uid, field, contentType } = modelsByName[modelName];
try {
await hasRequiredModelScopes(strapi, uid, auth);
} catch (error) {
return ctx.forbidden();
}
// add slug filter to any already existing query restrictions
let query = ctx.query || {};
if (!query.filters) {
query.filters = {};
}
query.filters[field] = slug;
// only return published entries by default if content type has draftAndPublish enabled
if (_.get(contentType, ['options', 'draftAndPublish'], false) && !query.publicationState) {
query.publicationState = 'live';
}
const data = await getPluginService('slugService').findOne(uid, query);
if (data) {
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
ctx.body = transform.response({ data: sanitizedEntity, schema: contentType });
} else {
ctx.notFound();
} }
}, },
}); });

View File

@ -11,7 +11,7 @@ const registerGraphlQLQuery = (strapi) => {
resolversConfig: getResolversConfig(), resolversConfig: getResolversConfig(),
}); });
getPluginService('extension', 'graphql').use(extension); getPluginService(strapi, 'extension', 'graphql').use(extension);
}; };
module.exports = { module.exports = {

View File

@ -1,23 +1,20 @@
const _ = require('lodash'); const _ = require('lodash');
const { getPluginService } = require('../utils/getPluginService'); const { getPluginService } = require('../utils/getPluginService');
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams'); const { ValidationError } = require('@strapi/utils').errors;
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('utils', 'graphql'); const { naming } = getPluginService(strapi, 'utils', 'graphql');
const { toEntityResponse } = getPluginService('format', 'graphql').returnTypes; const { toEntityResponse } = getPluginService(strapi, 'format', 'graphql').returnTypes;
const { modelsByUID } = getPluginService('settingsService').get(); const { models } = getPluginService(strapi, '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, (contentType, uid) => { _.forEach(strapi.contentTypes, (value, key) => {
if (modelsByUID[uid]) { if (models[key]) {
findSlugTypes.response.push(getEntityResponseName(contentType)); findSlugTypes.response.push(getEntityResponseName(value));
} }
}); });
@ -34,7 +31,7 @@ const getCustomTypes = (strapi, nexus) => {
t.members(...findSlugTypes.response); t.members(...findSlugTypes.response);
}, },
resolveType: (ctx) => { resolveType: (ctx) => {
return getEntityResponseName(modelsByUID[ctx.info.resourceUID].contentType); return getEntityResponseName(models[ctx.info.resourceUID].contentType);
}, },
}); });
@ -48,46 +45,34 @@ 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, ctx) => { resolve: async (_parent, args) => {
const { modelsByName } = getPluginService('settingsService').get(); const { models } = getPluginService(strapi, 'settingsService').get();
const { modelName, slug, publicationState } = args; const { modelName, slug } = args;
const { auth } = ctx.state;
try { const model = models[modelName];
isValidFindSlugParams({
modelName,
slug,
modelsByName,
publicationState,
});
} catch (error) {
throw new ValidationError(error.message);
}
const { uid, field, contentType } = modelsByName[modelName];
try { // ensure valid model is passed
await hasRequiredModelScopes(strapi, uid, auth); if (!model) {
} catch (error) { throw new ValidationError(
throw new ForbiddenError(); `${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
);
} }
// build query const { uid, field, contentType } = model;
let query = { let query = {
filters: { filters: {
[field]: slug, [field]: slug,
}, },
}; };
// only return published entries by default if content type has draftAndPublish enabled // only return published entries
if (_.get(contentType, ['options', 'draftAndPublish'], false)) { if (_.get(contentType, ['options', 'draftAndPublish'], false)) {
query.publicationState = publicationState || 'live'; query.publicationState = 'live';
} }
const data = await getPluginService('slugService').findOne(uid, query); const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
const sanitizedEntity = await sanitizeOutput(data, contentType, auth); return toEntityResponse(data, { resourceUID: uid });
return toEntityResponse(sanitizedEntity, { resourceUID: uid });
}, },
}); });
}, },

View File

@ -1,19 +1,17 @@
'use strict'; 'use strict';
const bootstrap = require('./bootstrap'); const bootstrap = require('./bootstrap');
const config = require('./config');
const contentTypes = require('./content-types');
const controllers = require('./controllers');
const register = require('./register'); const register = require('./register');
const config = require('./config');
const controllers = require('./controllers');
const routes = require('./routes'); const routes = require('./routes');
const services = require('./services'); const services = require('./services');
module.exports = { module.exports = {
bootstrap, bootstrap,
config,
contentTypes,
controllers,
register, register,
config,
controllers,
routes, routes,
services, services,
}; };

View File

@ -1,15 +1,8 @@
'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');

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const settingsService = require('./settings-service');
const slugService = require('./slug-service'); const slugService = require('./slug-service');
const settingsService = require('./settings-service');
module.exports = { module.exports = {
slugService, slugService,

View File

@ -13,8 +13,7 @@ module.exports = ({ strapi }) => ({
}, },
build(settings) { build(settings) {
// build models // build models
settings.modelsByUID = {}; settings.models = {};
settings.modelsByName = {};
_.each(strapi.contentTypes, (contentType, uid) => { _.each(strapi.contentTypes, (contentType, uid) => {
const model = settings.contentTypes[contentType.modelName]; const model = settings.contentTypes[contentType.modelName];
if (!model) { if (!model) {
@ -31,9 +30,7 @@ module.exports = ({ strapi }) => ({
} }
let references = _.isArray(model.references) ? model.references : [model.references]; let references = _.isArray(model.references) ? model.references : [model.references];
const hasReferences = references.every((referenceField) => const hasReferences = references.every((r) => isValidModelField(contentType, r));
isValidModelField(contentType, referenceField)
);
if (!hasReferences) { if (!hasReferences) {
strapi.log.warn( strapi.log.warn(
`[slugify] skipping ${contentType.info.singularName} registration, invalid reference field provided.` `[slugify] skipping ${contentType.info.singularName} registration, invalid reference field provided.`
@ -47,8 +44,8 @@ module.exports = ({ strapi }) => ({
contentType, contentType,
references, references,
}; };
settings.modelsByUID[uid] = data; settings.models[uid] = data;
settings.modelsByName[contentType.modelName] = data; settings.models[contentType.modelName] = data;
}); });
_.omit(settings, ['contentTypes']); _.omit(settings, ['contentTypes']);

View File

@ -0,0 +1,56 @@
'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;
},
});

View File

@ -1,41 +0,0 @@
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,
};

View File

@ -1,40 +0,0 @@
'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,
};

View File

@ -1,74 +0,0 @@
'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;
},
});

View File

@ -1,50 +0,0 @@
'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,
};

View File

@ -0,0 +1,7 @@
'use strict';
const SUPPORTED_LIFECYCLES = ['beforeCreate', 'beforeUpdate'];
module.exports = {
SUPPORTED_LIFECYCLES,
};

View File

@ -7,7 +7,7 @@ const { pluginId } = require('./pluginId');
* *
* @return service * @return service
*/ */
const getPluginService = (name, plugin = pluginId) => strapi.plugin(plugin).service(name); const getPluginService = (strapi, name, plugin = pluginId) => strapi.plugin(plugin).service(name);
module.exports = { module.exports = {
getPluginService, getPluginService,

View File

@ -1,6 +0,0 @@
const hasRequiredModelScopes = (strapi, uid, auth) =>
strapi.auth.verify(auth, { scope: `${uid}.find` });
module.exports = {
hasRequiredModelScopes,
};

View File

@ -1,36 +0,0 @@
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,
};

View File

@ -2,8 +2,7 @@
const _ = require('lodash'); const _ = require('lodash');
const isValidModelField = (model, field) => const isValidModelField = (model, field) => _.get(model, ['attributes', field], false);
_.get(model, ['attributes', field], false) || field === 'id';
module.exports = { module.exports = {
isValidModelField, isValidModelField,

View File

@ -1,8 +0,0 @@
const { sanitize } = require('@strapi/utils');
const sanitizeOutput = (data, contentType, auth) =>
sanitize.contentAPI.output(data, contentType, { auth });
module.exports = {
sanitizeOutput,
};

View File

@ -0,0 +1,12 @@
'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,
};

View File

@ -1,97 +0,0 @@
'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,
};

9076
yarn.lock

File diff suppressed because it is too large Load Diff