From 4388f07de68946dbff7b0c39a2d50a678cecec1b Mon Sep 17 00:00:00 2001 From: Boaz Poolman Date: Mon, 9 Dec 2024 21:16:51 +0100 Subject: [PATCH] refactor: initial migration of the server code --- packages/core/server/admin-api/bootstrap.ts | 36 -- .../admin-api/controllers/url-pattern.ts | 90 ---- .../migrations/plugin-options-rename.ts | 38 -- packages/core/server/admin-api/routes/info.ts | 36 -- .../core/server/admin-api/routes/url-alias.ts | 60 --- .../server/admin-api/routes/url-pattern.ts | 60 --- .../services/query-layer-decorator.ts | 436 ------------------ .../server/admin-api/services/url-alias.ts | 168 ------- packages/core/server/bootstrap.ts | 67 +++ .../core/server/{admin-api => }/config.ts | 0 packages/core/server/content-api/bootstrap.js | 35 -- .../content-api/controllers/url-alias.ts | 10 - .../core/server/content-api/routes/core.ts | 12 - .../server/content-api/routes/url-alias.ts | 12 - .../server/content-api/services/by-path.ts | 62 --- .../server/content-api/services/url-alias.ts | 9 - packages/core/server/content-types/index.ts | 11 + .../content-types/url-alias/schema.json | 0 .../content-types/url-pattern/schema.json | 0 .../__tests__/core.test.ts | 0 .../{content-api => }/controllers/core.ts | 4 +- packages/core/server/controllers/index.ts | 11 + .../{admin-api => }/controllers/info.ts | 3 +- .../{admin-api => }/controllers/url-alias.ts | 48 +- .../core/server/controllers/url-pattern.ts | 55 +++ .../hooks/__tests__/disable.test.ts | 0 .../server/{admin-api => }/hooks/disable.ts | 10 +- packages/core/server/index.ts | 91 +--- .../core/server/{admin-api => }/register.ts | 14 +- packages/core/server/routes/index.ts | 183 ++++++++ .../__tests__/query-layer-decorator.test.ts | 0 .../{admin-api => }/services/bulk-generate.ts | 16 +- packages/core/server/services/index.ts | 11 + .../server/services/query-layer-decorator.ts | 436 ++++++++++++++++++ packages/core/server/services/url-alias.ts | 98 ++++ .../{admin-api => }/services/url-pattern.ts | 96 +--- 36 files changed, 940 insertions(+), 1278 deletions(-) delete mode 100644 packages/core/server/admin-api/bootstrap.ts delete mode 100644 packages/core/server/admin-api/controllers/url-pattern.ts delete mode 100644 packages/core/server/admin-api/migrations/plugin-options-rename.ts delete mode 100644 packages/core/server/admin-api/routes/info.ts delete mode 100644 packages/core/server/admin-api/routes/url-alias.ts delete mode 100644 packages/core/server/admin-api/routes/url-pattern.ts delete mode 100644 packages/core/server/admin-api/services/query-layer-decorator.ts delete mode 100644 packages/core/server/admin-api/services/url-alias.ts create mode 100644 packages/core/server/bootstrap.ts rename packages/core/server/{admin-api => }/config.ts (100%) delete mode 100644 packages/core/server/content-api/bootstrap.js delete mode 100644 packages/core/server/content-api/controllers/url-alias.ts delete mode 100644 packages/core/server/content-api/routes/core.ts delete mode 100644 packages/core/server/content-api/routes/url-alias.ts delete mode 100644 packages/core/server/content-api/services/by-path.ts delete mode 100644 packages/core/server/content-api/services/url-alias.ts create mode 100644 packages/core/server/content-types/index.ts rename packages/core/server/{admin-api => }/content-types/url-alias/schema.json (100%) rename packages/core/server/{admin-api => }/content-types/url-pattern/schema.json (100%) rename packages/core/server/{content-api => controllers}/__tests__/core.test.ts (100%) rename packages/core/server/{content-api => }/controllers/core.ts (88%) create mode 100644 packages/core/server/controllers/index.ts rename packages/core/server/{admin-api => }/controllers/info.ts (97%) rename packages/core/server/{admin-api => }/controllers/url-alias.ts (51%) create mode 100644 packages/core/server/controllers/url-pattern.ts rename packages/core/server/{admin-api => }/hooks/__tests__/disable.test.ts (100%) rename packages/core/server/{admin-api => }/hooks/disable.ts (78%) rename packages/core/server/{admin-api => }/register.ts (74%) create mode 100644 packages/core/server/routes/index.ts rename packages/core/server/{admin-api => services}/__tests__/query-layer-decorator.test.ts (100%) rename packages/core/server/{admin-api => }/services/bulk-generate.ts (93%) create mode 100644 packages/core/server/services/index.ts create mode 100644 packages/core/server/services/query-layer-decorator.ts create mode 100644 packages/core/server/services/url-alias.ts rename packages/core/server/{admin-api => }/services/url-pattern.ts (79%) diff --git a/packages/core/server/admin-api/bootstrap.ts b/packages/core/server/admin-api/bootstrap.ts deleted file mode 100644 index 04d410e1..00000000 --- a/packages/core/server/admin-api/bootstrap.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getPluginService } from '../util/getPluginService'; -import { IStrapi } from '../types/strapi'; - -export default (strapi: IStrapi) => { - try { - // Decorate the entity service with review workflow logic - const { decorator } = getPluginService('queryLayerDecorator'); - strapi.entityService.decorate(decorator); - - // Register permission actions. - const actions = [ - { - section: 'plugins', - displayName: 'Access the URL alias list', - uid: 'settings.list', - pluginName: 'webtools', - }, - { - section: 'plugins', - displayName: 'Access the URL alias patterns', - uid: 'settings.patterns', - pluginName: 'webtools', - }, - { - section: 'plugins', - displayName: 'Access the URL alias sidebar', - uid: 'edit-view.sidebar', - pluginName: 'webtools', - }, - ]; - - strapi.admin.services.permission.actionProvider.registerMany(actions); - } catch (error) { - strapi.log.error(`Bootstrap failed. ${String(error)}`); - } -}; diff --git a/packages/core/server/admin-api/controllers/url-pattern.ts b/packages/core/server/admin-api/controllers/url-pattern.ts deleted file mode 100644 index a08e96d8..00000000 --- a/packages/core/server/admin-api/controllers/url-pattern.ts +++ /dev/null @@ -1,90 +0,0 @@ - - -import get from 'lodash/get'; -import { Context } from 'koa'; -import { Common, EntityService, Schema } from '@strapi/strapi'; - -import { getPluginService } from '../../util/getPluginService'; -import { KoaContext } from '../../types/koa'; - -/** - * Pattern controller - */ - -const controller = () => ({ - findOne: async (ctx: Context & { params: { id: number } }) => { - const { id } = ctx.params; - const patternEntity = await getPluginService('urlPatternService').findOne( - id, - ); - ctx.body = patternEntity; - }, - findMany: async (ctx: Context) => { - const patternEntities = await getPluginService( - 'urlPatternService', - ).findMany({}); - ctx.body = patternEntities; - }, - delete: async (ctx: Context & { params: { id: number } }) => { - const { id } = ctx.params; - await getPluginService('urlPatternService').delete(id); - ctx.body = { succes: true }; - }, - update: async (ctx: KoaContext> & { params: { id: number } }) => { - const { id } = ctx.params; - const { data } = ctx.request.body; - const patternEntity = await getPluginService('urlPatternService').update( - id, - data, - ); - ctx.body = patternEntity; - }, - create: async (ctx: KoaContext>) => { - const { data } = ctx.request.body; - const patternEntity = await getPluginService('urlPatternService').create( - data, - ); - ctx.body = patternEntity; - }, - allowedFields: (ctx: Context) => { - const formattedFields = {}; - - Object.values(strapi.contentTypes).forEach((contentType: Schema.ContentType) => { - const { pluginOptions } = contentType; - - // Not for CTs that are not visible in the content manager. - const isInContentManager = get(pluginOptions, [ - 'content-manager', - 'visible', - ]) as boolean; - if (isInContentManager === false) return; - - const fields = getPluginService('urlPatternService').getAllowedFields( - contentType, - ['pluralName', 'string', 'uid', 'id'], - ); - formattedFields[contentType.uid] = fields; - }); - - ctx.body = formattedFields; - }, - - validatePattern: (ctx: KoaContext<{ pattern: string, modelName: Common.UID.ContentType }>) => { - const urlPatternService = getPluginService('urlPatternService'); - const { pattern, modelName } = ctx.request.body; - - const contentType = strapi.contentTypes[modelName]; - - const fields = urlPatternService.getAllowedFields(contentType, [ - 'pluralName', - 'string', - 'uid', - 'id', - ]); - const validated = urlPatternService.validatePattern(pattern, fields); - - ctx.body = validated; - }, -}); - -export default controller; diff --git a/packages/core/server/admin-api/migrations/plugin-options-rename.ts b/packages/core/server/admin-api/migrations/plugin-options-rename.ts deleted file mode 100644 index 667713f1..00000000 --- a/packages/core/server/admin-api/migrations/plugin-options-rename.ts +++ /dev/null @@ -1,38 +0,0 @@ -import get from 'lodash/get'; -import { Schema } from '@strapi/strapi'; -import { IStrapi } from '../../types/strapi'; - -const migratePluginOptionsRename = (strapi: IStrapi) => { - Object.values(strapi.contentTypes).forEach((contentType: Schema.ContentType) => { - const deprecatedPluginOptions = get(contentType.pluginOptions, ['url-alias'], null) as object; - - if (deprecatedPluginOptions === null) { - return; - } - - const updatedContentType: Schema.ContentType = contentType; - - // Delete the old plugin options - delete updatedContentType.pluginOptions['url-alias']; - - // Set the new options - // @ts-ignore - updatedContentType.pluginOptions.webtools = deprecatedPluginOptions; - - // Format the content type - /* disabled the no-unsafe-call linting rule */ - /* we do that because strapi.services is not properly typed */ - /* maby we can remove this in the future */ - /* eslint-disable @typescript-eslint/no-unsafe-call */ - const formattedContentType = strapi.services['plugin::content-type-builder.content-types'] - .formatContentType(updatedContentType) as { schema: object }; - - // Update the content type - strapi.services['plugin::content-type-builder.content-types'].editContentType( - contentType.uid, - { contentType: formattedContentType.schema }, - ); - }); -}; - -export default migratePluginOptionsRename; diff --git a/packages/core/server/admin-api/routes/info.ts b/packages/core/server/admin-api/routes/info.ts deleted file mode 100644 index 4e730252..00000000 --- a/packages/core/server/admin-api/routes/info.ts +++ /dev/null @@ -1,36 +0,0 @@ - - -export default [ - { - method: 'GET', - path: '/info/getContentTypes', - handler: 'info.getContentTypes', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/info/getLanguages', - handler: 'info.getLanguages', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/info/addons', - handler: 'info.getAddons', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/info/config', - handler: 'info.getConfig', - config: { - policies: [], - }, - }, -]; diff --git a/packages/core/server/admin-api/routes/url-alias.ts b/packages/core/server/admin-api/routes/url-alias.ts deleted file mode 100644 index 010facaf..00000000 --- a/packages/core/server/admin-api/routes/url-alias.ts +++ /dev/null @@ -1,60 +0,0 @@ - - -export default [ - { - method: 'GET', - path: '/url-alias/findOne/:id', - handler: 'url-alias-admin.findOne', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/url-alias/findMany', - handler: 'url-alias-admin.findMany', - config: { - policies: [], - }, - }, - { - method: 'POST', - path: '/url-alias/delete/:id', - handler: 'url-alias-admin.delete', - config: { - policies: [], - }, - }, - { - method: 'PUT', - path: '/url-alias/update/:id', - handler: 'url-alias-admin.update', - config: { - policies: [], - }, - }, - { - method: 'POST', - path: '/url-alias/create', - handler: 'url-alias-admin.create', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/url-alias/editLink', - handler: 'url-alias-admin.editLink', - config: { - policies: [], - }, - }, - { - method: 'POST', - path: '/url-alias/generate', - handler: 'url-alias-admin.generate', - config: { - policies: [], - }, - }, -]; diff --git a/packages/core/server/admin-api/routes/url-pattern.ts b/packages/core/server/admin-api/routes/url-pattern.ts deleted file mode 100644 index f99d39ad..00000000 --- a/packages/core/server/admin-api/routes/url-pattern.ts +++ /dev/null @@ -1,60 +0,0 @@ - - -export default [ - { - method: 'GET', - path: '/url-pattern/findOne/:id', - handler: 'url-pattern.findOne', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/url-pattern/findMany', - handler: 'url-pattern.findMany', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/url-pattern/delete/:id', - handler: 'url-pattern.delete', - config: { - policies: [], - }, - }, - { - method: 'PUT', - path: '/url-pattern/update/:id', - handler: 'url-pattern.update', - config: { - policies: [], - }, - }, - { - method: 'POST', - path: '/url-pattern/create', - handler: 'url-pattern.create', - config: { - policies: [], - }, - }, - { - method: 'GET', - path: '/url-pattern/allowed-fields', - handler: 'url-pattern.allowedFields', - config: { - policies: [], - }, - }, - { - method: 'POST', - path: '/url-pattern/validate', - handler: 'url-pattern.validatePattern', - config: { - policies: [], - }, - }, -]; diff --git a/packages/core/server/admin-api/services/query-layer-decorator.ts b/packages/core/server/admin-api/services/query-layer-decorator.ts deleted file mode 100644 index 88c8ac0c..00000000 --- a/packages/core/server/admin-api/services/query-layer-decorator.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { Attribute, Common } from '@strapi/types'; -import { ID } from '@strapi/types/dist/types/core/entity'; -import { IDecoratedService, IDecoratedServiceOptions } from '../../types/strapi'; -import { isContentTypeEnabled } from '../../util/enabledContentTypes'; -import { getPluginService } from '../../util/getPluginService'; - -/** - * Decorates the entity service with WT business logic - * @param {object} service - entity service - */ -const decorator = (service: IDecoratedService) => ({ - async create(uid: Common.UID.ContentType, opts: IDecoratedServiceOptions<{ url_alias: number }>) { - const hasWT = isContentTypeEnabled(uid); - let urlAliasEntity: Attribute.GetValues<'plugin::webtools.url-alias', Attribute.GetNonPopulatableKeys<'plugin::webtools.url-alias'>>; - - // If Webtools isn't enabled, do nothing. - if (!hasWT) { - return service.create.call(this, uid, opts); - } - - // Fetch the URL pattern for this content type. - let relations: string[] = []; - let languages: string[] = [undefined]; - - if (strapi.plugin('i18n')) { - languages = []; - const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); - languages = locales.map((locale) => locale.code); - } - - await Promise.all(languages.map(async (lang) => { - const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); - const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); - - relations = [...relations, ...languageRelations]; - })); - - // If a URL alias was created, fetch it. - if (opts.data.url_alias) { - urlAliasEntity = await getPluginService('urlAliasService').findOne(opts.data.url_alias); - } - - // If a URL alias was created and 'generated' is set to false, do nothing. - if (urlAliasEntity?.generated === false) { - return service.create.call(this, uid, opts); - } - - // Ideally here we would create the URL alias an directly fire - // the `service.create.call` function with the new URL alias id. - // Though it is possible that the `id` field is used in the URL. - // In that case we have to create the entity first. Then when we know - // the id, can we create the URL alias entity and can we update - // the previously created entity. - const newEntity = await service.create.call(this, uid, { - ...opts, - data: opts.data, - populate: { - ...opts.populate, - ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), - localizations: { - populate: { - url_alias: { - fields: ['id'], - }, - }, - }, - }, - }); - - // Fetch the URL alias localizations. - const urlAliasLocalizations = newEntity.localizations - ?.map((loc) => loc.url_alias.id) - ?.filter((loc) => loc) || []; - - const newEntityWithoutLocalizations = { - ...newEntity, - localizations: undefined, - }; - - const combinedEntity = { ...newEntityWithoutLocalizations }; - const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, combinedEntity.locale); - - await Promise.all(urlPatterns.map(async (urlPattern) => { - const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, combinedEntity, urlPattern); - - // If a URL alias was created and 'generated' is set to true, update the alias. - if (urlAliasEntity?.generated === true) { - urlAliasEntity = await getPluginService('urlAliasService').update(urlAliasEntity.id, { - // @ts-ignore - url_path: generatedPath, - generated: true, - contenttype: uid, - // @ts-ignore - locale: combinedEntity.locale, - localizations: urlAliasLocalizations, - }); - } - - // If no URL alias was created, create one. - if (!urlAliasEntity) { - urlAliasEntity = await getPluginService('urlAliasService').create({ - url_path: generatedPath, - generated: true, - contenttype: uid, - // @ts-ignore - locale: combinedEntity.locale, - // @ts-ignore - localizations: urlAliasLocalizations, - }); - } - })); - - // Update all the URL alias localizations. - await Promise.all(urlAliasLocalizations.map(async (localization) => { - await strapi.db.query('plugin::webtools.url-alias').update({ - where: { - id: localization, - }, - data: { - localizations: [ - ...(urlAliasLocalizations.filter((loc) => loc !== localization)), - urlAliasEntity.id, - ], - }, - }); - })); - - // Eventually update the entity to include the URL alias. - const dataWithUrlAlias = { ...opts.data, url_alias: urlAliasEntity.id }; - const updatedEntity = await service.update.call(this, uid, newEntity.id, { - ...opts, data: dataWithUrlAlias, - }); - - return updatedEntity; - }, - - async update( - uid: Common.UID.ContentType, - entityId: number, - opts: IDecoratedServiceOptions<{ url_alias: number }>, - ) { - const hasWT = isContentTypeEnabled(uid); - let urlAliasEntity: Attribute.GetValues<'plugin::webtools.url-alias', Attribute.GetNonPopulatableKeys<'plugin::webtools.url-alias'>>; - - // If Webtools isn't enabled, do nothing. - if (!hasWT) { - return service.update.call(this, uid, entityId, opts); - } - - // Fetch the URL pattern for this content type. - let relations: string[] = []; - let languages: string[] = [undefined]; - - if (strapi.plugin('i18n')) { - languages = []; - const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); - languages = locales.map((locale) => locale.code); - } - - await Promise.all(languages.map(async (lang) => { - const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); - const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); - - relations = [...relations, ...languageRelations]; - })); - - // Manually fetch the entity that's being updated. - // We do this because not all it's data is present in opts.data. - const entity = await service.update.call(this, uid, entityId, { - ...opts, - populate: { - ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), - url_alias: { - fields: ['id', 'generated'], - }, - localizations: { - populate: { - url_alias: { - fields: ['id'], - }, - }, - }, - }, - }); - - // Fetch the URL alias localizations. - const urlAliasLocalizations = entity.localizations - ?.map((loc) => loc.url_alias?.id) - ?.filter((loc) => loc) || []; - - const entityWithoutLocalizations = { - ...entity, - localizations: undefined, - }; - - // @ts-ignore - if (opts.data.url_alias?.length) { - urlAliasEntity = await getPluginService('urlAliasService').findOne(opts.data.url_alias); - // @ts-ignore - // eslint-disable-next-line max-len - // eslint-disable-next-line ,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-member-access - } else if (entity.url_alias?.length) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - urlAliasEntity = entity.url_alias; - } - - // If a URL alias is present and 'generated' is set to false, do nothing. - if (urlAliasEntity?.generated === false) { - return service.update.call(this, uid, entityId, opts); - } - - // Generate the path. - const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, entity.locale); - await Promise.all(urlPatterns.map(async (urlPattern) => { - const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, entityWithoutLocalizations, urlPattern); - - // @ts-ignore - if (urlAliasEntity?.length) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - await Promise.all(urlAliasEntity.map(async (alias: { generated: boolean; id: ID; }) => { - if (alias.generated === true) { - await getPluginService('urlAliasService').update(alias.id, { - // @ts-ignore - url_path: generatedPath, - generated: true, - contenttype: uid, - // @ts-ignore - locale: entity.locale, - // @ts-ignore - localizations: urlAliasLocalizations, - }); - } - })); - } - - // @ts-ignore - if (!urlAliasEntity?.length) { - console.log('creating new url alias because empty array', urlAliasEntity); - urlAliasEntity = await getPluginService('urlAliasService').create({ - url_path: generatedPath, - generated: true, - contenttype: uid, - // @ts-ignore - locale: entity.locale, - // @ts-ignore - localizations: urlAliasLocalizations, - }); - } - })); - - // Update all the URL alias localizations. - await Promise.all(urlAliasLocalizations.map(async (localization) => { - await strapi.db.query('plugin::webtools.url-alias').update({ - where: { - id: localization, - }, - data: { - localizations: [ - ...(urlAliasLocalizations.filter((loc) => loc !== localization)), - urlAliasEntity.id, - ], - }, - }); - })); - - // Eventually update the entity. - return service.update.call(this, uid, entityId, { - ...opts, - data: { - ...opts.data, - url_alias: urlAliasEntity.id, - }, - }); - }, - - async delete(uid: Common.UID.ContentType, entityId: number) { - const hasWT = isContentTypeEnabled(uid); - - // If Webtools isn't enabled, do nothing. - if (!hasWT) { - return service.delete.call(this, uid, entityId); - } - - // Fetch the entity because we need the url_alias id. - const entity = await service.findOne.call(this, uid, entityId, { - populate: { - url_alias: { - fields: ['id'], - }, - }, - }); - - // If a URL alias is present, delete it. - // @ts-ignore - if (entity.url_alias.length) { - // @ts-ignore - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call - await Promise.all(entity.url_alias.map(async (url_alias: { id: string | number; }) => { - if (url_alias?.id) { - await getPluginService('urlAliasService').delete(url_alias.id); - } - })); - } - - // Eventually delete the entity. - return service.delete.call(this, uid, entityId); - }, - - // eslint-disable-next-line max-len, consistent-return - async clone(uid: Common.UID.ContentType, cloneId: number, params?: IDecoratedServiceOptions<{ url_alias: number }>) { - const hasWT = isContentTypeEnabled(uid); - - if (!hasWT) { - return service.clone.call(this, uid, cloneId, params); - } - - // Fetch the URL pattern for this content type. - let relations: string[] = []; - let languages: string[] = [undefined]; - - if (strapi.plugin('i18n')) { - languages = []; - const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); - languages = locales.map((locale) => locale.code); - } - - await Promise.all(languages.map(async (lang) => { - const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); - const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); - relations = [...relations, ...languageRelations]; - })); - - // Create the cloned entity - const clonedEntity = await service.clone.call(this, uid, cloneId, { - ...params, - populate: { - ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), - url_alias: { - fields: ['id', 'generated'], - }, - localizations: { - populate: { - url_alias: { - fields: ['id'], - }, - }, - }, - }, - }); - - if (!clonedEntity) { - throw new Error('Cloning failed, cloned entity is null or undefined'); - } - - // Fetch the URL alias localizations. - const urlAliasLocalizations = clonedEntity.localizations - ?.map((loc) => loc.url_alias.id) - ?.filter((loc) => loc) || []; - - const clonedEntityWithoutLocalizations = { - ...clonedEntity, - localizations: undefined, - }; - - const combinedEntity = { ...clonedEntityWithoutLocalizations }; - - const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, combinedEntity.locale); - await Promise.all(urlPatterns.map(async (urlPattern) => { - const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, combinedEntity, urlPattern); - // Create a new URL alias for the cloned entity - const newUrlAlias = await getPluginService('urlAliasService').create({ - url_path: generatedPath, - generated: true, - contenttype: uid, - // @ts-ignore - locale: combinedEntity.locale, - // @ts-ignore - localizations: urlAliasLocalizations, - }); - - // Update all the URL alias localizations. - await Promise.all(urlAliasLocalizations.map(async (localization) => { - await strapi.db.query('plugin::webtools.url-alias').update({ - where: { - id: localization, - }, - data: { - localizations: [ - ...(urlAliasLocalizations.filter((loc) => loc !== localization)), - newUrlAlias.id, - ], - }, - }); - })); - - // Update the cloned entity with the new URL alias id - return service.update.call(this, uid, clonedEntity.id, { data: { url_alias: newUrlAlias.id }, populate: ['url_alias'] }); - })); - }, - - async deleteMany(uid: Common.UID.ContentType, params: any) { - const hasWT = isContentTypeEnabled(uid); - if (!hasWT) { - return service.deleteMany.call(this, uid, params); - } - - // Find entities matching the criteria to delete their URL aliases - const entitiesToDelete = await strapi.entityService.findMany(uid, { ...params, fields: ['id'], populate: ['url_alias'] }); - - entitiesToDelete.map(async (entity) => { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (entity.url_alias.length) { - // @ts-ignore - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call - await Promise.all(entity.url_alias.map(async (url_alias: { id: string | number; }) => { - if (url_alias?.id) { - await getPluginService('urlAliasService').delete(url_alias.id); - } - })); - } - }); - - // Delete the entities after URL aliases - return service.deleteMany.call(this, uid, params); - }, - -}); - -export default () => ({ - decorator, -}); diff --git a/packages/core/server/admin-api/services/url-alias.ts b/packages/core/server/admin-api/services/url-alias.ts deleted file mode 100644 index bd43e0fd..00000000 --- a/packages/core/server/admin-api/services/url-alias.ts +++ /dev/null @@ -1,168 +0,0 @@ - - -import { EntityService, Entity, Common } from '@strapi/types'; -import { getPluginService } from '../../util/getPluginService'; - -/** - * Finds a path from the original path that is unique - */ -const duplicateCheck = async ( - originalPath: string, - ignoreId?: Entity.ID, - ext: number = -1, -): Promise => { - const extension = ext >= 0 ? `-${ext}` : ''; - const newPath = originalPath + extension; - const pathAlreadyExists = await getPluginService('urlAliasService').findByPath(newPath, ignoreId); - - if (pathAlreadyExists) { - return duplicateCheck(originalPath, ignoreId, ext + 1); - } - - return newPath; -}; - -/** - * Create. - * - * @param {object} data the data. - * @returns {void} - */ -const create = async (data: EntityService.Params.Pick<'plugin::webtools.url-alias', 'data'>['data']) => { - const urlPath = await duplicateCheck(data.url_path); - - const pathEntity = await strapi.entityService.create('plugin::webtools.url-alias', { - data: { - ...data, - url_path: urlPath, - }, - }); - - return pathEntity; -}; - -/** - * Find related entity. - * - * @param {object} data the data. - * @returns {void} - */ -const findRelatedEntity = async (urlAlias: EntityService.GetValues<'plugin::webtools.url-alias'>, query: EntityService.Params.Pick = {}) => { - const type = urlAlias.contenttype as Common.UID.ContentType; - const entity = await strapi.entityService.findMany(type, { - locale: 'all', - ...query, - filters: { - ...query?.filters, - // @ts-ignore - url_alias: urlAlias.id, - }, - }); - - if (!entity[0]) return null; - - return entity[0]; -}; - -/** - * findOne. - * - * @param {number} id the id. - * @returns {void} - */ -const findOne = async (id: number | string) => { - const pathEntity = await strapi.entityService.findOne('plugin::webtools.url-alias', id); - - return pathEntity; -}; - -/** - * findMany. - * - * @param {boolean} showDrafts wheter to include the drafts. - * @param {object} query the entity service query. - * @returns {void} - */ -const findMany = async (showDrafts: boolean = false, query: EntityService.Params.Pick<'plugin::webtools.url-alias', 'fields' | 'populate' | 'pagination' | 'sort' | 'filters' | '_q' | 'publicationState' | 'plugin'> = {}) => { - const excludeDrafts = false; - - // Check drafAndPublish setting. - if (!showDrafts) { - // TODO: - // Exclude draft URLs. - // We need to check the publication status of the linked entity. - } - - const { results, pagination } = await strapi.entityService.findPage('plugin::webtools.url-alias', { - ...query, - locale: 'all', - filters: { - ...query?.filters, - published_at: excludeDrafts ? { - $notNull: true, - } : {}, - }, - }); - - return { results, pagination }; -}; - -/** - * findByPath. - * - * @param {string} path the path. - * @param {number} id the id to ignore. - */ -const findByPath = async (path: string, id: Entity.ID = 0) => { - const pathEntity = await strapi.entityService.findMany('plugin::webtools.url-alias', { - filters: { - url_path: path, - id: { - $not: id, - }, - }, - limit: 1, - }); - - return pathEntity[0]; -}; - -/** - * Update. - * - * @param {number} id the id. - * @param {object} data the data. - * @returns {void} - */ -const update = async (id: Entity.ID, data: EntityService.Params.Pick<'plugin::webtools.url-alias', 'data'>['data']) => { - const pathEntity = await strapi.entityService.update('plugin::webtools.url-alias', id, { - data: { - ...data, - // url_path: data.url_path[0], - }, - }); - - return pathEntity; -}; - -/** - * Delete. - * - * @param {number} id the id. - * @returns {void} - */ -const deleteUrlAlias = async (id: number | string) => { - if (!id) return; - - await strapi.entityService.delete('plugin::webtools.url-alias', id); -}; - -export default () => ({ - create, - update, - findOne, - findMany, - findByPath, - findRelatedEntity, - delete: deleteUrlAlias, -}); diff --git a/packages/core/server/bootstrap.ts b/packages/core/server/bootstrap.ts new file mode 100644 index 00000000..10442b01 --- /dev/null +++ b/packages/core/server/bootstrap.ts @@ -0,0 +1,67 @@ +import { Core } from '@strapi/strapi'; +import { IStrapi } from './types/strapi'; +import { getPluginService } from './util/getPluginService'; + +export default async ({ strapi }: { strapi: Core.Strapi }) => { + try { + // // Decorate the entity service with review workflow logic + // const { decorator } = getPluginService('queryLayerDecorator'); + // strapi.entityService.decorate(decorator); + + // Register permission actions. + const actions = [ + { + section: 'plugins', + displayName: 'Access the URL alias list', + uid: 'settings.list', + pluginName: 'webtools', + }, + { + section: 'plugins', + displayName: 'Access the URL alias patterns', + uid: 'settings.patterns', + pluginName: 'webtools', + }, + { + section: 'plugins', + displayName: 'Access the URL alias sidebar', + uid: 'edit-view.sidebar', + pluginName: 'webtools', + }, + ]; + + strapi.admin.services.permission.actionProvider.registerMany(actions); + + // Give the public role permissions to access the public API endpoints. + if (strapi.plugin('users-permissions')) { + const roles = await strapi + .service('plugin::users-permissions.role') + .find(); + + const publicId = roles.filter((role) => role.type === 'public')[0]?.id; + + if (publicId) { + const publicRole = await strapi + .service('plugin::users-permissions.role') + .findOne(publicId); + + publicRole.permissions['plugin::webtools'] = { + controllers: { + core: { + router: { enabled: true }, + }, + 'url-alias': { + find: { enabled: true }, + }, + }, + }; + + await strapi + .service('plugin::users-permissions.role') + .updateRole(publicRole.id, publicRole); + } + } + } catch (error) { + strapi.log.error(`Bootstrap failed. ${String(error)}`); + } +}; diff --git a/packages/core/server/admin-api/config.ts b/packages/core/server/config.ts similarity index 100% rename from packages/core/server/admin-api/config.ts rename to packages/core/server/config.ts diff --git a/packages/core/server/content-api/bootstrap.js b/packages/core/server/content-api/bootstrap.js deleted file mode 100644 index f49798df..00000000 --- a/packages/core/server/content-api/bootstrap.js +++ /dev/null @@ -1,35 +0,0 @@ -export default async (strapi) => { - try { - // Give the public role permissions to access the public API endpoints. - if (strapi.plugin('users-permissions')) { - const roles = await strapi - .service('plugin::users-permissions.role') - .find(); - - const publicId = roles.filter((role) => role.type === 'public')[0]?.id; - - if (publicId) { - const publicRole = await strapi - .service('plugin::users-permissions.role') - .findOne(publicId); - - publicRole.permissions['plugin::webtools'] = { - controllers: { - core: { - router: { enabled: true }, - }, - 'url-alias': { - find: { enabled: true }, - }, - }, - }; - - await strapi - .service('plugin::users-permissions.role') - .updateRole(publicRole.id, publicRole); - } - } - } catch (error) { - strapi.log.error(`Bootstrap failed. ${String(error)}`); - } -}; diff --git a/packages/core/server/content-api/controllers/url-alias.ts b/packages/core/server/content-api/controllers/url-alias.ts deleted file mode 100644 index 738d983a..00000000 --- a/packages/core/server/content-api/controllers/url-alias.ts +++ /dev/null @@ -1,10 +0,0 @@ - -import { factories } from '@strapi/strapi'; - -/** - * URL alias controller - */ - -const contentTypeSlug = 'plugin::webtools.url-alias'; - -export default factories.createCoreController(contentTypeSlug); diff --git a/packages/core/server/content-api/routes/core.ts b/packages/core/server/content-api/routes/core.ts deleted file mode 100644 index 9693b531..00000000 --- a/packages/core/server/content-api/routes/core.ts +++ /dev/null @@ -1,12 +0,0 @@ - - -export default [ - { - method: 'GET', - path: '/router', - handler: 'core.router', - config: { - policies: [], - }, - }, -]; diff --git a/packages/core/server/content-api/routes/url-alias.ts b/packages/core/server/content-api/routes/url-alias.ts deleted file mode 100644 index a16b0808..00000000 --- a/packages/core/server/content-api/routes/url-alias.ts +++ /dev/null @@ -1,12 +0,0 @@ - - -export default [ - { - method: 'GET', - path: '/url-alias', - handler: 'url-alias.find', - config: { - policies: [], - }, - }, -]; diff --git a/packages/core/server/content-api/services/by-path.ts b/packages/core/server/content-api/services/by-path.ts deleted file mode 100644 index 50963156..00000000 --- a/packages/core/server/content-api/services/by-path.ts +++ /dev/null @@ -1,62 +0,0 @@ - - -import get from 'lodash/get'; -import { Common, EntityService } from '@strapi/strapi'; -import { getPluginService } from '../../util/getPluginService'; - -export default () => ({ - /** - * Get an entity by it's path. - * - * @param {string} path the path. - * @param {object} query the entity service query. - * @returns {object} the entity. - */ - byPath: async (path: string, query: EntityService.Params.Pick = {}) => { - let excludeDrafts = false; - - const urlAliasEntity = await getPluginService('urlAliasService').findByPath(path); - if (!urlAliasEntity) { - return {}; - } - - const contentTypeUid = urlAliasEntity.contenttype as Common.UID.ContentType; - - // Check drafAndPublish setting. - const contentType = strapi.contentTypes[contentTypeUid]; - if (get(contentType, ['options', 'draftAndPublish'], false)) { - excludeDrafts = true; - } - - const entities = await strapi.entityService.findMany(contentTypeUid, { - ...query, - filters: { - ...query?.filters, - // @ts-ignore - url_alias: urlAliasEntity.id, - published_at: excludeDrafts ? { - $notNull: true, - } : {}, - }, - locale: 'all', - limit: 1, - }); - - /** - * If we're querying a single type, which does not have localizations enabled, - * Strapi will return a single entity instead of an array. Which is slightly weird, - * because the API we're querying is called `findMany`. That's why we need to check - * if the result is an array or not and handle it accordingly. - */ - const entity = Array.isArray(entities) ? entities[0] : entities; - - if (!entity) { - return {}; - } - - return { - entity, - contentType: urlAliasEntity.contenttype as Common.UID.ContentType, - }; - }, -}); diff --git a/packages/core/server/content-api/services/url-alias.ts b/packages/core/server/content-api/services/url-alias.ts deleted file mode 100644 index d758f90d..00000000 --- a/packages/core/server/content-api/services/url-alias.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { factories } from '@strapi/strapi'; - -/** - * URL alias service - */ - -const contentTypeSlug = 'plugin::webtools.url-alias'; - -export default factories.createCoreService(contentTypeSlug); diff --git a/packages/core/server/content-types/index.ts b/packages/core/server/content-types/index.ts new file mode 100644 index 00000000..6dee8e65 --- /dev/null +++ b/packages/core/server/content-types/index.ts @@ -0,0 +1,11 @@ +import urlAliasSchema from './url-alias/schema.json'; +import urlPatternSchema from './url-pattern/schema.json'; + +export default { + 'url-alias': { + schema: urlAliasSchema, + }, + 'url-pattern': { + schema: urlPatternSchema, + }, +}; diff --git a/packages/core/server/admin-api/content-types/url-alias/schema.json b/packages/core/server/content-types/url-alias/schema.json similarity index 100% rename from packages/core/server/admin-api/content-types/url-alias/schema.json rename to packages/core/server/content-types/url-alias/schema.json diff --git a/packages/core/server/admin-api/content-types/url-pattern/schema.json b/packages/core/server/content-types/url-pattern/schema.json similarity index 100% rename from packages/core/server/admin-api/content-types/url-pattern/schema.json rename to packages/core/server/content-types/url-pattern/schema.json diff --git a/packages/core/server/content-api/__tests__/core.test.ts b/packages/core/server/controllers/__tests__/core.test.ts similarity index 100% rename from packages/core/server/content-api/__tests__/core.test.ts rename to packages/core/server/controllers/__tests__/core.test.ts diff --git a/packages/core/server/content-api/controllers/core.ts b/packages/core/server/controllers/core.ts similarity index 88% rename from packages/core/server/content-api/controllers/core.ts rename to packages/core/server/controllers/core.ts index 6e84d149..caff18ac 100644 --- a/packages/core/server/content-api/controllers/core.ts +++ b/packages/core/server/controllers/core.ts @@ -1,8 +1,8 @@ import { Context } from 'koa'; -import { getPluginService } from '../../util/getPluginService'; -import { sanitizeOutput } from '../../util/sanitizeOutput'; +import { getPluginService } from '../util/getPluginService'; +import { sanitizeOutput } from '../util/sanitizeOutput'; /** * Router controller diff --git a/packages/core/server/controllers/index.ts b/packages/core/server/controllers/index.ts new file mode 100644 index 00000000..7e77af6a --- /dev/null +++ b/packages/core/server/controllers/index.ts @@ -0,0 +1,11 @@ +import urlAliasController from './url-alias'; +import urlPatternController from './url-pattern'; +import infoController from './info'; +import coreController from './core'; + +export default { + 'url-alias': urlAliasController, + 'url-pattern': urlPatternController, + info: infoController, + core: coreController, +}; diff --git a/packages/core/server/admin-api/controllers/info.ts b/packages/core/server/controllers/info.ts similarity index 97% rename from packages/core/server/admin-api/controllers/info.ts rename to packages/core/server/controllers/info.ts index f3da4e0c..52ceb6dd 100644 --- a/packages/core/server/admin-api/controllers/info.ts +++ b/packages/core/server/controllers/info.ts @@ -3,8 +3,7 @@ import get from 'lodash/get'; import { Schema } from '@strapi/strapi'; import { Context } from 'koa'; - -import getAddons from '../../util/getAddons'; +import getAddons from '../util/getAddons'; /** * Info controller diff --git a/packages/core/server/admin-api/controllers/url-alias.ts b/packages/core/server/controllers/url-alias.ts similarity index 51% rename from packages/core/server/admin-api/controllers/url-alias.ts rename to packages/core/server/controllers/url-alias.ts index a509086b..1f614f35 100644 --- a/packages/core/server/admin-api/controllers/url-alias.ts +++ b/packages/core/server/controllers/url-alias.ts @@ -1,49 +1,19 @@ - +import { factories } from '@strapi/strapi'; import { Context } from 'koa'; -import { EntityService } from '@strapi/strapi'; import { errors } from '@strapi/utils'; -import { getPluginService } from '../../util/getPluginService'; -import { KoaContext } from '../../types/koa'; -import { GenerateParams } from '../services/bulk-generate'; - +import { getPluginService } from '../util/getPluginService'; +import { KoaContext } from '../types/koa'; +import { GenerateParams } from '../admin-api/services/bulk-generate'; /** - * Path controller + * URL alias controller */ -export default { - findOne: async (ctx: Context & { params: { id: number } }) => { - const { id } = ctx.params; - const pathEntity = await getPluginService('urlAliasService').findOne(id); - ctx.body = pathEntity; - }, - findMany: async (ctx: Context) => { - const pathEntities = await getPluginService('urlAliasService').findMany(true, ctx.query); - ctx.body = pathEntities; - }, - delete: async (ctx: Context & { params: { id: number } }) => { - const { id } = ctx.params; - await getPluginService('urlAliasService').delete(id); - ctx.body = { succes: true }; - }, - update: async (ctx: KoaContext> & { params: { id: number } }) => { - const { id } = ctx.params; - const { data } = ctx.request.body; - const patternEntity = await getPluginService('urlAliasService').update( - id, - data, - ); - ctx.body = patternEntity; - }, - create: async (ctx: KoaContext>) => { - const { data } = ctx.request.body; - const patternEntity = await getPluginService('urlAliasService').create( - data, - ); - ctx.body = patternEntity; - }, +const contentTypeSlug = 'plugin::webtools.url-alias'; + +export default factories.createCoreController(contentTypeSlug, ({ strapi }) => ({ editLink: async (ctx: Context) => { const { path } = ctx.query; const { entity, contentType } = await getPluginService('byPathService').byPath(path as string); @@ -87,4 +57,4 @@ export default { message: `Successfully generated ${generatedCount} URL alias${generatedCount > 1 ? 'es' : ''}.`, }; }, -}; +})); diff --git a/packages/core/server/controllers/url-pattern.ts b/packages/core/server/controllers/url-pattern.ts new file mode 100644 index 00000000..5b44afbb --- /dev/null +++ b/packages/core/server/controllers/url-pattern.ts @@ -0,0 +1,55 @@ + + +import get from 'lodash/get'; +import { Context } from 'koa'; +import { factories, Schema, UID } from '@strapi/strapi'; +import { KoaContext } from '../types/koa'; +import { getPluginService } from '../util/getPluginService'; + +/** + * URL pattern controller + */ + +const contentTypeSlug = 'plugin::webtools.url-pattern'; + +export default factories.createCoreController(contentTypeSlug, ({ strapi }) => ({ + allowedFields: (ctx: Context) => { + const formattedFields = {}; + + Object.values(strapi.contentTypes).forEach((contentType: Schema.ContentType) => { + const { pluginOptions } = contentType; + + // Not for CTs that are not visible in the content manager. + const isInContentManager = get(pluginOptions, [ + 'content-manager', + 'visible', + ]) as boolean; + if (isInContentManager === false) return; + + const fields = getPluginService('urlPatternService').getAllowedFields( + contentType, + ['pluralName', 'string', 'uid', 'id'], + ); + formattedFields[contentType.uid] = fields; + }); + + ctx.body = formattedFields; + }, + + validatePattern: (ctx: KoaContext<{ pattern: string, modelName: UID.ContentType }>) => { + const urlPatternService = getPluginService('urlPatternService'); + const { pattern, modelName } = ctx.request.body; + + const contentType = strapi.contentTypes[modelName]; + + const fields = urlPatternService.getAllowedFields(contentType, [ + 'pluralName', + 'string', + 'uid', + 'id', + ]); + const validated = urlPatternService.validatePattern(pattern, fields); + + ctx.body = validated; + }, +})); diff --git a/packages/core/server/admin-api/hooks/__tests__/disable.test.ts b/packages/core/server/hooks/__tests__/disable.test.ts similarity index 100% rename from packages/core/server/admin-api/hooks/__tests__/disable.test.ts rename to packages/core/server/hooks/__tests__/disable.test.ts diff --git a/packages/core/server/admin-api/hooks/disable.ts b/packages/core/server/hooks/disable.ts similarity index 78% rename from packages/core/server/admin-api/hooks/disable.ts rename to packages/core/server/hooks/disable.ts index b402bb12..c7470a98 100644 --- a/packages/core/server/admin-api/hooks/disable.ts +++ b/packages/core/server/hooks/disable.ts @@ -1,15 +1,15 @@ -import { Schema, Shared } from '@strapi/strapi'; -import { isContentTypeEnabled } from '../../util/enabledContentTypes'; -import { pluginId } from '../../util/pluginId'; +import { Schema } from '@strapi/strapi'; +import { isContentTypeEnabled } from '../util/enabledContentTypes'; +import { pluginId } from '../util/pluginId'; export const disableContentType = async ({ oldContentTypes, contentTypes }: { oldContentTypes?: null | { - [uid in keyof Shared.ContentTypes]?: Shared.ContentTypes[uid]; + [uid in keyof Schema.ContentTypes]?: Schema.ContentTypes[uid]; } & { [uid: string]: Schema.ContentType; }, contentTypes: { - [uid in keyof Shared.ContentTypes]: Shared.ContentTypes[uid]; + [uid in keyof Schema.ContentTypes]: Schema.ContentTypes[uid]; } & { [uid: string]: Schema.ContentType; }, diff --git a/packages/core/server/index.ts b/packages/core/server/index.ts index 7e6855b9..4cdb13ab 100644 --- a/packages/core/server/index.ts +++ b/packages/core/server/index.ts @@ -1,81 +1,18 @@ - -// Has to be imported once for build -import { } from '@strapi/strapi'; -// Admin API -import adminApiRegister from './admin-api/register'; -import adminApiBootstrap from './admin-api/bootstrap'; -import adminApiConfig from './admin-api/config'; -import adminApiUrlAliasSchema from './admin-api/content-types/url-alias/schema.json'; -import adminApiUrlPatternSchema from './admin-api/content-types/url-pattern/schema.json'; -import adminApiUrlAliasController from './admin-api/controllers/url-alias'; -import adminApiUrlPatternController from './admin-api/controllers/url-pattern'; -import adminApiInfoController from './admin-api/controllers/info'; -import adminApiUrlAliasService from './admin-api/services/url-alias'; -import adminApiUrlPatternService from './admin-api/services/url-pattern'; -import adminApiBulkGenerateService from './admin-api/services/bulk-generate'; -import adminApiUrlAliasRoutes from './admin-api/routes/url-alias'; -import adminApiUrlPatternRoutes from './admin-api/routes/url-pattern'; -import adminApiInfoRoutes from './admin-api/routes/info'; -import queryLayerDecoratorService from './admin-api/services/query-layer-decorator'; - -// Content API -import contentApiBootstrap from './content-api/bootstrap'; -import contentApiUrlAliasController from './content-api/controllers/url-alias'; -import contentApiCoreController from './content-api/controllers/core'; -import contentApiByPathService from './content-api/services/by-path'; -import contentApiUrlAliasRoutes from './content-api/routes/url-alias'; -import contentApiUrlAliasService from './content-api/services/url-alias'; -import contentApiCoreRoutes from './content-api/routes/core'; -import { IStrapi } from './types/strapi'; +import register from './register'; +import bootstrap from './bootstrap'; +import routes from './routes'; +import controllers from './controllers'; +import services from './services'; +import config from './config'; +import contentTypes from './content-types'; export default { - register: ({ strapi }: { strapi: IStrapi }) => { - adminApiRegister(strapi); - }, - bootstrap: async ({ strapi }: { strapi: IStrapi }) => { - adminApiBootstrap(strapi); - await contentApiBootstrap(strapi); - }, - config: adminApiConfig, - contentTypes: { - 'url-alias': { - schema: adminApiUrlAliasSchema, - }, - 'url-pattern': { - schema: adminApiUrlPatternSchema, - }, - }, - routes: { - admin: { - type: 'admin', - routes: [ - ...adminApiUrlAliasRoutes, - ...adminApiUrlPatternRoutes, - ...adminApiInfoRoutes, - ], - }, - 'content-api': { - type: 'content-api', - routes: [ - ...contentApiUrlAliasRoutes, - ...contentApiCoreRoutes, - ], - }, - }, - controllers: { - 'url-alias': contentApiUrlAliasController, - 'url-alias-admin': adminApiUrlAliasController, - 'url-pattern': adminApiUrlPatternController, - info: adminApiInfoController, - core: contentApiCoreController, - }, - services: { - 'url-alias': contentApiUrlAliasService, - urlAliasService: adminApiUrlAliasService, - urlPatternService: adminApiUrlPatternService, - byPathService: contentApiByPathService, - queryLayerDecorator: queryLayerDecoratorService, - bulkGenerate: adminApiBulkGenerateService, - }, + register, + bootstrap, + config, + contentTypes, + routes, + controllers, + services, }; diff --git a/packages/core/server/admin-api/register.ts b/packages/core/server/register.ts similarity index 74% rename from packages/core/server/admin-api/register.ts rename to packages/core/server/register.ts index 43b49bf1..f6cb5643 100644 --- a/packages/core/server/admin-api/register.ts +++ b/packages/core/server/register.ts @@ -2,17 +2,13 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import set from 'lodash/set'; -import { Schema } from '@strapi/strapi'; -import { IStrapi } from '../types/strapi'; -import { isContentTypeEnabled } from '../util/enabledContentTypes'; -import migratePluginOptionsRename from './migrations/plugin-options-rename'; +import { Core, Schema } from '@strapi/strapi'; +import { IStrapi } from './types/strapi'; +import { isContentTypeEnabled } from './util/enabledContentTypes'; import { disableContentType } from './hooks/disable'; -export default (strapi: IStrapi) => { - // Migrate the pluginOptions to reflect the plugin rename. - migratePluginOptionsRename(strapi); - - strapi.hook('strapi::content-types.beforeSync').register(disableContentType); +export default ({ strapi }: { strapi: Core.Strapi }) => { + // strapi.hook('strapi::content-types.beforeSync').register(disableContentType); // Register the url_alias field. Object.values(strapi.contentTypes).forEach((contentType: Schema.ContentType) => { diff --git a/packages/core/server/routes/index.ts b/packages/core/server/routes/index.ts new file mode 100644 index 00000000..a60f4e48 --- /dev/null +++ b/packages/core/server/routes/index.ts @@ -0,0 +1,183 @@ +export default { + 'content-api': { + type: 'content-api', + routes: [ + { + method: 'GET', + path: '/url-alias', + handler: 'url-alias.find', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/router', + handler: 'core.router', + config: { + policies: [], + }, + }, + ], + }, + admin: { + type: 'admin', + routes: [ + /** + * URL Alias routes + */ + { + method: 'GET', + path: '/url-alias/findOne/:id', + handler: 'url-alias.findOne', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/url-alias/findMany', + handler: 'url-alias.find', + config: { + policies: [], + }, + }, + { + method: 'POST', + path: '/url-alias/delete/:id', + handler: 'url-alias.delete', + config: { + policies: [], + }, + }, + { + method: 'PUT', + path: '/url-alias/update/:id', + handler: 'url-alias.update', + config: { + policies: [], + }, + }, + { + method: 'POST', + path: '/url-alias/create', + handler: 'url-alias.create', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/url-alias/editLink', + handler: 'url-alias.editLink', + config: { + policies: [], + }, + }, + { + method: 'POST', + path: '/url-alias/generate', + handler: 'url-alias.generate', + config: { + policies: [], + }, + }, + + /** + * Info routes + */ + { + method: 'GET', + path: '/info/getContentTypes', + handler: 'info.getContentTypes', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/info/getLanguages', + handler: 'info.getLanguages', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/info/addons', + handler: 'info.getAddons', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/info/config', + handler: 'info.getConfig', + config: { + policies: [], + }, + }, + + /** + * URL Pattern routes + */ + { + method: 'GET', + path: '/url-pattern/findOne/:id', + handler: 'url-pattern.findOne', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/url-pattern/findMany', + handler: 'url-pattern.find', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/url-pattern/delete/:id', + handler: 'url-pattern.delete', + config: { + policies: [], + }, + }, + { + method: 'PUT', + path: '/url-pattern/update/:id', + handler: 'url-pattern.update', + config: { + policies: [], + }, + }, + { + method: 'POST', + path: '/url-pattern/create', + handler: 'url-pattern.create', + config: { + policies: [], + }, + }, + { + method: 'GET', + path: '/url-pattern/allowed-fields', + handler: 'url-pattern.allowedFields', + config: { + policies: [], + }, + }, + { + method: 'POST', + path: '/url-pattern/validate', + handler: 'url-pattern.validatePattern', + config: { + policies: [], + }, + }, + ], + }, +}; diff --git a/packages/core/server/admin-api/__tests__/query-layer-decorator.test.ts b/packages/core/server/services/__tests__/query-layer-decorator.test.ts similarity index 100% rename from packages/core/server/admin-api/__tests__/query-layer-decorator.test.ts rename to packages/core/server/services/__tests__/query-layer-decorator.test.ts diff --git a/packages/core/server/admin-api/services/bulk-generate.ts b/packages/core/server/services/bulk-generate.ts similarity index 93% rename from packages/core/server/admin-api/services/bulk-generate.ts rename to packages/core/server/services/bulk-generate.ts index d92844b7..b2352f5f 100644 --- a/packages/core/server/admin-api/services/bulk-generate.ts +++ b/packages/core/server/services/bulk-generate.ts @@ -1,10 +1,11 @@ -import { Common } from '@strapi/types'; -import { getPluginService } from '../../util/getPluginService'; -import { GenerationType } from '../../types'; +import { UID } from '@strapi/strapi'; + +import { getPluginService } from '../util/getPluginService'; +import { GenerationType } from '../types'; export interface GenerateParams { - types: Common.UID.ContentType[], - generationType: GenerationType + types: UID.ContentType[], + generationType: GenerationType, } /** @@ -13,12 +14,11 @@ export interface GenerateParams { * @returns {void} */ const createLanguageLinksForUrlAliases = async () => { - const urlAliases = await getPluginService('urlAliasService').findMany(true, { - // @ts-ignore + const urlAliases = await strapi.documents('plugin::webtools.url-alias').findMany({ fields: ['id', 'contenttype', 'locale'], }); - await Promise.all(urlAliases.results.map(async (urlAlias) => { + await Promise.all(urlAliases.map(async (urlAlias) => { const relatedEntity = await getPluginService('urlAliasService').findRelatedEntity(urlAlias, { fields: [], populate: { diff --git a/packages/core/server/services/index.ts b/packages/core/server/services/index.ts new file mode 100644 index 00000000..294e42a4 --- /dev/null +++ b/packages/core/server/services/index.ts @@ -0,0 +1,11 @@ +import urlAliasController from './url-alias'; +import urlPatternController from './url-pattern'; +import bulkGenerateController from './bulk-generate'; +import queryLayerDecorator from './query-layer-decorator'; + +export default { + 'url-alias': urlAliasController, + 'url-pattern': urlPatternController, + 'bulk-generate': bulkGenerateController, + 'query-layer-decorator': queryLayerDecorator, +}; diff --git a/packages/core/server/services/query-layer-decorator.ts b/packages/core/server/services/query-layer-decorator.ts new file mode 100644 index 00000000..66e10141 --- /dev/null +++ b/packages/core/server/services/query-layer-decorator.ts @@ -0,0 +1,436 @@ +// import { Attribute, Common } from '@strapi/types'; +// import { ID } from '@strapi/types/dist/types/core/entity'; +// import { IDecoratedService, IDecoratedServiceOptions } from '../../types/strapi'; +// import { isContentTypeEnabled } from '../../util/enabledContentTypes'; +// import { getPluginService } from '../../util/getPluginService'; + +// /** +// * Decorates the entity service with WT business logic +// * @param {object} service - entity service +// */ +// const decorator = (service: IDecoratedService) => ({ +// async create(uid: Common.UID.ContentType, opts: IDecoratedServiceOptions<{ url_alias: number }>) { +// const hasWT = isContentTypeEnabled(uid); +// let urlAliasEntity: Attribute.GetValues<'plugin::webtools.url-alias', Attribute.GetNonPopulatableKeys<'plugin::webtools.url-alias'>>; + +// // If Webtools isn't enabled, do nothing. +// if (!hasWT) { +// return service.create.call(this, uid, opts); +// } + +// // Fetch the URL pattern for this content type. +// let relations: string[] = []; +// let languages: string[] = [undefined]; + +// if (strapi.plugin('i18n')) { +// languages = []; +// const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); +// languages = locales.map((locale) => locale.code); +// } + +// await Promise.all(languages.map(async (lang) => { +// const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); +// const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); + +// relations = [...relations, ...languageRelations]; +// })); + +// // If a URL alias was created, fetch it. +// if (opts.data.url_alias) { +// urlAliasEntity = await getPluginService('urlAliasService').findOne(opts.data.url_alias); +// } + +// // If a URL alias was created and 'generated' is set to false, do nothing. +// if (urlAliasEntity?.generated === false) { +// return service.create.call(this, uid, opts); +// } + +// // Ideally here we would create the URL alias an directly fire +// // the `service.create.call` function with the new URL alias id. +// // Though it is possible that the `id` field is used in the URL. +// // In that case we have to create the entity first. Then when we know +// // the id, can we create the URL alias entity and can we update +// // the previously created entity. +// const newEntity = await service.create.call(this, uid, { +// ...opts, +// data: opts.data, +// populate: { +// ...opts.populate, +// ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), +// localizations: { +// populate: { +// url_alias: { +// fields: ['id'], +// }, +// }, +// }, +// }, +// }); + +// // Fetch the URL alias localizations. +// const urlAliasLocalizations = newEntity.localizations +// ?.map((loc) => loc.url_alias.id) +// ?.filter((loc) => loc) || []; + +// const newEntityWithoutLocalizations = { +// ...newEntity, +// localizations: undefined, +// }; + +// const combinedEntity = { ...newEntityWithoutLocalizations }; +// const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, combinedEntity.locale); + +// await Promise.all(urlPatterns.map(async (urlPattern) => { +// const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, combinedEntity, urlPattern); + +// // If a URL alias was created and 'generated' is set to true, update the alias. +// if (urlAliasEntity?.generated === true) { +// urlAliasEntity = await getPluginService('urlAliasService').update(urlAliasEntity.id, { +// // @ts-ignore +// url_path: generatedPath, +// generated: true, +// contenttype: uid, +// // @ts-ignore +// locale: combinedEntity.locale, +// localizations: urlAliasLocalizations, +// }); +// } + +// // If no URL alias was created, create one. +// if (!urlAliasEntity) { +// urlAliasEntity = await getPluginService('urlAliasService').create({ +// url_path: generatedPath, +// generated: true, +// contenttype: uid, +// // @ts-ignore +// locale: combinedEntity.locale, +// // @ts-ignore +// localizations: urlAliasLocalizations, +// }); +// } +// })); + +// // Update all the URL alias localizations. +// await Promise.all(urlAliasLocalizations.map(async (localization) => { +// await strapi.db.query('plugin::webtools.url-alias').update({ +// where: { +// id: localization, +// }, +// data: { +// localizations: [ +// ...(urlAliasLocalizations.filter((loc) => loc !== localization)), +// urlAliasEntity.id, +// ], +// }, +// }); +// })); + +// // Eventually update the entity to include the URL alias. +// const dataWithUrlAlias = { ...opts.data, url_alias: urlAliasEntity.id }; +// const updatedEntity = await service.update.call(this, uid, newEntity.id, { +// ...opts, data: dataWithUrlAlias, +// }); + +// return updatedEntity; +// }, + +// async update( +// uid: Common.UID.ContentType, +// entityId: number, +// opts: IDecoratedServiceOptions<{ url_alias: number }>, +// ) { +// const hasWT = isContentTypeEnabled(uid); +// let urlAliasEntity: Attribute.GetValues<'plugin::webtools.url-alias', Attribute.GetNonPopulatableKeys<'plugin::webtools.url-alias'>>; + +// // If Webtools isn't enabled, do nothing. +// if (!hasWT) { +// return service.update.call(this, uid, entityId, opts); +// } + +// // Fetch the URL pattern for this content type. +// let relations: string[] = []; +// let languages: string[] = [undefined]; + +// if (strapi.plugin('i18n')) { +// languages = []; +// const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); +// languages = locales.map((locale) => locale.code); +// } + +// await Promise.all(languages.map(async (lang) => { +// const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); +// const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); + +// relations = [...relations, ...languageRelations]; +// })); + +// // Manually fetch the entity that's being updated. +// // We do this because not all it's data is present in opts.data. +// const entity = await service.update.call(this, uid, entityId, { +// ...opts, +// populate: { +// ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), +// url_alias: { +// fields: ['id', 'generated'], +// }, +// localizations: { +// populate: { +// url_alias: { +// fields: ['id'], +// }, +// }, +// }, +// }, +// }); + +// // Fetch the URL alias localizations. +// const urlAliasLocalizations = entity.localizations +// ?.map((loc) => loc.url_alias?.id) +// ?.filter((loc) => loc) || []; + +// const entityWithoutLocalizations = { +// ...entity, +// localizations: undefined, +// }; + +// // @ts-ignore +// if (opts.data.url_alias?.length) { +// urlAliasEntity = await getPluginService('urlAliasService').findOne(opts.data.url_alias); +// // @ts-ignore +// // eslint-disable-next-line max-len +// // eslint-disable-next-line ,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-member-access +// } else if (entity.url_alias?.length) { +// // @ts-ignore +// // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +// urlAliasEntity = entity.url_alias; +// } + +// // If a URL alias is present and 'generated' is set to false, do nothing. +// if (urlAliasEntity?.generated === false) { +// return service.update.call(this, uid, entityId, opts); +// } + +// // Generate the path. +// const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, entity.locale); +// await Promise.all(urlPatterns.map(async (urlPattern) => { +// const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, entityWithoutLocalizations, urlPattern); + +// // @ts-ignore +// if (urlAliasEntity?.length) { +// // @ts-ignore +// // eslint-disable-next-line @typescript-eslint/no-unsafe-call +// await Promise.all(urlAliasEntity.map(async (alias: { generated: boolean; id: ID; }) => { +// if (alias.generated === true) { +// await getPluginService('urlAliasService').update(alias.id, { +// // @ts-ignore +// url_path: generatedPath, +// generated: true, +// contenttype: uid, +// // @ts-ignore +// locale: entity.locale, +// // @ts-ignore +// localizations: urlAliasLocalizations, +// }); +// } +// })); +// } + +// // @ts-ignore +// if (!urlAliasEntity?.length) { +// console.log('creating new url alias because empty array', urlAliasEntity); +// urlAliasEntity = await getPluginService('urlAliasService').create({ +// url_path: generatedPath, +// generated: true, +// contenttype: uid, +// // @ts-ignore +// locale: entity.locale, +// // @ts-ignore +// localizations: urlAliasLocalizations, +// }); +// } +// })); + +// // Update all the URL alias localizations. +// await Promise.all(urlAliasLocalizations.map(async (localization) => { +// await strapi.db.query('plugin::webtools.url-alias').update({ +// where: { +// id: localization, +// }, +// data: { +// localizations: [ +// ...(urlAliasLocalizations.filter((loc) => loc !== localization)), +// urlAliasEntity.id, +// ], +// }, +// }); +// })); + +// // Eventually update the entity. +// return service.update.call(this, uid, entityId, { +// ...opts, +// data: { +// ...opts.data, +// url_alias: urlAliasEntity.id, +// }, +// }); +// }, + +// async delete(uid: Common.UID.ContentType, entityId: number) { +// const hasWT = isContentTypeEnabled(uid); + +// // If Webtools isn't enabled, do nothing. +// if (!hasWT) { +// return service.delete.call(this, uid, entityId); +// } + +// // Fetch the entity because we need the url_alias id. +// const entity = await service.findOne.call(this, uid, entityId, { +// populate: { +// url_alias: { +// fields: ['id'], +// }, +// }, +// }); + +// // If a URL alias is present, delete it. +// // @ts-ignore +// if (entity.url_alias.length) { +// // @ts-ignore +// // eslint-disable-next-line max-len +// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call +// await Promise.all(entity.url_alias.map(async (url_alias: { id: string | number; }) => { +// if (url_alias?.id) { +// await getPluginService('urlAliasService').delete(url_alias.id); +// } +// })); +// } + +// // Eventually delete the entity. +// return service.delete.call(this, uid, entityId); +// }, + +// // eslint-disable-next-line max-len, consistent-return +// async clone(uid: Common.UID.ContentType, cloneId: number, params?: IDecoratedServiceOptions<{ url_alias: number }>) { +// const hasWT = isContentTypeEnabled(uid); + +// if (!hasWT) { +// return service.clone.call(this, uid, cloneId, params); +// } + +// // Fetch the URL pattern for this content type. +// let relations: string[] = []; +// let languages: string[] = [undefined]; + +// if (strapi.plugin('i18n')) { +// languages = []; +// const locales = await strapi.entityService.findMany('plugin::i18n.locale', {}); +// languages = locales.map((locale) => locale.code); +// } + +// await Promise.all(languages.map(async (lang) => { +// const urlPattern = await getPluginService('urlPatternService').findByUid(uid, lang); +// const languageRelations = getPluginService('urlPatternService').getRelationsFromPattern(urlPattern); +// relations = [...relations, ...languageRelations]; +// })); + +// // Create the cloned entity +// const clonedEntity = await service.clone.call(this, uid, cloneId, { +// ...params, +// populate: { +// ...relations.reduce((obj, key) => ({ ...obj, [key]: {} }), {}), +// url_alias: { +// fields: ['id', 'generated'], +// }, +// localizations: { +// populate: { +// url_alias: { +// fields: ['id'], +// }, +// }, +// }, +// }, +// }); + +// if (!clonedEntity) { +// throw new Error('Cloning failed, cloned entity is null or undefined'); +// } + +// // Fetch the URL alias localizations. +// const urlAliasLocalizations = clonedEntity.localizations +// ?.map((loc) => loc.url_alias.id) +// ?.filter((loc) => loc) || []; + +// const clonedEntityWithoutLocalizations = { +// ...clonedEntity, +// localizations: undefined, +// }; + +// const combinedEntity = { ...clonedEntityWithoutLocalizations }; + +// const urlPatterns = await getPluginService('urlPatternService').findByUid(uid, combinedEntity.locale); +// await Promise.all(urlPatterns.map(async (urlPattern) => { +// const generatedPath = getPluginService('urlPatternService').resolvePattern(uid, combinedEntity, urlPattern); +// // Create a new URL alias for the cloned entity +// const newUrlAlias = await getPluginService('urlAliasService').create({ +// url_path: generatedPath, +// generated: true, +// contenttype: uid, +// // @ts-ignore +// locale: combinedEntity.locale, +// // @ts-ignore +// localizations: urlAliasLocalizations, +// }); + +// // Update all the URL alias localizations. +// await Promise.all(urlAliasLocalizations.map(async (localization) => { +// await strapi.db.query('plugin::webtools.url-alias').update({ +// where: { +// id: localization, +// }, +// data: { +// localizations: [ +// ...(urlAliasLocalizations.filter((loc) => loc !== localization)), +// newUrlAlias.id, +// ], +// }, +// }); +// })); + +// // Update the cloned entity with the new URL alias id +// return service.update.call(this, uid, clonedEntity.id, { data: { url_alias: newUrlAlias.id }, populate: ['url_alias'] }); +// })); +// }, + +// async deleteMany(uid: Common.UID.ContentType, params: any) { +// const hasWT = isContentTypeEnabled(uid); +// if (!hasWT) { +// return service.deleteMany.call(this, uid, params); +// } + +// // Find entities matching the criteria to delete their URL aliases +// const entitiesToDelete = await strapi.entityService.findMany(uid, { ...params, fields: ['id'], populate: ['url_alias'] }); + +// entitiesToDelete.map(async (entity) => { +// // @ts-ignore +// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +// if (entity.url_alias.length) { +// // @ts-ignore +// // eslint-disable-next-line max-len +// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call +// await Promise.all(entity.url_alias.map(async (url_alias: { id: string | number; }) => { +// if (url_alias?.id) { +// await getPluginService('urlAliasService').delete(url_alias.id); +// } +// })); +// } +// }); + +// // Delete the entities after URL aliases +// return service.deleteMany.call(this, uid, params); +// }, + +// }); + +export default () => ({ + // decorator, +}); diff --git a/packages/core/server/services/url-alias.ts b/packages/core/server/services/url-alias.ts new file mode 100644 index 00000000..7506bfc0 --- /dev/null +++ b/packages/core/server/services/url-alias.ts @@ -0,0 +1,98 @@ +import { factories, UID } from '@strapi/strapi'; +import { get } from 'lodash'; +import { getPluginService } from '../util/getPluginService'; + +/** + * URL alias service + */ + +const contentTypeSlug = 'plugin::webtools.url-alias'; + +/** + * Finds a path from the original path that is unique + */ +const duplicateCheck = async ( + originalPath: string, + ignoreId?: number, + ext: number = -1, +): Promise => { + const extension = ext >= 0 ? `-${ext}` : ''; + const newPath = originalPath + extension; + const pathAlreadyExists = await getPluginService('urlAliasService').findByPath(newPath, ignoreId); + + if (pathAlreadyExists) { + return duplicateCheck(originalPath, ignoreId, ext + 1); + } + + return newPath; +}; + +export default factories.createCoreService(contentTypeSlug, ({ strapi }) => ({ + findRelatedEntity: async (path: string, query: EntityService.Params.Pick = {}) => { + let excludeDrafts = false; + + const urlAliasEntity = await getPluginService('urlAliasService').findByPath(path); + if (!urlAliasEntity) { + return {}; + } + + const contentTypeUid = urlAliasEntity.contenttype as Common.UID.ContentType; + + // Check drafAndPublish setting. + const contentType = strapi.contentTypes[contentTypeUid]; + if (get(contentType, ['options', 'draftAndPublish'], false)) { + excludeDrafts = true; + } + + const entities = await strapi.entityService.findMany(contentTypeUid, { + ...query, + filters: { + ...query?.filters, + // @ts-ignore + url_alias: urlAliasEntity.id, + published_at: excludeDrafts ? { + $notNull: true, + } : {}, + }, + locale: 'all', + limit: 1, + }); + + /** + * If we're querying a single type, which does not have localizations enabled, + * Strapi will return a single entity instead of an array. Which is slightly weird, + * because the API we're querying is called `findMany`. That's why we need to check + * if the result is an array or not and handle it accordingly. + */ + const entity = Array.isArray(entities) ? entities[0] : entities; + + if (!entity) { + return {}; + } + + return { + entity, + contentType: urlAliasEntity.contenttype as UID.ContentType, + }; + }, + + /** + * findByPath. + * + * @param {string} path the path. + * @param {number} id the id to ignore. + */ + findByPath: async (path: string, id: Entity.ID = 0) => { + const pathEntity = await strapi.entityService.findMany('plugin::webtools.url-alias', { + filters: { + url_path: path, + id: { + $not: id, + }, + }, + limit: 1, + }); + + return pathEntity[0]; + }, +})); diff --git a/packages/core/server/admin-api/services/url-pattern.ts b/packages/core/server/services/url-pattern.ts similarity index 79% rename from packages/core/server/admin-api/services/url-pattern.ts rename to packages/core/server/services/url-pattern.ts index d95aa27b..55cd951d 100644 --- a/packages/core/server/admin-api/services/url-pattern.ts +++ b/packages/core/server/services/url-pattern.ts @@ -1,47 +1,34 @@ +import { factories, Schema, UID } from '@strapi/strapi'; import snakeCase from 'lodash/snakeCase'; import deburr from 'lodash/deburr'; import toLower from 'lodash/toLower'; import kebabCase from 'lodash/kebabCase'; -import { EntityService, Schema } from '@strapi/strapi'; -import { Common } from '@strapi/types'; +import { getPluginService } from '../util/getPluginService'; -import { getPluginService } from '../../util/getPluginService'; +const contentTypeSlug = 'plugin::webtools.url-pattern'; +export default factories.createCoreService(contentTypeSlug, ({ strapi }) => ({ + // /** + // * Create a new URL pattern. + // * + // * @param {object} data - The data to create the URL pattern with. + // * @returns {Promise} The created URL pattern entity. + // */ + // create: async (data: EntityService.Params.Pick<'plugin::webtools.url-pattern', 'data'>['data']) => { + // const formattedData = data; -export default () => ({ - /** - * Create a new URL pattern. - * - * @param {object} data - The data to create the URL pattern with. - * @returns {Promise} The created URL pattern entity. - */ - create: async (data: EntityService.Params.Pick<'plugin::webtools.url-pattern', 'data'>['data']) => { - const formattedData = data; - - if (data.code) { - formattedData.code = snakeCase(deburr(toLower(data.code))); - } else { - formattedData.code = snakeCase(deburr(toLower(data.label))); - } + // if (data.code) { + // formattedData.code = snakeCase(deburr(toLower(data.code))); + // } else { + // formattedData.code = snakeCase(deburr(toLower(data.label))); + // } - const patternEntity = await strapi.entityService.create('plugin::webtools.url-pattern', { - data, - }); + // const patternEntity = await strapi.entityService.create('plugin::webtools.url-pattern', { + // data, + // }); - return patternEntity; - }, - - /** - * Find one URL pattern by its ID. - * - * @param {number} id - The ID of the URL pattern. - * @returns {Promise} The found URL pattern entity. - */ - findOne: async (id: number) => { - const patternEntity = await strapi.entityService.findOne('plugin::webtools.url-pattern', id); - - return patternEntity; - }, + // return patternEntity; + // }, /** * Find URL patterns by UID and optionally language code. @@ -70,41 +57,6 @@ export default () => ({ return patternsArray; }, - /** - * Find many URL patterns based on given parameters. - * - * @param {object} params - The parameters for finding URL patterns. - * @returns {Promise} The found URL patterns. - */ - findMany: async (params: string) => { - const patternEntities = await strapi.entityService.findMany('plugin::webtools.url-pattern', params); - - return patternEntities; - }, - - /** - * Update a URL pattern by its ID. - * - * @param {number} id - The ID of the URL pattern to update. - * @param {object} data - The new data for the URL pattern. - * @returns {Promise} The updated URL pattern entity. - */ - update: async (id: number, data: EntityService.Params.Pick<'plugin::webtools.url-pattern', 'data'>['data']) => { - const patternEntity = await strapi.entityService.update('plugin::webtools.url-pattern', id, { data }); - - return patternEntity; - }, - - /** - * Delete a URL pattern by its ID. - * - * @param {number} id - The ID of the URL pattern to delete. - * @returns {Promise} - */ - delete: async (id: number) => { - await strapi.entityService.delete('plugin::webtools.url-pattern', id); - }, - /** * Get all field names allowed in the URL of a given content type. * @@ -225,7 +177,7 @@ export default () => ({ * @returns {string} The resolved path. */ resolvePattern: ( - uid: Common.UID.ContentType, + uid: UID.ContentType, entity: { [key: string]: string | number | Date }, urlPattern?: string, ): string => { @@ -328,4 +280,4 @@ export default () => ({ message: 'Valid pattern', }; }, -}); +}));