@@ -341,6 +341,8 @@ import {
341341} from './discovery-routes.js';
342342// [#23050 slice 8] The email route's body moved to its own module — see its header.
343343import { registerEmailEndpoints, type EmailRouteHost } from './email-routes.js';
344+ // [#23050 slice 9] The global search route's body moved to its own module — see its header.
345+ import { registerSearchEndpoints, type SearchRouteHost } from './search-routes.js';
344346import { loadExcelJs, type Worksheet } from './xlsx-module.js';
345347// [#18386] `?template=true` on the export door: the import template's column
346348// rule, request reading and workbook. See the module header.
@@ -8995,71 +8997,17 @@ export class RestServer {
89958997 * GET {basePath}/search?q=acme&objects=lead,account&limit=20&perObject=5
89968998 */
89978999 private registerSearchEndpoints(basePath: string): void {
8998- const isScoped = basePath.includes('/environments/:environmentId');
8999- this.routeManager.register({
9000- method: 'GET',
9001- path: `${basePath}/search`,
9002- handler: async (req: any, res: any) => {
9003- try {
9004- const environmentId = isScoped ? req.params?.environmentId : undefined;
9005- const p = await this.resolveProtocol(environmentId, req);
9006- const context = await this.resolveExecCtx(environmentId, req);
9007- if (this.enforceAuth(req, res, context)) return;
9008- const searchAll = (p as any).searchAll;
9009- if (typeof searchAll !== 'function') {
9010- res.status(501).json({ code: 'NOT_IMPLEMENTED', message: 'Search not supported by this protocol' });
9011- return;
9012- }
9013- // [#6877] `String(['a','b'])` searched for the literal
9014- // term `'a,b'`. `objects` is NOT listed: the next line reads
9015- // its array arm deliberately — a cross-object search over a
9016- // LIST of objects is the whole point of the parameter.
9017- // [#7606] Recognition before arity, same order and same
9018- // envelope as the export route — see `query-allowlist.ts`.
9019- // A dropped `?objects=` is the widening case at its worst:
9020- // the term fans out across every searchable object while the
9021- // caller believes they scoped it to one.
9022- if (refuseUnknownQueryParams(req, res, GLOBAL_SEARCH_PARAMS)) return;
9023- if (refuseRepeatedQueryParams(req, res, ['q', 'query', 'limit', 'perObject'])) return;
9024- const q = String(req.query?.q ?? req.query?.query ?? '');
9025- const objectsParam = req.query?.objects;
9026- const objects = typeof objectsParam === 'string'
9027- ? objectsParam.split(',').map((s: string) => s.trim()).filter(Boolean)
9028- : Array.isArray(objectsParam) ? objectsParam : undefined;
9029- // [#20062] Read, not coerced: `?limit=abc` used to reach
9030- // `searchAll` as `NaN`, and `hits.length >= NaN` is never true,
9031- // so the overall cap was silently gone. No request schema is
9032- // declared for this door, so the reading is "a whole number";
9033- // `searchAll`'s own `[1, 100]` clamp is unchanged, and an empty
9034- // `?limit=` stays absent as the old falsy guard had it.
9035- const limit = readDeclaredQueryNumber(req.query, 'limit',
9036- UNDECLARED_WHOLE_NUMBER_PARAM, { emptyIsAbsent: true });
9037- // [#20139] `perObject`, the same way: `?perObject=abc` reached
9038- // `searchAll` as `NaN`, its `Math.max(1, Math.min(25, NaN))` is
9039- // `NaN`, and the per-object cap was silently gone. Whole number;
9040- // the `[1, 25]` clamp stays `searchAll`'s own, and an empty
9041- // `?perObject=` stays absent as the old falsy guard had it.
9042- const perObject = readDeclaredQueryNumber(req.query, 'perObject',
9043- UNDECLARED_WHOLE_NUMBER_PARAM, { emptyIsAbsent: true });
9044- const result = await searchAll.call(p, {
9045- q,
9046- objects,
9047- limit,
9048- perObject,
9049- ...(context ? { context } : {}),
9050- });
9051- res.json(result);
9052- } catch (error: any) {
9053- const mapped = mapDataError(error);
9054- logUnexpectedRouteError(error, mapped);
9055- res.status(mapped.status).json(mapped.body);
9056- }
9057- },
9058- metadata: {
9059- summary: 'Global cross-object search',
9060- tags: ['search'],
9061- },
9062- });
9000+ // [#23050 slice 9] The body moved to `search-routes.ts`. This literal
9001+ // hands it every member it reads — the list IS its dependency on this
9002+ // class (see `SearchRouteHost`, and that module's header for why no
9003+ // member is a getter).
9004+ const host: SearchRouteHost = {
9005+ routeManager: this.routeManager,
9006+ resolveProtocol: (...a) => this.resolveProtocol(...a),
9007+ resolveExecCtx: (...a) => this.resolveExecCtx(...a),
9008+ enforceAuth: (...a) => this.enforceAuth(...a),
9009+ };
9010+ registerSearchEndpoints.call(host, basePath);
90639011 }
90649012
90659013 /**
0 commit comments