Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
2 changes: 2 additions & 0 deletions packages/core/server/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
94 changes: 94 additions & 0 deletions packages/core/server/controllers/search.ts
Original file line number Diff line number Diff line change
@@ -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] } : {}),
};
},
};
22 changes: 22 additions & 0 deletions packages/core/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
},
],
},
};
26 changes: 26 additions & 0 deletions packages/core/server/services/get-main-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { UID } from '@strapi/strapi';

interface ContentManagerConfig {
settings?: {
mainField?: string;
};
}

export const getMainField = async (uid: UID.ContentType): Promise<string | null> => {
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,
});
14 changes: 8 additions & 6 deletions packages/core/server/services/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};