@porulle/adapter-meilisearch 0.10.8 → 0.11.0

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.
@@ -16,6 +16,7 @@ interface MeiliIndexLike {
16
16
  facetDistribution?: Record<string, Record<string, number>>;
17
17
  }>;
18
18
  updateFilterableAttributes(attributes: string[]): Promise<unknown>;
19
+ getFilterableAttributes?(): Promise<unknown>;
19
20
  }
20
21
  interface MeiliClientLike {
21
22
  index(uid: string): MeiliIndexLike;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,cAAc,EAA4E,MAAM,eAAe,CAAC;AAExK,UAAU,cAAc;IACtB,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,GACA,OAAO,CAAC;QACT,IAAI,EAAE,KAAK,CAAC,cAAc,GAAG;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KAC5D,CAAC,CAAC;IACH,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAgDD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAgGpF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,cAAc,EAA4E,MAAM,eAAe,CAAC;AAExK,UAAU,cAAc;IACtB,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,GACA,OAAO,CAAC;QACT,IAAI,EAAE,KAAK,CAAC,cAAc,GAAG;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KAC5D,CAAC,CAAC;IACH,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,uBAAuB,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAgFD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAmHpF"}
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { MeiliSearch } from "meilisearch";
2
2
  import { Err, Ok } from "@porulle/core";
3
+ const SAFE_ATTRIBUTE_NAME = /^[A-Za-z0-9_-]+$/;
3
4
  function toFilter(params) {
4
5
  const filters = [];
5
6
  if (params.filters?.type) {
@@ -14,8 +15,33 @@ function toFilter(params) {
14
15
  if (params.filters?.brand) {
15
16
  filters.push(`brands = \"${params.filters.brand}\"`);
16
17
  }
18
+ for (const [name, requested] of Object.entries(params.filters?.attributes ?? {})) {
19
+ const values = Array.isArray(requested) ? requested : [requested];
20
+ if (values.length === 0 || !SAFE_ATTRIBUTE_NAME.test(name)) {
21
+ filters.push('id = ""');
22
+ continue;
23
+ }
24
+ const expressions = values.map((value) => `attributes.${name} = ${JSON.stringify(value)}`);
25
+ if (expressions.length > 1) {
26
+ filters.push(`(${expressions.join(" OR ")})`);
27
+ }
28
+ else if (expressions.length === 1) {
29
+ filters.push(expressions[0]);
30
+ }
31
+ }
17
32
  return filters;
18
33
  }
34
+ function normalizeFacets(facets) {
35
+ return Object.fromEntries(Object.entries(facets).map(([name, values]) => [
36
+ name.startsWith("attributes.") ? name.slice("attributes.".length) : name,
37
+ values,
38
+ ]));
39
+ }
40
+ function toFacetName(name) {
41
+ if (["type", "status", "category", "categories", "brand", "brands"].includes(name))
42
+ return name;
43
+ return name.startsWith("attributes.") ? name : `attributes.${name}`;
44
+ }
19
45
  function normalizeSearchResult(params, result) {
20
46
  const page = params.page ?? 1;
21
47
  const limit = params.limit ?? 20;
@@ -28,19 +54,37 @@ function normalizeSearchResult(params, result) {
28
54
  total: result.estimatedTotalHits ?? result.hits.length,
29
55
  page,
30
56
  limit,
31
- facets: result.facetDistribution ?? {},
57
+ facets: normalizeFacets(result.facetDistribution ?? {}),
32
58
  };
33
59
  }
34
60
  export function meilisearchAdapter(options) {
35
61
  const client = options.client ?? new MeiliSearch({ host: options.host, ...(options.apiKey ? { apiKey: options.apiKey } : {}) });
36
62
  const indexName = options.indexName ?? "catalog";
37
63
  const filterable = options.filterableAttributes ?? ["type", "status", "categories", "brands"];
38
- let filterableConfigured = false;
39
- async function ensureFilterable(index) {
40
- if (filterableConfigured)
64
+ const attributeNames = new Set();
65
+ let configuredFilterable;
66
+ async function ensureFilterable(index, documents) {
67
+ for (const document of documents) {
68
+ for (const name of Object.keys(document.attributes ?? {})) {
69
+ if (SAFE_ATTRIBUTE_NAME.test(name))
70
+ attributeNames.add(name);
71
+ }
72
+ }
73
+ if (configuredFilterable === undefined && index.getFilterableAttributes) {
74
+ const stored = await index.getFilterableAttributes();
75
+ configuredFilterable = Array.isArray(stored)
76
+ ? stored.filter((name) => typeof name === "string")
77
+ : [];
78
+ }
79
+ const nextFilterable = [
80
+ ...(configuredFilterable ?? []),
81
+ ...filterable,
82
+ ...[...attributeNames].map((name) => `attributes.${name}`),
83
+ ].filter((name, position, list) => list.indexOf(name) === position);
84
+ if (configuredFilterable?.length === nextFilterable.length && configuredFilterable.every((name, index) => name === nextFilterable[index]))
41
85
  return;
42
- await index.updateFilterableAttributes(filterable);
43
- filterableConfigured = true;
86
+ await index.updateFilterableAttributes(nextFilterable);
87
+ configuredFilterable = nextFilterable;
44
88
  }
45
89
  return {
46
90
  providerId: "meilisearch",
@@ -49,7 +93,7 @@ export function meilisearchAdapter(options) {
49
93
  if (documents.length === 0)
50
94
  return Ok(undefined);
51
95
  const index = client.index(indexName);
52
- await ensureFilterable(index);
96
+ await ensureFilterable(index, documents);
53
97
  await index.addDocuments(documents);
54
98
  return Ok(undefined);
55
99
  }
@@ -82,9 +126,11 @@ export function meilisearchAdapter(options) {
82
126
  const offset = (page - 1) * limit;
83
127
  const filters = toFilter(params);
84
128
  const index = client.index(indexName);
129
+ const requestedFacets = (params.facets ?? ["type", "status", "categories", "brands"]).map(toFacetName);
130
+ const facets = requestedFacets.filter((name, position, list) => list.indexOf(name) === position);
85
131
  const result = await index.search(params.query, {
86
132
  ...(filters.length > 0 ? { filter: filters } : {}),
87
- facets: params.facets ?? ["type", "status", "categories", "brands"],
133
+ facets,
88
134
  limit,
89
135
  offset,
90
136
  });