diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index a4ef65eb..94acfaa2 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -14,7 +14,7 @@ const badge = (text: string, bgColor: ChalkFunction, textColor: ChalkFunction = const textIndent = ( text: string | string[], indentFirst = true, - indent: number = MAX_PREFIX_LENGTH + 2 + indent: number = MAX_PREFIX_LENGTH + 2, ) => { const parts = Array.isArray(text) ? text : [text]; diff --git a/packages/core/server/controllers/index.ts b/packages/core/server/controllers/index.ts index 7e77af6a..70702ca1 100644 --- a/packages/core/server/controllers/index.ts +++ b/packages/core/server/controllers/index.ts @@ -2,10 +2,12 @@ import urlAliasController from './url-alias'; import urlPatternController from './url-pattern'; import infoController from './info'; import coreController from './core'; +import searchController from './search'; export default { 'url-alias': urlAliasController, 'url-pattern': urlPatternController, info: infoController, core: coreController, + search: searchController, }; diff --git a/packages/core/server/controllers/search.ts b/packages/core/server/controllers/search.ts new file mode 100644 index 00000000..c48e6b7a --- /dev/null +++ b/packages/core/server/controllers/search.ts @@ -0,0 +1,94 @@ +import { Context } from 'koa'; +import { UID, Schema } from '@strapi/strapi'; +import { errors } from '@strapi/utils'; +import { isContentTypeEnabled } from '../util/enabledContentTypes'; +import { getPluginService } from '../util/getPluginService'; + +interface DocumentEntry { + id: number; + documentId: string; + [key: string]: unknown; // Dynamic fields like title, etc. +} + +interface SearchResult extends DocumentEntry { + contentType: string; +} + +/** + * Search controller + */ +export default { + search: async (ctx: Context & { params: { id: number } }) => { + const { q } = ctx.query; + const results: SearchResult[] = []; + + const qStr = typeof q === 'string' ? q.trim() : ''; + if (!qStr) { + throw new errors.ValidationError('Missing or invalid query parameter "?q=" (must be a non-empty string)'); + } + + await Promise.all( + Object.entries(strapi.contentTypes).map( + async ([uid, config]: [UID.ContentType, Schema.ContentType]) => { + const hasWT = isContentTypeEnabled(config); + if (!hasWT) return; + + const mainField = await getPluginService('get-main-field').getMainField(uid); + if (!mainField) return; + + const fieldsArr: string[] = ['documentId', ...(mainField ? [mainField] : [])]; + const entries = (await strapi.documents(uid).findMany({ + filters: { + [mainField]: { $containsi: qStr }, + }, + // @ts-expect-error + fields: fieldsArr, + })); + + if (!entries || entries.length === 0) return; + + const entriesWithContentType: SearchResult[] = entries.map((entry: DocumentEntry) => ({ + ...entry, + contentType: uid, + })); + + results.push(...entriesWithContentType); + }, + ), + ); + + ctx.body = results; + }, + reverseSearch: async (ctx: Context & { params: { contentType: string; documentId: string } }) => { + const { contentType, documentId } = ctx.params; + + if (typeof contentType !== 'string' || !(contentType in strapi.contentTypes)) { + throw new errors.ValidationError(`Unknown or invalid content type: ${contentType}`); + } + + const mainField = await getPluginService('get-main-field').getMainField( + contentType as UID.ContentType, + ); + // eslint-disable-next-line max-len + const fieldsArr: string[] = ['id', 'documentId', ...(mainField ? [mainField] : [])]; + + const entry = (await strapi + .documents(contentType as UID.ContentType) + .findOne({ + documentId, + // @ts-expect-error + fields: fieldsArr, + })); + + if (!entry) { + throw new errors.NotFoundError('Entry not found'); + } + + ctx.body = { + id: entry.id, + documentId: entry.documentId, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ...(mainField ? { [mainField]: entry[mainField] } : {}), + }; + }, +}; diff --git a/packages/core/server/routes/index.ts b/packages/core/server/routes/index.ts index 8811d691..780db7b4 100644 --- a/packages/core/server/routes/index.ts +++ b/packages/core/server/routes/index.ts @@ -186,6 +186,28 @@ export default { policies: [], }, }, + /** + * Search routes + */ + { + method: 'GET', + path: '/search', + handler: 'search.search', + config: { + policies: [], + }, + }, + /** + * Reverse Search routes for a title or slug + */ + { + method: 'GET', + path: '/search/reverse/:contentType/:documentId', + handler: 'search.reverseSearch', + config: { + policies: [], + }, + }, ], }, }; diff --git a/packages/core/server/services/get-main-field.ts b/packages/core/server/services/get-main-field.ts new file mode 100644 index 00000000..d5e43a17 --- /dev/null +++ b/packages/core/server/services/get-main-field.ts @@ -0,0 +1,26 @@ +import { UID } from '@strapi/strapi'; + +interface ContentManagerConfig { + settings?: { + mainField?: string; + }; +} + +export const getMainField = async (uid: UID.ContentType): Promise => { + const coreStoreSettings = (await strapi + // TODO use documents service instead of any + .query('strapi::core-store' as UID.Schema) + .findMany({ + where: { key: `plugin_content_manager_configuration_content_types::${uid}` }, + })) as Array<{ value: string }>; + + if (!coreStoreSettings?.[0]) return null; + + const value = JSON.parse(coreStoreSettings[0].value) as ContentManagerConfig; + + return value?.settings?.mainField ?? null; +}; + +export default () => ({ + getMainField, +}); diff --git a/packages/core/server/services/index.ts b/packages/core/server/services/index.ts index e9aebf1d..f7776871 100644 --- a/packages/core/server/services/index.ts +++ b/packages/core/server/services/index.ts @@ -1,9 +1,11 @@ -import urlAliasController from './url-alias'; -import urlPatternController from './url-pattern'; -import bulkGenerateController from './bulk-generate'; +import urlAliasService from './url-alias'; +import urlPatternService from './url-pattern'; +import bulkGenerateService from './bulk-generate'; +import getMainFieldService from './get-main-field'; export default { - 'url-alias': urlAliasController, - 'url-pattern': urlPatternController, - 'bulk-generate': bulkGenerateController, + 'url-alias': urlAliasService, + 'url-pattern': urlPatternService, + 'bulk-generate': bulkGenerateService, + 'get-main-field': getMainFieldService, };