@vicacha-devs/strapi-plugin-global-search 0.0.0-stage → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -2
  3. package/dist/admin/index.d.ts +2 -0
  4. package/dist/admin/index.js +2310 -0
  5. package/dist/admin/index.js.map +7 -0
  6. package/dist/admin/index.mjs +2310 -0
  7. package/dist/admin/index.mjs.map +7 -0
  8. package/dist/admin/translations/ar.json +89 -0
  9. package/dist/admin/translations/en.json +89 -0
  10. package/dist/admin/translations/es.json +89 -0
  11. package/dist/admin/translations/fr.json +89 -0
  12. package/dist/admin/translations/tr.json +89 -0
  13. package/dist/server/bootstrap.d.ts +6 -0
  14. package/dist/server/bootstrap.d.ts.map +1 -0
  15. package/dist/server/config/index.d.ts +15 -0
  16. package/dist/server/config/index.d.ts.map +1 -0
  17. package/dist/server/controllers/index.d.ts +94 -0
  18. package/dist/server/controllers/index.d.ts.map +1 -0
  19. package/dist/server/controllers/schema.d.ts +24 -0
  20. package/dist/server/controllers/schema.d.ts.map +1 -0
  21. package/dist/server/controllers/search.d.ts +21 -0
  22. package/dist/server/controllers/search.d.ts.map +1 -0
  23. package/dist/server/controllers/settings.d.ts +23 -0
  24. package/dist/server/controllers/settings.d.ts.map +1 -0
  25. package/dist/server/index.d.ts +212 -0
  26. package/dist/server/index.d.ts.map +1 -0
  27. package/dist/server/index.js +1186 -0
  28. package/dist/server/index.js.map +7 -0
  29. package/dist/server/index.mjs +1186 -0
  30. package/dist/server/index.mjs.map +7 -0
  31. package/dist/server/register.d.ts +6 -0
  32. package/dist/server/register.d.ts.map +1 -0
  33. package/dist/server/routes/index.d.ts +20 -0
  34. package/dist/server/routes/index.d.ts.map +1 -0
  35. package/dist/server/services/index.d.ts +89 -0
  36. package/dist/server/services/index.d.ts.map +1 -0
  37. package/dist/server/services/permissions.d.ts +13 -0
  38. package/dist/server/services/permissions.d.ts.map +1 -0
  39. package/dist/server/services/query.d.ts +25 -0
  40. package/dist/server/services/query.d.ts.map +1 -0
  41. package/dist/server/services/ranking.d.ts +27 -0
  42. package/dist/server/services/ranking.d.ts.map +1 -0
  43. package/dist/server/services/schema.d.ts +70 -0
  44. package/dist/server/services/schema.d.ts.map +1 -0
  45. package/dist/server/services/search.d.ts +36 -0
  46. package/dist/server/services/search.d.ts.map +1 -0
  47. package/dist/server/services/settings.d.ts +15 -0
  48. package/dist/server/services/settings.d.ts.map +1 -0
  49. package/dist/server/types.d.ts +195 -0
  50. package/dist/server/types.d.ts.map +1 -0
  51. package/dist/server/utils/async.d.ts +8 -0
  52. package/dist/server/utils/async.d.ts.map +1 -0
  53. package/dist/server/utils/constants.d.ts +42 -0
  54. package/dist/server/utils/constants.d.ts.map +1 -0
  55. package/dist/server/utils/text.d.ts +20 -0
  56. package/dist/server/utils/text.d.ts.map +1 -0
  57. package/package.json +79 -3
@@ -0,0 +1,1186 @@
1
+ // server/utils/constants.ts
2
+ var PLUGIN_ID = "global-search";
3
+ var SEARCHABLE_SCALAR_TYPES = [
4
+ "string",
5
+ "text",
6
+ "richtext",
7
+ "customField",
8
+ "uid",
9
+ "email",
10
+ "enumeration"
11
+ ];
12
+ var SYSTEM_PLUGIN_PREFIXES = [
13
+ "plugin::upload.",
14
+ "plugin::i18n.",
15
+ "plugin::users-permissions.",
16
+ "plugin::content-releases.",
17
+ "plugin::review-workflows.",
18
+ "plugin::navigation.",
19
+ "plugin::config-sync.",
20
+ "plugin::email-designer-5.",
21
+ "plugin::publisher.",
22
+ "plugin::seo.",
23
+ "plugin::tiptap-editor.",
24
+ "plugin::global-search."
25
+ ];
26
+ var SKIPPED_ATTRIBUTE_TYPES = [
27
+ "dynamiczone",
28
+ "blocks",
29
+ "json",
30
+ "relation",
31
+ "media",
32
+ "password"
33
+ ];
34
+ var BLOCKLISTED_ATTRIBUTE_NAMES = [
35
+ "password",
36
+ "resetPasswordToken",
37
+ "registrationToken",
38
+ "confirmationToken"
39
+ ];
40
+ var IDENTIFIER_FIELDS = [
41
+ "uid",
42
+ "slug",
43
+ "code",
44
+ "key",
45
+ "referenceCode",
46
+ "bookingCode",
47
+ "certificateNumber",
48
+ "transactionReference"
49
+ ];
50
+ var MAIN_FIELD_FALLBACKS = [
51
+ "name",
52
+ "title",
53
+ "label",
54
+ "displayName",
55
+ "heading",
56
+ "heroTitle",
57
+ "question",
58
+ "term",
59
+ "subject",
60
+ "fullName",
61
+ "applicantName",
62
+ "customerName",
63
+ "donorName",
64
+ "referenceCode",
65
+ "bookingCode",
66
+ "certificateNumber",
67
+ "slug"
68
+ ];
69
+ var TIER = {
70
+ ID_EXACT: 0,
71
+ ID_CONTAINS: 1,
72
+ MAIN_EXACT: 2,
73
+ MAIN_STARTS_WITH: 3,
74
+ MAIN_CONTAINS: 4,
75
+ FIELD_CONTAINS: 5,
76
+ NESTED_CONTAINS: 6,
77
+ UNKNOWN: 7
78
+ };
79
+ var TIER_LABELS = {
80
+ 0: "id:exact",
81
+ 1: "id:partial",
82
+ 2: "name:exact",
83
+ 3: "name:starts-with",
84
+ 4: "name:partial",
85
+ 5: "field",
86
+ 6: "nested-field",
87
+ 7: "other"
88
+ };
89
+ var MAX_QUERY_LENGTH = 200;
90
+ var SNIPPET_RADIUS = 45;
91
+
92
+ // server/bootstrap.ts
93
+ var bootstrap_default = async ({ strapi }) => {
94
+ try {
95
+ const types = await strapi.plugin(PLUGIN_ID).service("schema").getSearchableContentTypes();
96
+ const uidList = types.map((type) => type.uid).join(", ");
97
+ strapi.log.debug(
98
+ `[${PLUGIN_ID}] indexed ${types.length} content type(s): ${uidList}`
99
+ );
100
+ } catch (error) {
101
+ const msg = error.message;
102
+ strapi.log.error(
103
+ `[${PLUGIN_ID}] failed to introspect content types: ${msg}`
104
+ );
105
+ }
106
+ };
107
+
108
+ // server/config/index.ts
109
+ var defaults = {
110
+ /** Minimum number of characters before a search is executed. */
111
+ minChars: 2,
112
+ /** Client-side debounce, in ms. Served to the admin so both UIs stay in sync. */
113
+ debounce: 250,
114
+ /** Maximum rows fetched per content type, per search. */
115
+ perTypeLimit: 20,
116
+ /** Hard cap on the merged, ranked result set. */
117
+ maxResults: 100,
118
+ /** Search inside nested component fields. */
119
+ deep: true,
120
+ /** How many component levels to walk when collecting searchable paths. */
121
+ maxDepth: 3,
122
+ /** Include unpublished entries (draft & publish types). */
123
+ includeDrafts: true,
124
+ /** Include Content-Manager-visible content types registered by plugins (plugin::*). */
125
+ includePluginContentTypes: true,
126
+ /** Per-content-type query timeout, in ms. */
127
+ queryTimeout: 4e3,
128
+ /** Maximum number of content types queried in parallel. */
129
+ concurrency: 8,
130
+ /** Content type UIDs never searched. */
131
+ excludedContentTypes: [],
132
+ /** Field paths never searched, as `uid:path` (e.g. `api::article.article:body`). */
133
+ excludedFields: [],
134
+ /** Up to two extra fields shown next to the main field, keyed by content type UID. */
135
+ displayFields: {}
136
+ };
137
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
138
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
139
+ var validator = (config2 = {}) => {
140
+ if (!isPlainObject(config2)) {
141
+ throw new Error("[global-search] config must be a plain object");
142
+ }
143
+ const numbers = {
144
+ minChars: [1, 10],
145
+ debounce: [0, 5e3],
146
+ perTypeLimit: [1, 200],
147
+ maxResults: [1, 1e3],
148
+ maxDepth: [1, 6],
149
+ queryTimeout: [500, 6e4],
150
+ concurrency: [1, 32]
151
+ };
152
+ Object.entries(numbers).forEach(([key, [min, max]]) => {
153
+ if (config2[key] === void 0) return;
154
+ const value = config2[key];
155
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
156
+ throw new Error(
157
+ `[global-search] config.${key} must be an integer between ${min} and ${max}`
158
+ );
159
+ }
160
+ });
161
+ ["deep", "includeDrafts", "includePluginContentTypes"].forEach(
162
+ (key) => {
163
+ if (config2[key] !== void 0 && typeof config2[key] !== "boolean") {
164
+ throw new Error(`[global-search] config.${key} must be a boolean`);
165
+ }
166
+ }
167
+ );
168
+ ["excludedContentTypes", "excludedFields"].forEach((key) => {
169
+ if (config2[key] !== void 0 && !isStringArray(config2[key])) {
170
+ throw new Error(
171
+ `[global-search] config.${key} must be an array of strings`
172
+ );
173
+ }
174
+ });
175
+ if (config2.displayFields !== void 0) {
176
+ const validMap = isPlainObject(config2.displayFields) && Object.values(config2.displayFields).every(isStringArray);
177
+ if (!validMap) {
178
+ throw new Error(
179
+ "[global-search] config.displayFields must map content type UIDs to string arrays"
180
+ );
181
+ }
182
+ }
183
+ };
184
+ var config = {
185
+ default: defaults,
186
+ validator,
187
+ defaults
188
+ };
189
+ var config_default = config;
190
+
191
+ // server/controllers/schema.ts
192
+ var collectAllFields = (type) => {
193
+ const main = type.mainField && type.mainField !== "id" && type.mainFieldSearchable !== false ? [type.mainField] : [];
194
+ return Array.from(
195
+ /* @__PURE__ */ new Set([
196
+ ...type.idFields,
197
+ ...main,
198
+ ...type.topLevelFields,
199
+ ...type.nestedFields
200
+ ])
201
+ );
202
+ };
203
+ var toSummary = (type) => ({
204
+ uid: type.uid,
205
+ kind: type.kind,
206
+ displayName: type.displayName,
207
+ mainField: type.mainField,
208
+ localized: type.localized,
209
+ draftAndPublish: type.draftAndPublish,
210
+ fieldCount: collectAllFields(type).length,
211
+ searchable: type.searchable
212
+ });
213
+ var schema_default = ({ strapi }) => ({
214
+ /** Powers the content-type filter chips on the search page. */
215
+ async find(ctx) {
216
+ const types = await strapi.plugin(PLUGIN_ID).service("schema").getSearchableContentTypes();
217
+ const readable = strapi.plugin(PLUGIN_ID).service("permissions").filterReadableTypes(types, ctx.state.userAbility);
218
+ const settings = await strapi.plugin(PLUGIN_ID).service("settings").get();
219
+ ctx.body = {
220
+ contentTypes: readable.map(toSummary),
221
+ settings: { minChars: settings.minChars, debounce: settings.debounce }
222
+ };
223
+ },
224
+ /** Full picture, including excluded and unsearchable types, for Settings. */
225
+ async findAll(ctx) {
226
+ const types = await strapi.plugin(PLUGIN_ID).service("schema").getAllContentTypes();
227
+ const settings = await strapi.plugin(PLUGIN_ID).service("settings").get();
228
+ ctx.body = {
229
+ contentTypes: types.map((type) => ({
230
+ ...toSummary(type),
231
+ excluded: (settings.excludedContentTypes || []).includes(type.uid),
232
+ fields: collectAllFields(type)
233
+ }))
234
+ };
235
+ }
236
+ });
237
+
238
+ // server/controllers/search.ts
239
+ var toArray = (value) => {
240
+ if (!value) return [];
241
+ if (Array.isArray(value)) {
242
+ return value.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
243
+ }
244
+ if (typeof value === "object") {
245
+ return Object.values(value).flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
246
+ }
247
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
248
+ };
249
+ var search_default = ({ strapi }) => ({
250
+ async find(ctx) {
251
+ const { q, locale, page, pageSize } = ctx.request.query;
252
+ const rawTypes = ctx.request.query.types ?? ctx.request.query["types[]"];
253
+ try {
254
+ ctx.body = await strapi.plugin(PLUGIN_ID).service("search").search({
255
+ query: typeof q === "string" ? q : void 0,
256
+ types: toArray(rawTypes),
257
+ locale: typeof locale === "string" ? locale : void 0,
258
+ page: page !== void 0 ? Number(page) : void 0,
259
+ pageSize: pageSize !== void 0 ? Number(pageSize) : void 0,
260
+ userAbility: ctx.state.userAbility
261
+ });
262
+ } catch (error) {
263
+ const detail = error.stack || error.message;
264
+ strapi.log.error(`[${PLUGIN_ID}] search request failed: ${detail}`);
265
+ ctx.throw(
266
+ 500,
267
+ "Global search failed. Check the server logs for details."
268
+ );
269
+ }
270
+ }
271
+ });
272
+
273
+ // server/controllers/settings.ts
274
+ var settings_default = ({ strapi }) => ({
275
+ async find(ctx) {
276
+ ctx.body = await strapi.plugin(PLUGIN_ID).service("settings").get();
277
+ },
278
+ async update(ctx) {
279
+ try {
280
+ ctx.body = await strapi.plugin(PLUGIN_ID).service("settings").set(ctx.request.body || {});
281
+ } catch (error) {
282
+ ctx.throw(400, error.message);
283
+ }
284
+ },
285
+ async reset(ctx) {
286
+ ctx.body = await strapi.plugin(PLUGIN_ID).service("settings").reset();
287
+ }
288
+ });
289
+
290
+ // server/controllers/index.ts
291
+ var controllers_default = {
292
+ search: search_default,
293
+ schema: schema_default,
294
+ settings: settings_default
295
+ };
296
+
297
+ // server/register.ts
298
+ var RBAC_ACTIONS = [
299
+ {
300
+ section: "plugins",
301
+ displayName: "Search content",
302
+ uid: "read",
303
+ pluginName: PLUGIN_ID
304
+ },
305
+ {
306
+ section: "settings",
307
+ category: "global search",
308
+ subCategory: "general",
309
+ displayName: "Configure global search",
310
+ uid: "settings",
311
+ pluginName: PLUGIN_ID
312
+ }
313
+ ];
314
+ var register_default = ({ strapi }) => {
315
+ strapi.admin?.services?.permission?.actionProvider?.registerMany(
316
+ RBAC_ACTIONS
317
+ );
318
+ };
319
+
320
+ // server/routes/index.ts
321
+ var withPermission = (action) => ({
322
+ policies: [
323
+ {
324
+ name: "admin::hasPermissions",
325
+ config: { actions: [`plugin::${PLUGIN_ID}.${action}`] }
326
+ }
327
+ ]
328
+ });
329
+ var routes_default = {
330
+ admin: {
331
+ type: "admin",
332
+ routes: [
333
+ {
334
+ method: "GET",
335
+ path: "/search",
336
+ handler: "search.find",
337
+ config: withPermission("read")
338
+ },
339
+ {
340
+ method: "GET",
341
+ path: "/schema",
342
+ handler: "schema.find",
343
+ config: withPermission("read")
344
+ },
345
+ {
346
+ method: "GET",
347
+ path: "/schema/all",
348
+ handler: "schema.findAll",
349
+ config: withPermission("settings")
350
+ },
351
+ {
352
+ method: "GET",
353
+ path: "/settings",
354
+ handler: "settings.find",
355
+ config: withPermission("settings")
356
+ },
357
+ {
358
+ method: "PUT",
359
+ path: "/settings",
360
+ handler: "settings.update",
361
+ config: withPermission("settings")
362
+ },
363
+ {
364
+ method: "POST",
365
+ path: "/settings/reset",
366
+ handler: "settings.reset",
367
+ config: withPermission("settings")
368
+ }
369
+ ]
370
+ }
371
+ };
372
+
373
+ // server/services/permissions.ts
374
+ var CM_READ_ACTION = "plugin::content-manager.explorer.read";
375
+ var permissions_default = () => ({
376
+ /**
377
+ * An editor restricted to Articles must not discover Orders through search, so
378
+ * candidate types are intersected with what the caller can read in the Content
379
+ * Manager before any query runs.
380
+ */
381
+ filterReadableTypes(types, userAbility) {
382
+ if (!userAbility || typeof userAbility.can !== "function") return types;
383
+ return types.filter((type) => {
384
+ try {
385
+ return userAbility.can(CM_READ_ACTION, type.uid);
386
+ } catch {
387
+ return false;
388
+ }
389
+ });
390
+ }
391
+ });
392
+
393
+ // server/services/query.ts
394
+ var nest = (path, condition) => path.split(".").reduceRight(
395
+ (acc, segment) => ({ [segment]: acc }),
396
+ condition
397
+ );
398
+ var buildPopulate = (paths) => {
399
+ const tree = {};
400
+ paths.forEach((path) => {
401
+ const segments = path.split(".");
402
+ segments.pop();
403
+ let node = tree;
404
+ segments.forEach((segment) => {
405
+ node[segment] = node[segment] || {};
406
+ node = node[segment];
407
+ });
408
+ });
409
+ const toPopulate = (node) => Object.entries(node).reduce(
410
+ (acc, [key, child]) => {
411
+ const childObj = child;
412
+ acc[key] = Object.keys(childObj).length ? { populate: toPopulate(childObj) } : true;
413
+ return acc;
414
+ },
415
+ {}
416
+ );
417
+ return Object.keys(tree).length ? toPopulate(tree) : void 0;
418
+ };
419
+ var MAX_PG_INT32 = 2147483647;
420
+ var query_default = () => ({
421
+ nest,
422
+ buildPopulate,
423
+ /** All field paths a hit could have matched, in ranking order. */
424
+ searchablePaths(descriptor) {
425
+ const main = descriptor.mainField && descriptor.mainField !== "id" && descriptor.mainFieldSearchable !== false ? [descriptor.mainField] : [];
426
+ return Array.from(
427
+ /* @__PURE__ */ new Set([
428
+ ...descriptor.idFields,
429
+ ...main,
430
+ ...descriptor.topLevelFields,
431
+ ...descriptor.nestedFields
432
+ ])
433
+ );
434
+ },
435
+ /**
436
+ * One `$or` over every searchable path. `id` is numeric, so it is only added —
437
+ * as an equality — when the query is a plain integer within 32-bit Postgres INT
438
+ * bounds; `$containsi` on an integer column is rejected by Postgres.
439
+ * `documentId` is matched as a string unless excluded.
440
+ */
441
+ buildFilters(descriptor, query) {
442
+ const paths = this.searchablePaths(descriptor);
443
+ const conditions = [];
444
+ if (paths.includes("documentId")) {
445
+ conditions.push({ documentId: { $containsi: query } });
446
+ }
447
+ if (/^\d+$/.test(query)) {
448
+ const numericId = Number(query);
449
+ if (Number.isSafeInteger(numericId) && numericId >= 1 && numericId <= MAX_PG_INT32) {
450
+ conditions.push({ id: { $eq: numericId } });
451
+ }
452
+ }
453
+ paths.forEach((path) => {
454
+ if (path === "documentId") return;
455
+ conditions.push(nest(path, { $containsi: query }));
456
+ });
457
+ return conditions.length ? { $or: conditions } : null;
458
+ },
459
+ /** Bounded projection — never `populate: '*'`, never a full row. */
460
+ buildParams(descriptor, {
461
+ query,
462
+ locale,
463
+ limit,
464
+ includeDrafts
465
+ }) {
466
+ const filters = this.buildFilters(descriptor, query);
467
+ if (!filters) return null;
468
+ const fields = Array.from(
469
+ new Set(
470
+ [
471
+ "documentId",
472
+ ...descriptor.idFields,
473
+ descriptor.mainField,
474
+ ...descriptor.topLevelFields,
475
+ ...descriptor.displayFields || [],
476
+ "createdAt",
477
+ "updatedAt",
478
+ ...descriptor.draftAndPublish ? ["publishedAt"] : [],
479
+ ...descriptor.localized ? ["locale"] : []
480
+ ].filter((field) => field && field !== "id")
481
+ )
482
+ );
483
+ const params = {
484
+ filters,
485
+ fields,
486
+ sort: { updatedAt: "desc" },
487
+ limit,
488
+ start: 0
489
+ };
490
+ const populate = buildPopulate(descriptor.nestedFields);
491
+ if (populate) params.populate = populate;
492
+ if (descriptor.draftAndPublish) {
493
+ params.status = includeDrafts ? "draft" : "published";
494
+ }
495
+ if (descriptor.localized) {
496
+ params.locale = locale && locale !== "all" ? locale : "*";
497
+ }
498
+ return params;
499
+ }
500
+ });
501
+
502
+ // server/utils/text.ts
503
+ var MAX_DOC_DEPTH = 32;
504
+ var MAX_STRUCTURED_CHARS = 5e5;
505
+ var sanitizeQuery = (raw) => {
506
+ if (typeof raw !== "string") return "";
507
+ const cleaned = raw.replace(/%/g, "").trim().slice(0, MAX_QUERY_LENGTH);
508
+ return /^[_\s]+$/.test(cleaned) ? "" : cleaned;
509
+ };
510
+ var isProseMirrorRoot = (value) => {
511
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
512
+ const obj = value;
513
+ if (obj.type === "doc" && Array.isArray(obj.content)) return true;
514
+ if (obj.doc && typeof obj.doc === "object" && !Array.isArray(obj.doc)) {
515
+ const inner = obj.doc;
516
+ return inner.type === "doc" && Array.isArray(inner.content);
517
+ }
518
+ return false;
519
+ };
520
+ var walkDocNodes = (node, chunks, depth = 0, visited = /* @__PURE__ */ new WeakSet()) => {
521
+ if (!node || depth > MAX_DOC_DEPTH) return;
522
+ if (Array.isArray(node)) {
523
+ if (visited.has(node)) return;
524
+ visited.add(node);
525
+ node.forEach((child) => walkDocNodes(child, chunks, depth + 1, visited));
526
+ return;
527
+ }
528
+ if (typeof node !== "object") return;
529
+ if (visited.has(node)) return;
530
+ visited.add(node);
531
+ const obj = node;
532
+ if (typeof obj.text === "string" && obj.text.trim()) {
533
+ chunks.push(obj.text);
534
+ }
535
+ if (obj.doc && typeof obj.doc === "object") {
536
+ walkDocNodes(obj.doc, chunks, depth + 1, visited);
537
+ }
538
+ if (Array.isArray(obj.content)) {
539
+ walkDocNodes(obj.content, chunks, depth + 1, visited);
540
+ }
541
+ if (Array.isArray(obj.children)) {
542
+ walkDocNodes(obj.children, chunks, depth + 1, visited);
543
+ }
544
+ };
545
+ var extractStructuredText = (value) => {
546
+ let candidate = value;
547
+ if (typeof value === "string") {
548
+ const trimmed = value.trim();
549
+ if (!trimmed.startsWith("{") || trimmed.length > MAX_STRUCTURED_CHARS) {
550
+ return null;
551
+ }
552
+ try {
553
+ candidate = JSON.parse(trimmed);
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
558
+ if (!isProseMirrorRoot(candidate)) return null;
559
+ const chunks = [];
560
+ walkDocNodes(candidate, chunks);
561
+ return chunks.join(" ").replace(/\s+/g, " ").trim();
562
+ };
563
+ var stripHtmlTags = (input) => {
564
+ let out = "";
565
+ let tagStart = -1;
566
+ for (let i = 0; i < input.length; i += 1) {
567
+ const ch = input[i];
568
+ if (ch === "<") {
569
+ if (tagStart !== -1) {
570
+ out += input.slice(tagStart, i);
571
+ }
572
+ tagStart = i;
573
+ } else if (ch === ">" && tagStart !== -1) {
574
+ out += " ";
575
+ tagStart = -1;
576
+ } else if (tagStart === -1) {
577
+ out += ch;
578
+ }
579
+ }
580
+ if (tagStart !== -1) {
581
+ out += input.slice(tagStart);
582
+ }
583
+ return out;
584
+ };
585
+ var toPlainText = (value) => {
586
+ if (value === null || value === void 0) return "";
587
+ const structured = extractStructuredText(value);
588
+ if (structured !== null) return structured;
589
+ return stripHtmlTags(String(value)).replace(/[#*_>`~]+/g, " ").replace(/\s+/g, " ").trim();
590
+ };
591
+ var collectValues = (node, parts) => {
592
+ if (node === null || node === void 0) return [];
593
+ if (Array.isArray(node))
594
+ return node.flatMap((item) => collectValues(item, parts));
595
+ if (parts.length === 0) {
596
+ const structured = extractStructuredText(node);
597
+ if (structured !== null) return structured ? [structured] : [];
598
+ if (typeof node === "string") return [node];
599
+ if (typeof node === "number") return [String(node)];
600
+ return [];
601
+ }
602
+ if (typeof node !== "object") return [];
603
+ return collectValues(
604
+ node[parts[0]],
605
+ parts.slice(1)
606
+ );
607
+ };
608
+ var getValuesAtPath = (entry, path) => collectValues(entry, path.split("."));
609
+ var buildSnippet = (value, query) => {
610
+ const text = toPlainText(value);
611
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
612
+ if (index === -1) {
613
+ return text.length > SNIPPET_RADIUS * 2 ? `${text.slice(0, SNIPPET_RADIUS * 2)}\u2026` : text;
614
+ }
615
+ const start = Math.max(0, index - SNIPPET_RADIUS);
616
+ const end = Math.min(text.length, index + query.length + SNIPPET_RADIUS);
617
+ const prefix = start > 0 ? "\u2026" : "";
618
+ const suffix = end < text.length ? "\u2026" : "";
619
+ return `${prefix}${text.slice(start, end)}${suffix}`;
620
+ };
621
+ var humanizeFieldPath = (path) => path.split(".").map(
622
+ (segment) => segment.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase())
623
+ ).join(" \u203A ");
624
+
625
+ // server/services/ranking.ts
626
+ var ranking_default = () => ({
627
+ /** First path whose value satisfies `test`, searched in the given order. */
628
+ findMatch(entry, paths, test, normalize = false) {
629
+ for (const path of paths) {
630
+ const values = getValuesAtPath(entry, path);
631
+ for (const rawValue of values) {
632
+ const value = normalize ? toPlainText(rawValue) || rawValue : rawValue;
633
+ if (test(value)) return { path, value };
634
+ }
635
+ }
636
+ return null;
637
+ },
638
+ score(entry, descriptor, query) {
639
+ const lowered = query.toLowerCase();
640
+ const idSet = /* @__PURE__ */ new Set(["id", ...descriptor.idFields]);
641
+ const idPaths = Array.from(idSet);
642
+ const mainPaths = descriptor.mainField && descriptor.mainField !== "id" && descriptor.mainFieldSearchable !== false ? [descriptor.mainField] : [];
643
+ const attempts = [
644
+ [TIER.ID_EXACT, idPaths, (val) => val.toLowerCase() === lowered, false],
645
+ [
646
+ TIER.ID_CONTAINS,
647
+ idPaths,
648
+ (val) => val.toLowerCase().includes(lowered),
649
+ false
650
+ ],
651
+ [
652
+ TIER.MAIN_EXACT,
653
+ mainPaths,
654
+ (val) => val.toLowerCase() === lowered,
655
+ true
656
+ ],
657
+ [
658
+ TIER.MAIN_STARTS_WITH,
659
+ mainPaths,
660
+ (val) => val.toLowerCase().startsWith(lowered),
661
+ true
662
+ ],
663
+ [
664
+ TIER.MAIN_CONTAINS,
665
+ mainPaths,
666
+ (val) => val.toLowerCase().includes(lowered),
667
+ true
668
+ ],
669
+ [
670
+ TIER.FIELD_CONTAINS,
671
+ descriptor.topLevelFields,
672
+ (val) => val.toLowerCase().includes(lowered),
673
+ true
674
+ ],
675
+ [
676
+ TIER.NESTED_CONTAINS,
677
+ descriptor.nestedFields,
678
+ (val) => val.toLowerCase().includes(lowered),
679
+ true
680
+ ]
681
+ ];
682
+ for (const [tier, paths, test, normalize] of attempts) {
683
+ const match = this.findMatch(entry, paths, test, normalize);
684
+ if (match) {
685
+ return {
686
+ tier,
687
+ tierLabel: TIER_LABELS[tier],
688
+ matchedField: match.path,
689
+ matchedFieldLabel: humanizeFieldPath(match.path),
690
+ matchedSnippet: buildSnippet(match.value, query),
691
+ exactCase: match.value.includes(query),
692
+ valueLength: match.value.length
693
+ };
694
+ }
695
+ }
696
+ return {
697
+ tier: TIER.UNKNOWN,
698
+ tierLabel: TIER_LABELS[TIER.UNKNOWN],
699
+ matchedField: null,
700
+ matchedFieldLabel: null,
701
+ matchedSnippet: null,
702
+ exactCase: false,
703
+ valueLength: Number.MAX_SAFE_INTEGER
704
+ };
705
+ },
706
+ compare(a, b) {
707
+ if (a.tier !== b.tier) return a.tier - b.tier;
708
+ if (a.exactCase !== b.exactCase) return a.exactCase ? -1 : 1;
709
+ if (a.valueLength !== b.valueLength) return a.valueLength - b.valueLength;
710
+ return new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime();
711
+ },
712
+ sort(hits) {
713
+ return [...hits].sort((a, b) => this.compare(a, b));
714
+ }
715
+ });
716
+
717
+ // server/services/schema.ts
718
+ var schema_default2 = ({ strapi }) => {
719
+ let cache = null;
720
+ const computeSignature = (settings) => JSON.stringify({
721
+ settings,
722
+ uids: Object.keys(strapi.contentTypes).sort()
723
+ });
724
+ return {
725
+ /**
726
+ * Content types the plugin is allowed to look at: the app's own `api::` types
727
+ * plus user-facing `plugin::` types (excluding internal Strapi system plugins
728
+ * and types explicitly hidden from the Content Manager).
729
+ */
730
+ listCandidateUids(includePluginContentTypes = true) {
731
+ return Object.keys(strapi.contentTypes).filter((uid) => {
732
+ if (uid.startsWith("api::")) return true;
733
+ if (!includePluginContentTypes || !uid.startsWith("plugin::"))
734
+ return false;
735
+ if (SYSTEM_PLUGIN_PREFIXES.some((prefix) => uid.startsWith(prefix)))
736
+ return false;
737
+ const contentType = strapi.contentTypes[uid];
738
+ if (contentType?.pluginOptions?.["content-manager"]?.visible === false)
739
+ return false;
740
+ return true;
741
+ });
742
+ },
743
+ isSearchableAttribute(name, attribute) {
744
+ if (!attribute || BLOCKLISTED_ATTRIBUTE_NAMES.includes(name))
745
+ return false;
746
+ if (attribute.private === true) return false;
747
+ if (attribute.searchable === false) return false;
748
+ if (attribute.type && SKIPPED_ATTRIBUTE_TYPES.includes(attribute.type))
749
+ return false;
750
+ return Boolean(
751
+ attribute.type && SEARCHABLE_SCALAR_TYPES.includes(attribute.type)
752
+ );
753
+ },
754
+ isSafeDisplayAttribute(name, attribute) {
755
+ if (!name || typeof name !== "string") return false;
756
+ if (BLOCKLISTED_ATTRIBUTE_NAMES.includes(name)) return false;
757
+ if (!attribute || attribute.private === true) return false;
758
+ if (attribute.type && SKIPPED_ATTRIBUTE_TYPES.includes(attribute.type))
759
+ return false;
760
+ return true;
761
+ },
762
+ /**
763
+ * Flattens an attribute map into dotted, searchable field paths, walking into
764
+ * components up to `maxDepth`. `visited` breaks component cycles (a component
765
+ * that, directly or not, contains itself).
766
+ */
767
+ collectFieldPaths(attributes = {}, {
768
+ maxDepth,
769
+ depth = 0,
770
+ prefix = "",
771
+ visited = /* @__PURE__ */ new Set()
772
+ } = {
773
+ maxDepth: 0
774
+ }) {
775
+ const paths = [];
776
+ Object.entries(attributes).forEach(([name, attribute]) => {
777
+ const path = prefix ? `${prefix}.${name}` : name;
778
+ if (this.isSearchableAttribute(name, attribute)) {
779
+ paths.push(path);
780
+ return;
781
+ }
782
+ if (attribute.type !== "component" || depth >= maxDepth) return;
783
+ const component = strapi.components[attribute.component];
784
+ if (!component || !attribute.component || visited.has(attribute.component))
785
+ return;
786
+ paths.push(
787
+ ...this.collectFieldPaths(component.attributes, {
788
+ maxDepth,
789
+ depth: depth + 1,
790
+ prefix: path,
791
+ visited: /* @__PURE__ */ new Set([...visited, attribute.component])
792
+ })
793
+ );
794
+ });
795
+ return paths;
796
+ },
797
+ /** Resolves the field the Content Manager uses to label an entry. */
798
+ async resolveMainField(contentType) {
799
+ try {
800
+ const configuration = await strapi.plugin("content-manager").service("content-types").findConfiguration(contentType);
801
+ const mainField = configuration && configuration.settings && configuration.settings.mainField;
802
+ if (mainField && mainField !== "id" && this.isSafeDisplayAttribute(
803
+ mainField,
804
+ contentType.attributes?.[mainField]
805
+ )) {
806
+ return mainField;
807
+ }
808
+ } catch (error) {
809
+ const msg = error.message;
810
+ strapi.log.debug(
811
+ `[${PLUGIN_ID}] CM config read failed (${contentType.uid}): ${msg}`
812
+ );
813
+ }
814
+ const fallback = MAIN_FIELD_FALLBACKS.find(
815
+ (name) => this.isSearchableAttribute(name, contentType.attributes?.[name])
816
+ );
817
+ return fallback || "id";
818
+ },
819
+ extractExcludedFields(uid, excludedFields) {
820
+ return new Set(
821
+ (excludedFields || []).filter(
822
+ (entry) => typeof entry === "string" && (entry.startsWith(`${uid}:`) || entry.startsWith(`${uid}.`))
823
+ ).map((entry) => entry.slice(uid.length + 1))
824
+ );
825
+ },
826
+ resolveDisplayName(uid, info) {
827
+ return info?.displayName || info?.singularName || uid;
828
+ },
829
+ hasAnySearchableField(fields, isMainSearchable) {
830
+ if (fields.idFields.length > 0 || fields.topLevelFields.length > 0)
831
+ return true;
832
+ return fields.nestedFields.length > 0 || isMainSearchable;
833
+ },
834
+ async describeContentType(uid, settings) {
835
+ const contentType = strapi.contentTypes[uid];
836
+ if (!contentType) return null;
837
+ const attributes = contentType.attributes || {};
838
+ const excludedForType = this.extractExcludedFields(
839
+ uid,
840
+ settings.excludedFields
841
+ );
842
+ const allPaths = this.collectFieldPaths(attributes, {
843
+ maxDepth: settings.maxDepth
844
+ }).filter((path) => !excludedForType.has(path));
845
+ const mainField = await this.resolveMainField(contentType);
846
+ const mainFieldSearchable = !excludedForType.has(mainField) && this.isSearchableAttribute(mainField, attributes[mainField]);
847
+ const idFields = [
848
+ "documentId",
849
+ ...IDENTIFIER_FIELDS.filter(
850
+ (name) => this.isSearchableAttribute(name, attributes[name])
851
+ )
852
+ ].filter((name) => !excludedForType.has(name));
853
+ const topLevelFields = allPaths.filter(
854
+ (path) => !path.includes(".") && path !== mainField && !idFields.includes(path)
855
+ );
856
+ const nestedFields = settings.deep ? allPaths.filter((path) => path.includes(".")) : [];
857
+ const rawDisplay = settings.displayFields?.[uid];
858
+ const displayFields = Array.isArray(rawDisplay) ? rawDisplay.filter(
859
+ (name) => this.isSafeDisplayAttribute(name, attributes[name])
860
+ ).slice(0, 2) : [];
861
+ return {
862
+ uid,
863
+ kind: contentType.kind || "collectionType",
864
+ displayName: this.resolveDisplayName(uid, contentType.info),
865
+ apiName: contentType.apiName,
866
+ mainField,
867
+ mainFieldSearchable,
868
+ idFields,
869
+ topLevelFields,
870
+ nestedFields,
871
+ displayFields,
872
+ localized: Boolean(contentType.pluginOptions?.i18n?.localized),
873
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
874
+ /** Nothing to match against — the type is listed but never queried. */
875
+ searchable: this.hasAnySearchableField(
876
+ { idFields, topLevelFields, nestedFields },
877
+ mainFieldSearchable
878
+ )
879
+ };
880
+ },
881
+ async getSearchableContentTypes({
882
+ force = false
883
+ } = {}) {
884
+ const settings = await strapi.plugin(PLUGIN_ID).service("settings").get();
885
+ const signature = computeSignature(settings);
886
+ if (cache && !force && cache.signature === signature) {
887
+ return cache.types;
888
+ }
889
+ const excluded = new Set(settings.excludedContentTypes || []);
890
+ const includePlugins = settings.includePluginContentTypes !== false;
891
+ const described = await Promise.all(
892
+ this.listCandidateUids(includePlugins).filter((uid) => !excluded.has(uid)).map((uid) => this.describeContentType(uid, settings))
893
+ );
894
+ const types = described.filter(
895
+ (type) => Boolean(type && type.searchable)
896
+ ).sort((a, b) => a.displayName.localeCompare(b.displayName));
897
+ cache = { signature, types };
898
+ return types;
899
+ },
900
+ /** Every candidate type, searchable or not — the Settings page needs the full list. */
901
+ async getAllContentTypes() {
902
+ const settings = await strapi.plugin(PLUGIN_ID).service("settings").get();
903
+ const includePlugins = settings.includePluginContentTypes !== false;
904
+ const described = await Promise.all(
905
+ this.listCandidateUids(includePlugins).map(
906
+ (uid) => this.describeContentType(uid, settings)
907
+ )
908
+ );
909
+ return described.filter((type) => Boolean(type)).sort((a, b) => a.displayName.localeCompare(b.displayName));
910
+ },
911
+ clearCache() {
912
+ cache = null;
913
+ }
914
+ };
915
+ };
916
+
917
+ // server/utils/async.ts
918
+ var withTimeout = (factory, ms, label) => {
919
+ let timer;
920
+ return Promise.race([
921
+ Promise.resolve().then(factory).finally(() => clearTimeout(timer)),
922
+ new Promise((_, reject) => {
923
+ timer = setTimeout(
924
+ () => reject(new Error(`GLOBAL_SEARCH_TIMEOUT:${label}`)),
925
+ ms
926
+ );
927
+ })
928
+ ]);
929
+ };
930
+ var mapWithConcurrency = async (items, concurrency, worker) => {
931
+ const results = Array.from({ length: items.length });
932
+ let cursor = 0;
933
+ const safeConcurrency = Math.max(1, Math.floor(Number(concurrency) || 1));
934
+ const runners = Array.from(
935
+ { length: Math.min(safeConcurrency, items.length) },
936
+ async () => {
937
+ while (cursor < items.length) {
938
+ const index = cursor;
939
+ cursor += 1;
940
+ results[index] = await worker(items[index], index);
941
+ }
942
+ }
943
+ );
944
+ await Promise.all(runners);
945
+ return results;
946
+ };
947
+
948
+ // server/services/search.ts
949
+ var service = (strapi, name) => strapi.plugin(PLUGIN_ID).service(name);
950
+ var search_default2 = ({ strapi }) => ({
951
+ /** Content Manager deep-link for a hit. */
952
+ buildAdminUrl(descriptor, entry) {
953
+ const localeQuery = entry.locale ? `?locale=${encodeURIComponent(entry.locale)}` : "";
954
+ if (descriptor.kind === "singleType") {
955
+ return `/content-manager/single-types/${descriptor.uid}${localeQuery}`;
956
+ }
957
+ const documentId = encodeURIComponent(String(entry.documentId || entry.id));
958
+ const base = `/content-manager/collection-types/${descriptor.uid}`;
959
+ return `${base}/${documentId}${localeQuery}`;
960
+ },
961
+ async queryContentType(descriptor, {
962
+ query,
963
+ locale,
964
+ settings
965
+ }) {
966
+ const params = service(strapi, "query").buildParams(descriptor, {
967
+ query,
968
+ locale,
969
+ limit: settings.perTypeLimit,
970
+ includeDrafts: settings.includeDrafts
971
+ });
972
+ if (!params) return [];
973
+ const result = await strapi.documents(descriptor.uid).findMany(params);
974
+ return Array.isArray(result) ? result : [result].filter(Boolean);
975
+ },
976
+ resolveEntryTitle(descriptor, entry) {
977
+ const mainVal = descriptor.mainField ? entry[descriptor.mainField] : "";
978
+ if (mainVal) return String(mainVal);
979
+ return String(
980
+ entry.name || entry.title || `#${entry.documentId || entry.id}`
981
+ );
982
+ },
983
+ resolveEntryStatus(descriptor, entry) {
984
+ if (!descriptor.draftAndPublish) return null;
985
+ return entry.publishedAt ? "published" : "draft";
986
+ },
987
+ toHit(descriptor, entry, query, settings) {
988
+ const scored = service(strapi, "ranking").score(entry, descriptor, query);
989
+ const rawExtra = descriptor.displayFields ?? (Array.isArray(settings.displayFields?.[descriptor.uid]) ? settings.displayFields[descriptor.uid] : []);
990
+ const extraFields = rawExtra.filter(
991
+ (field) => typeof field === "string" && Boolean(field) && !BLOCKLISTED_ATTRIBUTE_NAMES.includes(field)
992
+ ).slice(0, 2);
993
+ return {
994
+ ...scored,
995
+ contentTypeUid: descriptor.uid,
996
+ contentTypeLabel: descriptor.displayName,
997
+ kind: descriptor.kind,
998
+ id: entry.id,
999
+ documentId: entry.documentId || null,
1000
+ title: this.resolveEntryTitle(descriptor, entry),
1001
+ mainField: descriptor.mainField,
1002
+ extra: extraFields.map((field) => ({
1003
+ field,
1004
+ value: entry[field] ?? null
1005
+ })),
1006
+ locale: entry.locale || null,
1007
+ status: this.resolveEntryStatus(descriptor, entry),
1008
+ updatedAt: entry.updatedAt || null,
1009
+ adminUrl: this.buildAdminUrl(descriptor, entry)
1010
+ };
1011
+ },
1012
+ /**
1013
+ * Fans out across every permitted content type, ranks the merged set, then
1014
+ * paginates in memory.
1015
+ *
1016
+ * `total` is the size of the *capped* merged set (`perTypeLimit` rows per type,
1017
+ * `maxResults` overall), not a database count — searching for a common word in a
1018
+ * 100k-row table must not turn into a full-table `COUNT`.
1019
+ */
1020
+ async search({
1021
+ query: rawQuery,
1022
+ types,
1023
+ locale,
1024
+ page = 1,
1025
+ pageSize = 20,
1026
+ userAbility
1027
+ } = {}) {
1028
+ const settings = await service(
1029
+ strapi,
1030
+ "settings"
1031
+ ).get();
1032
+ const query = sanitizeQuery(rawQuery);
1033
+ const safePageSize = Math.min(
1034
+ Math.max(Math.floor(Number(pageSize)) || 20, 1),
1035
+ 100
1036
+ );
1037
+ const empty = {
1038
+ query,
1039
+ results: [],
1040
+ groups: [],
1041
+ pagination: { page: 1, pageSize: safePageSize, total: 0, pageCount: 0 },
1042
+ meta: {
1043
+ searchedTypes: 0,
1044
+ truncatedTypes: [],
1045
+ failedTypes: [],
1046
+ minChars: settings.minChars
1047
+ }
1048
+ };
1049
+ if (query.length < settings.minChars) return empty;
1050
+ const searchable = await service(
1051
+ strapi,
1052
+ "schema"
1053
+ ).getSearchableContentTypes();
1054
+ const requested = Array.isArray(types) && types.length ? new Set(types) : null;
1055
+ const candidates = service(strapi, "permissions").filterReadableTypes(searchable, userAbility).filter(
1056
+ (descriptor) => !requested || requested.has(descriptor.uid)
1057
+ );
1058
+ if (!candidates.length) return { ...empty, query };
1059
+ const truncatedTypes = [];
1060
+ const failedTypes = [];
1061
+ const perType = await mapWithConcurrency(
1062
+ candidates,
1063
+ settings.concurrency,
1064
+ async (descriptor) => {
1065
+ try {
1066
+ const entries = await withTimeout(
1067
+ () => this.queryContentType(descriptor, { query, locale, settings }),
1068
+ settings.queryTimeout,
1069
+ descriptor.uid
1070
+ );
1071
+ if (entries.length >= settings.perTypeLimit)
1072
+ truncatedTypes.push(descriptor.uid);
1073
+ return entries.map(
1074
+ (entry) => this.toHit(descriptor, entry, query, settings)
1075
+ );
1076
+ } catch (error) {
1077
+ const msg = error.message;
1078
+ failedTypes.push({ uid: descriptor.uid, message: msg });
1079
+ strapi.log.warn(
1080
+ `[${PLUGIN_ID}] search failed for ${descriptor.uid}: ${msg}`
1081
+ );
1082
+ return [];
1083
+ }
1084
+ }
1085
+ );
1086
+ const allHits = perType.flat();
1087
+ const ranked = service(strapi, "ranking").sort(allHits).slice(0, settings.maxResults);
1088
+ const total = ranked.length;
1089
+ const pageCount = Math.ceil(total / safePageSize) || 0;
1090
+ const safePage = Math.min(
1091
+ Math.max(Math.floor(Number(page)) || 1, 1),
1092
+ pageCount || 1
1093
+ );
1094
+ const start = (safePage - 1) * safePageSize;
1095
+ const results = ranked.slice(start, start + safePageSize);
1096
+ const groups = candidates.map((descriptor) => ({
1097
+ uid: descriptor.uid,
1098
+ label: descriptor.displayName,
1099
+ kind: descriptor.kind,
1100
+ count: ranked.filter((hit) => hit.contentTypeUid === descriptor.uid).length,
1101
+ truncated: truncatedTypes.includes(descriptor.uid)
1102
+ })).filter((group) => group.count > 0).sort((a, b) => b.count - a.count);
1103
+ return {
1104
+ query,
1105
+ results,
1106
+ groups,
1107
+ pagination: { page: safePage, pageSize: safePageSize, total, pageCount },
1108
+ meta: {
1109
+ searchedTypes: candidates.length,
1110
+ truncatedTypes,
1111
+ failedTypes,
1112
+ minChars: settings.minChars,
1113
+ capped: allHits.length > settings.maxResults || total >= settings.maxResults || truncatedTypes.length > 0
1114
+ }
1115
+ };
1116
+ }
1117
+ });
1118
+
1119
+ // server/services/settings.ts
1120
+ var STORE_KEY = "settings";
1121
+ var ALLOWED_KEYS = Object.keys(defaults);
1122
+ var getStore = (strapi) => strapi.store({ type: "plugin", name: PLUGIN_ID });
1123
+ var pickAllowed = (source) => {
1124
+ if (!isPlainObject(source)) return {};
1125
+ return Object.entries(source).reduce((acc, [key, value]) => {
1126
+ if (ALLOWED_KEYS.includes(key) && value !== void 0) {
1127
+ acc[key] = value;
1128
+ }
1129
+ return acc;
1130
+ }, {});
1131
+ };
1132
+ var settings_default2 = ({ strapi }) => ({
1133
+ /**
1134
+ * Resolution order: values saved from the Settings page, then `config/plugins.js`,
1135
+ * then the built-in defaults. Every key always resolves to something usable.
1136
+ */
1137
+ async get() {
1138
+ const fromFile = pickAllowed(strapi.config.get(`plugin::${PLUGIN_ID}`, {}));
1139
+ const fromStore = pickAllowed(
1140
+ await getStore(strapi).get({ key: STORE_KEY })
1141
+ );
1142
+ return { ...defaults, ...fromFile, ...fromStore };
1143
+ },
1144
+ async set(patch = {}) {
1145
+ if (!isPlainObject(patch)) {
1146
+ throw new Error("[global-search] settings payload must be an object");
1147
+ }
1148
+ const sanitized = pickAllowed(patch);
1149
+ validator(sanitized);
1150
+ const current = pickAllowed(await getStore(strapi).get({ key: STORE_KEY }));
1151
+ const next = { ...current, ...sanitized };
1152
+ await getStore(strapi).set({ key: STORE_KEY, value: next });
1153
+ strapi.plugin(PLUGIN_ID).service("schema").clearCache();
1154
+ return this.get();
1155
+ },
1156
+ async reset() {
1157
+ await getStore(strapi).set({ key: STORE_KEY, value: {} });
1158
+ strapi.plugin(PLUGIN_ID).service("schema").clearCache();
1159
+ return this.get();
1160
+ },
1161
+ defaults
1162
+ });
1163
+
1164
+ // server/services/index.ts
1165
+ var services_default = {
1166
+ schema: schema_default2,
1167
+ query: query_default,
1168
+ ranking: ranking_default,
1169
+ permissions: permissions_default,
1170
+ settings: settings_default2,
1171
+ search: search_default2
1172
+ };
1173
+
1174
+ // server/index.ts
1175
+ var index_default = {
1176
+ register: register_default,
1177
+ bootstrap: bootstrap_default,
1178
+ config: config_default,
1179
+ controllers: controllers_default,
1180
+ routes: routes_default,
1181
+ services: services_default
1182
+ };
1183
+ export {
1184
+ index_default as default
1185
+ };
1186
+ //# sourceMappingURL=index.mjs.map