@dalencatt/strapi-plugin-fuzzy-search-private 0.0.0-development
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.
- package/LICENSE +21 -0
- package/README.md +579 -0
- package/dist/server/index.js +695 -0
- package/dist/server/index.mjs +677 -0
- package/dist/server/src/bootstrap.d.ts +2 -0
- package/dist/server/src/config/config.schema.d.ts +19 -0
- package/dist/server/src/config/index.d.ts +8 -0
- package/dist/server/src/config/query.schema.d.ts +34 -0
- package/dist/server/src/controllers/index.d.ts +8 -0
- package/dist/server/src/controllers/search-controller.d.ts +7 -0
- package/dist/server/src/graphql/index.d.ts +3 -0
- package/dist/server/src/graphql/resolvers-config.d.ts +8 -0
- package/dist/server/src/graphql/types.d.ts +3 -0
- package/dist/server/src/index.d.ts +33 -0
- package/dist/server/src/interfaces/interfaces.d.ts +98 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/index.d.ts +11 -0
- package/dist/server/src/routes/search-routes.d.ts +6 -0
- package/dist/server/src/services/fuzzySearch-service.d.ts +23 -0
- package/dist/server/src/services/index.d.ts +4 -0
- package/dist/server/src/services/pagination-service.d.ts +9 -0
- package/dist/server/src/services/response-transformation-service.d.ts +7 -0
- package/dist/server/src/services/settings-service.d.ts +8 -0
- package/dist/server/src/services/validation-service.d.ts +4 -0
- package/dist/server/src/utils/pluginId.d.ts +2 -0
- package/package.json +114 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const yup = require("yup");
|
|
3
|
+
const utils = require("@strapi/utils");
|
|
4
|
+
const fuzzysort = require("fuzzysort");
|
|
5
|
+
const transliteration = require("transliteration");
|
|
6
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
7
|
+
function _interopNamespace(e) {
|
|
8
|
+
if (e && e.__esModule) return e;
|
|
9
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
10
|
+
if (e) {
|
|
11
|
+
for (const k in e) {
|
|
12
|
+
if (k !== "default") {
|
|
13
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: () => e[k]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
|
|
25
|
+
const fuzzysort__default = /* @__PURE__ */ _interopDefault(fuzzysort);
|
|
26
|
+
const bootstrap = () => {
|
|
27
|
+
};
|
|
28
|
+
const pluginConfigSchema = yup__namespace.object({
|
|
29
|
+
contentTypes: yup__namespace.array().of(
|
|
30
|
+
yup__namespace.object({
|
|
31
|
+
uid: yup__namespace.string().required(),
|
|
32
|
+
modelName: yup__namespace.string().required(),
|
|
33
|
+
transliterate: yup__namespace.boolean(),
|
|
34
|
+
fuzzysortOptions: yup__namespace.object({
|
|
35
|
+
threshold: yup__namespace.number(),
|
|
36
|
+
limit: yup__namespace.number(),
|
|
37
|
+
keys: yup__namespace.array().of(
|
|
38
|
+
yup__namespace.object({
|
|
39
|
+
name: yup__namespace.string().required(),
|
|
40
|
+
weight: yup__namespace.number()
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
}).required()
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
}).noUnknown();
|
|
47
|
+
const config = {
|
|
48
|
+
default() {
|
|
49
|
+
return {
|
|
50
|
+
contentTypes: {}
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
async validator(config2) {
|
|
54
|
+
await pluginConfigSchema.validate(config2);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const paginationSchema = yup.object({
|
|
58
|
+
pageSize: yup.string().matches(/^\d+$/, "pageSize must be an integer"),
|
|
59
|
+
page: yup.string().matches(/^\d+$/, "page must be an integer"),
|
|
60
|
+
withCount: yup.string().oneOf(
|
|
61
|
+
["true", "false"],
|
|
62
|
+
"withCount must either be 'true' or 'false'"
|
|
63
|
+
)
|
|
64
|
+
});
|
|
65
|
+
yup.string().required();
|
|
66
|
+
const querySchema = yup.object({
|
|
67
|
+
query: yup.string().required(),
|
|
68
|
+
locale: yup.string(),
|
|
69
|
+
filters: yup.object({
|
|
70
|
+
contentTypes: yup.string()
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
const { ValidationError } = utils.errors;
|
|
74
|
+
const validateFilteredContentTypes = (configModels, filterModel) => {
|
|
75
|
+
if (!configModels.has(filterModel))
|
|
76
|
+
throw new Error(
|
|
77
|
+
`'${filterModel}' was found in contentTypes filter query, however this model is not configured in the fuzzy-search config`
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
const validatePaginationQueryParams = async (configModels, pagination) => {
|
|
81
|
+
const paginatedEntries = Object.entries(pagination);
|
|
82
|
+
for (const [pluralName, paginationValues] of paginatedEntries) {
|
|
83
|
+
if (!configModels.has(pluralName)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Pagination queries for model '${pluralName}' were found, however this model is not configured in the fuzzy-search config`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
await paginationSchema.validate(paginationValues);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const validateNestedQueryParams = (configModels, nestedParams) => {
|
|
92
|
+
const filterKeys = Object.keys(nestedParams);
|
|
93
|
+
filterKeys.forEach((key) => {
|
|
94
|
+
if (key !== "contentTypes" && !configModels.has(key)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Query params for model '${key}' were found, however this model is not configured in the fuzzy-search config`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const validateQueryParams = async (query, contentTypes, pagination, populate, filteredContentTypes) => {
|
|
102
|
+
const configModels = new Set(
|
|
103
|
+
contentTypes.map((contentType) => contentType.info.pluralName)
|
|
104
|
+
);
|
|
105
|
+
await querySchema.validate(query);
|
|
106
|
+
if (pagination) await validatePaginationQueryParams(configModels, pagination);
|
|
107
|
+
if (query.filters) validateNestedQueryParams(configModels, query.filters);
|
|
108
|
+
if (populate) validateNestedQueryParams(configModels, populate);
|
|
109
|
+
if (filteredContentTypes)
|
|
110
|
+
filteredContentTypes.forEach(
|
|
111
|
+
(model) => validateFilteredContentTypes(configModels, model)
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
const validateQuery = async (contentType, locale) => {
|
|
115
|
+
if (contentType.kind !== "collectionType")
|
|
116
|
+
throw new ValidationError(
|
|
117
|
+
`Content type: '${contentType.modelName}' is not a collectionType`
|
|
118
|
+
);
|
|
119
|
+
contentType.fuzzysortOptions.keys.forEach((key) => {
|
|
120
|
+
const attributeKeys = Object.keys(contentType.attributes);
|
|
121
|
+
if (!attributeKeys.includes(key.name))
|
|
122
|
+
throw new ValidationError(
|
|
123
|
+
`Key: '${key.name}' is not a valid field for model: '${contentType.modelName}`
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
if (!locale) return;
|
|
127
|
+
const isLocalizedContentType = await strapi.plugins.i18n.services["content-types"].isLocalizedContentType(
|
|
128
|
+
contentType
|
|
129
|
+
);
|
|
130
|
+
if (!isLocalizedContentType) {
|
|
131
|
+
throw new ValidationError(
|
|
132
|
+
`A query for the locale: '${locale}' was found, however model: '${contentType.modelName}' is not a localized content type. Enable localization for all content types if you want to query for localized entries via the locale parameter.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const weightScores = (a, keys) => {
|
|
137
|
+
const weightedScores = keys.map((key, index2) => {
|
|
138
|
+
const weight = key.weight || 0;
|
|
139
|
+
return a[index2] ? +a[index2].score + weight : -9999;
|
|
140
|
+
});
|
|
141
|
+
return Math.max(...weightedScores);
|
|
142
|
+
};
|
|
143
|
+
const transformEntryKeysToString = (entries, keys) => entries.map(
|
|
144
|
+
(entry) => transformEntry(entry, keys, (value) => value?.toString())
|
|
145
|
+
);
|
|
146
|
+
const limitCharacters = (entries, characterLimit, keys) => entries.map(
|
|
147
|
+
(entry) => transformEntry(
|
|
148
|
+
entry,
|
|
149
|
+
keys,
|
|
150
|
+
(value) => value?.toString().slice(0, characterLimit)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
const transformEntry = (entry, keys, transformFn) => {
|
|
154
|
+
const transformedEntry = { ...entry };
|
|
155
|
+
const entryKeys = Object.keys(transformedEntry);
|
|
156
|
+
entryKeys.forEach((key) => {
|
|
157
|
+
if (!keys.includes(key)) return;
|
|
158
|
+
transformedEntry[key] = transformFn(transformedEntry[key]);
|
|
159
|
+
});
|
|
160
|
+
return transformedEntry;
|
|
161
|
+
};
|
|
162
|
+
const buildResult = ({
|
|
163
|
+
entries,
|
|
164
|
+
fuzzysortOptions,
|
|
165
|
+
keys,
|
|
166
|
+
query
|
|
167
|
+
}) => {
|
|
168
|
+
const transformedEntries = fuzzysortOptions.characterLimit ? limitCharacters(entries, fuzzysortOptions.characterLimit, keys) : transformEntryKeysToString(entries, keys);
|
|
169
|
+
return fuzzysort__default.default.go(query, transformedEntries, {
|
|
170
|
+
threshold: fuzzysortOptions.threshold,
|
|
171
|
+
limit: fuzzysortOptions.limit,
|
|
172
|
+
keys,
|
|
173
|
+
scoreFn: (a) => weightScores(a, fuzzysortOptions.keys)
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const transliterateEntries = (entries) => entries.map((entry) => {
|
|
177
|
+
const entryKeys = Object.keys(entry);
|
|
178
|
+
entry.transliterations = {};
|
|
179
|
+
entryKeys.forEach((key) => {
|
|
180
|
+
if (!entry[key]) return;
|
|
181
|
+
entry.transliterations[key] = transliteration.transliterate(entry[key]);
|
|
182
|
+
});
|
|
183
|
+
return entry;
|
|
184
|
+
});
|
|
185
|
+
const buildTransliteratedResult = ({
|
|
186
|
+
entries,
|
|
187
|
+
fuzzysortOptions,
|
|
188
|
+
keys,
|
|
189
|
+
query,
|
|
190
|
+
result
|
|
191
|
+
}) => {
|
|
192
|
+
const { keys: fuzzysortKeys, threshold, limit } = fuzzysortOptions;
|
|
193
|
+
const transliteratedEntries = transliterateEntries(entries);
|
|
194
|
+
const transliterationKeys = keys.map((key) => `transliterations.${key}`);
|
|
195
|
+
const transliteratedResult = fuzzysort__default.default.go(
|
|
196
|
+
query,
|
|
197
|
+
transliteratedEntries,
|
|
198
|
+
{
|
|
199
|
+
threshold,
|
|
200
|
+
limit,
|
|
201
|
+
keys: transliterationKeys,
|
|
202
|
+
scoreFn: (a) => weightScores(a, fuzzysortKeys)
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
entries.forEach((entry) => delete entry.transliterations);
|
|
206
|
+
if (!result.total) return transliteratedResult;
|
|
207
|
+
const newResults = [...result];
|
|
208
|
+
transliteratedResult.forEach((res) => {
|
|
209
|
+
const origIndex = newResults.findIndex(
|
|
210
|
+
(origRes) => origRes.obj.id === res.obj.id && origRes.score <= res.score
|
|
211
|
+
);
|
|
212
|
+
if (origIndex >= 0) newResults[origIndex] = res;
|
|
213
|
+
});
|
|
214
|
+
newResults.sort((a, b) => b.score - a.score);
|
|
215
|
+
return newResults;
|
|
216
|
+
};
|
|
217
|
+
async function getResult({
|
|
218
|
+
contentType,
|
|
219
|
+
query,
|
|
220
|
+
filters,
|
|
221
|
+
populate,
|
|
222
|
+
locale,
|
|
223
|
+
status
|
|
224
|
+
}) {
|
|
225
|
+
const buildFilteredEntries = async () => {
|
|
226
|
+
await validateQuery(contentType, locale);
|
|
227
|
+
return await strapi.documents(contentType.uid).findMany({
|
|
228
|
+
...filters && { filters },
|
|
229
|
+
...locale && { locale },
|
|
230
|
+
...populate && { populate },
|
|
231
|
+
status: status ?? "published"
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const filteredEntries = await buildFilteredEntries();
|
|
235
|
+
const keys = contentType.fuzzysortOptions.keys.map((key) => key.name);
|
|
236
|
+
let result = buildResult({
|
|
237
|
+
entries: filteredEntries,
|
|
238
|
+
fuzzysortOptions: contentType.fuzzysortOptions,
|
|
239
|
+
keys,
|
|
240
|
+
query
|
|
241
|
+
});
|
|
242
|
+
if (contentType.transliterate) {
|
|
243
|
+
result = buildTransliteratedResult({
|
|
244
|
+
entries: filteredEntries,
|
|
245
|
+
fuzzysortOptions: contentType.fuzzysortOptions,
|
|
246
|
+
keys,
|
|
247
|
+
query,
|
|
248
|
+
result
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
fuzzysortResults: result,
|
|
253
|
+
schema: contentType
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const parsePaginationArgs = ({
|
|
257
|
+
page: pageQuery = "1",
|
|
258
|
+
pageSize: pageSizeQuery = "25",
|
|
259
|
+
withCount: withCountQuery = "true"
|
|
260
|
+
}) => {
|
|
261
|
+
const page = parseInt(pageQuery, 10);
|
|
262
|
+
const pageSize = parseInt(pageSizeQuery, 10);
|
|
263
|
+
const withCount = withCountQuery === "true";
|
|
264
|
+
return { page, pageSize, withCount };
|
|
265
|
+
};
|
|
266
|
+
const paginateRestResults = async (pagination, pluralNames, resultsResponse) => {
|
|
267
|
+
const currentResult = { ...resultsResponse };
|
|
268
|
+
const paginatedResult = {};
|
|
269
|
+
const buildPaginatedResults = (pluralName) => {
|
|
270
|
+
const { page, pageSize, withCount } = parsePaginationArgs(
|
|
271
|
+
pagination[pluralName]
|
|
272
|
+
);
|
|
273
|
+
paginatedResult[pluralName] = {
|
|
274
|
+
data: [],
|
|
275
|
+
meta: { pagination: { page: 1, pageSize: 25 } }
|
|
276
|
+
};
|
|
277
|
+
const startIndex = pageSize * (page - 1);
|
|
278
|
+
const endIndex = startIndex + pageSize;
|
|
279
|
+
paginatedResult[pluralName].data = currentResult[pluralName].slice(
|
|
280
|
+
startIndex,
|
|
281
|
+
endIndex
|
|
282
|
+
);
|
|
283
|
+
const meta = {
|
|
284
|
+
pagination: {
|
|
285
|
+
page,
|
|
286
|
+
pageSize
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
if (withCount) {
|
|
290
|
+
const total = resultsResponse[pluralName].length;
|
|
291
|
+
meta.pagination.total = total;
|
|
292
|
+
meta.pagination.pageCount = Math.ceil(total / pageSize);
|
|
293
|
+
}
|
|
294
|
+
paginatedResult[pluralName].meta = meta;
|
|
295
|
+
};
|
|
296
|
+
pluralNames.forEach((pluralName) => {
|
|
297
|
+
if (!pagination[pluralName]) return;
|
|
298
|
+
buildPaginatedResults(pluralName);
|
|
299
|
+
});
|
|
300
|
+
return { ...resultsResponse, ...paginatedResult };
|
|
301
|
+
};
|
|
302
|
+
const paginateGraphQlResults = (results, { limit, start }) => {
|
|
303
|
+
const resultsCopy = [...results];
|
|
304
|
+
const data = resultsCopy.slice(start, start + limit);
|
|
305
|
+
const meta = {
|
|
306
|
+
start,
|
|
307
|
+
limit
|
|
308
|
+
};
|
|
309
|
+
return { data, meta };
|
|
310
|
+
};
|
|
311
|
+
const { sanitize } = strapi.contentAPI;
|
|
312
|
+
const sanitizeOutput = (data, schema, auth) => sanitize.output(data, schema, { auth });
|
|
313
|
+
const buildGraphqlResponse = async (searchResult, schema, auth, pagination) => {
|
|
314
|
+
const { service: getService } = strapi.plugin("graphql");
|
|
315
|
+
const { returnTypes } = getService("format");
|
|
316
|
+
const { toEntityResponseCollection } = returnTypes;
|
|
317
|
+
const results = await Promise.all(
|
|
318
|
+
searchResult.map(
|
|
319
|
+
async (fuzzyRes) => await sanitizeOutput(fuzzyRes.obj, schema, auth)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
const { data: nodes, meta } = paginateGraphQlResults(results, pagination);
|
|
323
|
+
return toEntityResponseCollection(nodes, {
|
|
324
|
+
args: meta,
|
|
325
|
+
resourceUID: schema.uid
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
const buildRestResponse = async (searchResults, auth, pagination, queriedContentTypes) => {
|
|
329
|
+
const resultsResponse = {};
|
|
330
|
+
for (const res of searchResults) {
|
|
331
|
+
const sanitizeEntry = async (fuzzyRes) => {
|
|
332
|
+
return await sanitizeOutput(fuzzyRes.obj, res.schema, auth);
|
|
333
|
+
};
|
|
334
|
+
const buildSanitizedEntries = async () => res.fuzzysortResults.map(
|
|
335
|
+
async (fuzzyRes) => await sanitizeEntry(fuzzyRes)
|
|
336
|
+
);
|
|
337
|
+
resultsResponse[res.schema.info.pluralName] = await Promise.all(
|
|
338
|
+
await buildSanitizedEntries()
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!pagination) return resultsResponse;
|
|
342
|
+
const modelNames = queriedContentTypes || Object.keys(pagination);
|
|
343
|
+
return await paginateRestResults(pagination, modelNames, resultsResponse);
|
|
344
|
+
};
|
|
345
|
+
const name = "strapi-plugin-fuzzy-search";
|
|
346
|
+
const version = "0.0.0-development";
|
|
347
|
+
const description = "Register a weighted fuzzy search endpoint for Strapi Headless CMS you can add your content types to in no time.";
|
|
348
|
+
const strapi$1 = {
|
|
349
|
+
displayName: "Fuzzy Search",
|
|
350
|
+
name: "fuzzy-search",
|
|
351
|
+
description: "Register a weighted fuzzy search endpoint to your content types in no time.",
|
|
352
|
+
kind: "plugin"
|
|
353
|
+
};
|
|
354
|
+
const type = "commonjs";
|
|
355
|
+
const exports$1 = {
|
|
356
|
+
"./package.json": "./package.json",
|
|
357
|
+
"./strapi-server": {
|
|
358
|
+
types: "./dist/server/src/index.d.ts",
|
|
359
|
+
source: "./server/src/index.ts",
|
|
360
|
+
"import": "./dist/server/index.mjs",
|
|
361
|
+
require: "./dist/server/index.js",
|
|
362
|
+
"default": "./dist/server/index.js"
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const files = [
|
|
366
|
+
"dist"
|
|
367
|
+
];
|
|
368
|
+
const scripts = {
|
|
369
|
+
"semantic-release": "semantic-release",
|
|
370
|
+
build: "strapi-plugin build",
|
|
371
|
+
watch: "strapi-plugin watch",
|
|
372
|
+
"watch:link": "strapi-plugin watch:link",
|
|
373
|
+
verify: "strapi-plugin verify",
|
|
374
|
+
"test:ts:back": "run -T tsc -p server/tsconfig.json",
|
|
375
|
+
typecheck: "tsc --noEmit -p server/tsconfig.json",
|
|
376
|
+
test: "vitest",
|
|
377
|
+
"test:coverage": "vitest run --coverage",
|
|
378
|
+
"test:coverage:json": "vitest run --coverage.enabled --coverage.reporter=json-summary",
|
|
379
|
+
lint: "eslint .",
|
|
380
|
+
"prettier:check": "prettier --check ./server ./tests",
|
|
381
|
+
"prettier:write": "prettier --write ./server ./tests"
|
|
382
|
+
};
|
|
383
|
+
const publishConfig = {
|
|
384
|
+
registry: "https://registry.npmjs.org/",
|
|
385
|
+
tag: "latest"
|
|
386
|
+
};
|
|
387
|
+
const release = {
|
|
388
|
+
branches: [
|
|
389
|
+
"main",
|
|
390
|
+
{
|
|
391
|
+
name: "beta",
|
|
392
|
+
prerelease: true
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
const dependencies = {
|
|
397
|
+
fuzzysort: "3.1.0",
|
|
398
|
+
transliteration: "2.3.5"
|
|
399
|
+
};
|
|
400
|
+
const peerDependencies = {
|
|
401
|
+
"@strapi/sdk-plugin": "^5.2.7",
|
|
402
|
+
"@strapi/strapi": "^5.1.1",
|
|
403
|
+
"@strapi/utils": "^5.1.1",
|
|
404
|
+
yup: "1.4.0"
|
|
405
|
+
};
|
|
406
|
+
const devDependencies = {
|
|
407
|
+
"@eslint/compat": "^1.2.2",
|
|
408
|
+
"@eslint/eslintrc": "^3.1.0",
|
|
409
|
+
"@eslint/js": "^9.13.0",
|
|
410
|
+
"@strapi/sdk-plugin": "^5.2.7",
|
|
411
|
+
"@strapi/strapi": "^5.1.1",
|
|
412
|
+
"@strapi/typescript-utils": "^5.1.1",
|
|
413
|
+
"@typescript-eslint/eslint-plugin": "8.12.1",
|
|
414
|
+
"@typescript-eslint/parser": "8.12.1",
|
|
415
|
+
"@vitest/coverage-v8": "2.1.4",
|
|
416
|
+
"all-contributors-cli": "^6.20.0",
|
|
417
|
+
eslint: "^9.13.0",
|
|
418
|
+
"eslint-config-prettier": "9.1.0",
|
|
419
|
+
"eslint-plugin-import": "2.31.0",
|
|
420
|
+
"eslint-plugin-prettier": "5.2.1",
|
|
421
|
+
"eslint-plugin-promise": "7.1.0",
|
|
422
|
+
globals: "^15.11.0",
|
|
423
|
+
prettier: "3.3.3",
|
|
424
|
+
"semantic-release": "^24.0.0",
|
|
425
|
+
typescript: "^5.6.3",
|
|
426
|
+
"typescript-eslint": "^8.12.1",
|
|
427
|
+
vitest: "2.1.4"
|
|
428
|
+
};
|
|
429
|
+
const author = "@DomDew (https://github.com/DomDew)";
|
|
430
|
+
const maintainers = [
|
|
431
|
+
"@DomDew (https://github.com/DomDew)",
|
|
432
|
+
"@wfproductions (https://github.com/wfproductions)"
|
|
433
|
+
];
|
|
434
|
+
const engines = {
|
|
435
|
+
node: ">=18.x.x <=20.x.x",
|
|
436
|
+
npm: ">=6.0.0"
|
|
437
|
+
};
|
|
438
|
+
const license = "MIT";
|
|
439
|
+
const repository = {
|
|
440
|
+
type: "git",
|
|
441
|
+
url: "https://github.com/DomDew/strapi-plugin-fuzzy-search.git"
|
|
442
|
+
};
|
|
443
|
+
const keywords = [
|
|
444
|
+
"strapi",
|
|
445
|
+
"fuzzysort",
|
|
446
|
+
"fuzzysearch",
|
|
447
|
+
"search"
|
|
448
|
+
];
|
|
449
|
+
const bugs = {
|
|
450
|
+
url: "https://github.com/DomDew/strapi-plugin-fuzzy-search/issues"
|
|
451
|
+
};
|
|
452
|
+
const homepage = "https://github.com/DomDew/strapi-plugin-fuzzy-search#readme";
|
|
453
|
+
const packageJson = {
|
|
454
|
+
name,
|
|
455
|
+
version,
|
|
456
|
+
description,
|
|
457
|
+
strapi: strapi$1,
|
|
458
|
+
type,
|
|
459
|
+
exports: exports$1,
|
|
460
|
+
files,
|
|
461
|
+
scripts,
|
|
462
|
+
publishConfig,
|
|
463
|
+
release,
|
|
464
|
+
dependencies,
|
|
465
|
+
peerDependencies,
|
|
466
|
+
devDependencies,
|
|
467
|
+
author,
|
|
468
|
+
maintainers,
|
|
469
|
+
engines,
|
|
470
|
+
license,
|
|
471
|
+
repository,
|
|
472
|
+
keywords,
|
|
473
|
+
bugs,
|
|
474
|
+
homepage
|
|
475
|
+
};
|
|
476
|
+
const pluginId = packageJson.strapi.name;
|
|
477
|
+
const settingsService = () => ({
|
|
478
|
+
get() {
|
|
479
|
+
return strapi.config.get(`plugin::${pluginId}`);
|
|
480
|
+
},
|
|
481
|
+
set(settings) {
|
|
482
|
+
return strapi.config.set(`plugin::${pluginId}`, settings);
|
|
483
|
+
},
|
|
484
|
+
build(settings) {
|
|
485
|
+
return {
|
|
486
|
+
...settings,
|
|
487
|
+
contentTypes: settings.contentTypes.map((contentType) => ({
|
|
488
|
+
...contentType,
|
|
489
|
+
...strapi.contentTypes[contentType.uid]
|
|
490
|
+
}))
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
const { NotFoundError } = utils.errors;
|
|
495
|
+
const searchController = () => ({
|
|
496
|
+
async search(ctx) {
|
|
497
|
+
const { contentTypes } = settingsService().get();
|
|
498
|
+
const {
|
|
499
|
+
query,
|
|
500
|
+
pagination,
|
|
501
|
+
filters: filtersQuery,
|
|
502
|
+
locale,
|
|
503
|
+
populate,
|
|
504
|
+
status: statusQuery
|
|
505
|
+
} = ctx.query;
|
|
506
|
+
const { auth } = ctx.state;
|
|
507
|
+
const queriedContentTypes = filtersQuery && filtersQuery.contentTypes ? filtersQuery.contentTypes?.split(",") : void 0;
|
|
508
|
+
try {
|
|
509
|
+
await validateQueryParams(
|
|
510
|
+
ctx.query,
|
|
511
|
+
contentTypes,
|
|
512
|
+
pagination,
|
|
513
|
+
populate,
|
|
514
|
+
queriedContentTypes
|
|
515
|
+
);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
let message = "unknown error";
|
|
518
|
+
if (err instanceof Error) message = err.message;
|
|
519
|
+
return ctx.badRequest("Invalid query", message);
|
|
520
|
+
}
|
|
521
|
+
const queriedContentTypesSet = new Set(queriedContentTypes);
|
|
522
|
+
const filteredContentTypes = filtersQuery?.contentTypes ? [...contentTypes].filter(
|
|
523
|
+
(contentType) => queriedContentTypesSet.has(contentType.info.pluralName)
|
|
524
|
+
) : contentTypes;
|
|
525
|
+
const results = await Promise.all(
|
|
526
|
+
filteredContentTypes.map(
|
|
527
|
+
async (contentType) => await getResult({
|
|
528
|
+
contentType,
|
|
529
|
+
query,
|
|
530
|
+
filters: filtersQuery?.[contentType.info.pluralName],
|
|
531
|
+
populate: populate?.[contentType.info.pluralName],
|
|
532
|
+
locale: filtersQuery?.[contentType.info.pluralName]?.locale || locale,
|
|
533
|
+
status: statusQuery?.[contentType.info.pluralName] || "published"
|
|
534
|
+
})
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
const response = await buildRestResponse(
|
|
538
|
+
results,
|
|
539
|
+
auth,
|
|
540
|
+
pagination,
|
|
541
|
+
queriedContentTypes
|
|
542
|
+
);
|
|
543
|
+
if (response) {
|
|
544
|
+
return response;
|
|
545
|
+
} else {
|
|
546
|
+
throw new NotFoundError();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
const controllers = { searchController };
|
|
551
|
+
const getResolversConfig = () => {
|
|
552
|
+
return {
|
|
553
|
+
"Query.search": {
|
|
554
|
+
auth: {
|
|
555
|
+
scope: "plugin::fuzzy-search.searchController.search"
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
const getCustomTypes = (strapi2, nexus) => {
|
|
561
|
+
const { service: getService } = strapi2.plugin("graphql");
|
|
562
|
+
const { naming } = getService("utils");
|
|
563
|
+
const { utils: utils2 } = getService("builders");
|
|
564
|
+
const { contentTypes } = settingsService().get();
|
|
565
|
+
const {
|
|
566
|
+
getEntityResponseCollectionName,
|
|
567
|
+
getFindQueryName,
|
|
568
|
+
getFiltersInputTypeName
|
|
569
|
+
} = naming;
|
|
570
|
+
const { transformArgs, getContentTypeArgs } = utils2;
|
|
571
|
+
const extendSearchType = (nexus2, model) => {
|
|
572
|
+
return nexus2.extendType({
|
|
573
|
+
type: "SearchResponse",
|
|
574
|
+
definition(t) {
|
|
575
|
+
t.field(getFindQueryName(model), {
|
|
576
|
+
type: getEntityResponseCollectionName(model),
|
|
577
|
+
args: getContentTypeArgs(model, { multiple: true }),
|
|
578
|
+
async resolve(parent, args, ctx, info) {
|
|
579
|
+
const { query, locale: parentLocaleQuery } = parent;
|
|
580
|
+
const {
|
|
581
|
+
pagination,
|
|
582
|
+
filters,
|
|
583
|
+
locale: contentTypeLocaleQuery,
|
|
584
|
+
status: contentTypeStatusQuery
|
|
585
|
+
} = args;
|
|
586
|
+
const locale = contentTypeLocaleQuery || parentLocaleQuery;
|
|
587
|
+
const {
|
|
588
|
+
start: transformedStart,
|
|
589
|
+
limit: transformedLimit,
|
|
590
|
+
filters: transformedFilters
|
|
591
|
+
} = transformArgs(
|
|
592
|
+
{ pagination, filters },
|
|
593
|
+
{
|
|
594
|
+
contentType: model,
|
|
595
|
+
usePagination: true
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
const contentType = contentTypes.find(
|
|
599
|
+
(contentType2) => contentType2.modelName === model.modelName
|
|
600
|
+
);
|
|
601
|
+
if (!contentType) return;
|
|
602
|
+
const results = await getResult({
|
|
603
|
+
contentType,
|
|
604
|
+
query,
|
|
605
|
+
filters: transformedFilters,
|
|
606
|
+
populate: void 0,
|
|
607
|
+
locale,
|
|
608
|
+
status: contentTypeStatusQuery
|
|
609
|
+
});
|
|
610
|
+
const resultsResponse = await buildGraphqlResponse(
|
|
611
|
+
results.fuzzysortResults,
|
|
612
|
+
contentType,
|
|
613
|
+
ctx.state?.auth,
|
|
614
|
+
{ start: transformedStart, limit: transformedLimit }
|
|
615
|
+
);
|
|
616
|
+
if (resultsResponse) return resultsResponse;
|
|
617
|
+
throw new Error(ctx.koaContext.response.message);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
const searchResponseType = nexus.extendType({
|
|
624
|
+
type: "Query",
|
|
625
|
+
definition(t) {
|
|
626
|
+
t.field("search", {
|
|
627
|
+
type: "SearchResponse",
|
|
628
|
+
args: {
|
|
629
|
+
query: nexus.nonNull(
|
|
630
|
+
nexus.stringArg(
|
|
631
|
+
"The query string by which the models are searched"
|
|
632
|
+
)
|
|
633
|
+
),
|
|
634
|
+
locale: nexus.stringArg("The locale by which to filter the models")
|
|
635
|
+
},
|
|
636
|
+
async resolve(_parent, args, ctx) {
|
|
637
|
+
const { query, locale } = args;
|
|
638
|
+
const { auth } = ctx.state;
|
|
639
|
+
return { query, locale, auth };
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
const returnTypes = [searchResponseType];
|
|
645
|
+
contentTypes.forEach((type2) => {
|
|
646
|
+
returnTypes.unshift(extendSearchType(nexus, type2));
|
|
647
|
+
});
|
|
648
|
+
return returnTypes;
|
|
649
|
+
};
|
|
650
|
+
const registerGraphlQLQuery = (strapi2) => {
|
|
651
|
+
const extension = ({ nexus }) => ({
|
|
652
|
+
types: getCustomTypes(strapi2, nexus),
|
|
653
|
+
resolversConfig: getResolversConfig()
|
|
654
|
+
});
|
|
655
|
+
strapi2.plugin("graphql").service("extension").use(extension);
|
|
656
|
+
};
|
|
657
|
+
const register = ({ strapi: strapi2 }) => {
|
|
658
|
+
const {
|
|
659
|
+
get: getSettings,
|
|
660
|
+
build: buildSettings,
|
|
661
|
+
set: setSettings
|
|
662
|
+
} = settingsService();
|
|
663
|
+
const settings = getSettings();
|
|
664
|
+
const normalizedSettings = buildSettings(settings);
|
|
665
|
+
setSettings(normalizedSettings);
|
|
666
|
+
if (strapi2.plugin("graphql")) {
|
|
667
|
+
strapi2.log.info("[fuzzy-search] graphql detected, registering queries");
|
|
668
|
+
registerGraphlQLQuery(strapi2);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const searchRoutes = [
|
|
672
|
+
{
|
|
673
|
+
method: "GET",
|
|
674
|
+
path: "/search",
|
|
675
|
+
handler: "searchController.search"
|
|
676
|
+
}
|
|
677
|
+
];
|
|
678
|
+
const routes = {
|
|
679
|
+
"content-api": {
|
|
680
|
+
type: "content-api",
|
|
681
|
+
routes: searchRoutes
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const services = {
|
|
685
|
+
settingsService
|
|
686
|
+
};
|
|
687
|
+
const index = {
|
|
688
|
+
bootstrap,
|
|
689
|
+
register,
|
|
690
|
+
config,
|
|
691
|
+
controllers,
|
|
692
|
+
routes,
|
|
693
|
+
services
|
|
694
|
+
};
|
|
695
|
+
module.exports = index;
|