@porulle/adapter-pg-search 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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAKnB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,sBAAsB,EAAE,CAAA;KAAE,CAAC,CAAC;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4ID,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,aAAa,CA8J9E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAKnB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,sBAAsB,EAAE,CAAA;KAAE,CAAC,CAAC;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyMD,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,aAAa,CAgK9E"}
package/dist/src/index.js CHANGED
@@ -39,7 +39,37 @@ function parseBrands(value) {
39
39
  }
40
40
  return [];
41
41
  }
42
+ function parseAttributes(value) {
43
+ let parsed = value;
44
+ if (typeof value === "string") {
45
+ try {
46
+ parsed = JSON.parse(value);
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
53
+ return {};
54
+ const attributes = {};
55
+ for (const [key, entry] of Object.entries(parsed)) {
56
+ if (typeof entry === "string") {
57
+ attributes[key] = entry;
58
+ }
59
+ else if (Array.isArray(entry) && entry.every((item) => typeof item === "string")) {
60
+ attributes[key] = entry;
61
+ }
62
+ }
63
+ return attributes;
64
+ }
65
+ function attributeValues(document, name) {
66
+ const value = document.attributes?.[name];
67
+ if (value == null)
68
+ return [];
69
+ return Array.isArray(value) ? value : [value];
70
+ }
42
71
  function toDocument(row) {
72
+ const attributes = parseAttributes(row.attributes);
43
73
  return {
44
74
  id: String(row.id ?? ""),
45
75
  type: String(row.type ?? ""),
@@ -50,6 +80,7 @@ function toDocument(row) {
50
80
  categories: parseCategories(row.categories),
51
81
  brands: parseBrands(row.brands),
52
82
  text: String(row.text ?? ""),
83
+ ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
53
84
  ...(row.payload && typeof row.payload === "object" ? { payload: row.payload } : {}),
54
85
  };
55
86
  }
@@ -76,6 +107,20 @@ function buildWhere(params, dictionary) {
76
107
  values.push(params.filters.brand);
77
108
  clauses.push(`$${values.length} = ANY(brands)`);
78
109
  }
110
+ for (const [name, requested] of Object.entries(params.filters?.attributes ?? {})) {
111
+ values.push(name);
112
+ const keyPlaceholder = `$${values.length}`;
113
+ const requestedValues = Array.isArray(requested) ? requested : [requested];
114
+ if (requestedValues.length === 0) {
115
+ clauses.push("FALSE");
116
+ continue;
117
+ }
118
+ const valuePlaceholders = requestedValues.map((value) => {
119
+ values.push(value);
120
+ return `$${values.length}`;
121
+ });
122
+ clauses.push(`EXISTS (SELECT 1 FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(attributes -> ${keyPlaceholder}) = 'array' THEN attributes -> ${keyPlaceholder} ELSE jsonb_build_array(attributes ->> ${keyPlaceholder}) END) AS attribute_value WHERE attribute_value IN (${valuePlaceholders.join(", ")}))`);
123
+ }
79
124
  return {
80
125
  sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
81
126
  values,
@@ -113,6 +158,21 @@ function computeFacets(documents, requested) {
113
158
  }
114
159
  }
115
160
  }
161
+ for (const facet of facets) {
162
+ const attributeName = facet.startsWith("attributes.")
163
+ ? facet.slice("attributes.".length)
164
+ : facet;
165
+ if (["type", "status", "category", "categories", "brand", "brands"].includes(attributeName))
166
+ continue;
167
+ const values = {};
168
+ for (const document of documents) {
169
+ for (const value of new Set(attributeValues(document, attributeName))) {
170
+ values[value] = (values[value] ?? 0) + 1;
171
+ }
172
+ }
173
+ if (Object.keys(values).length > 0)
174
+ output[attributeName] = values;
175
+ }
116
176
  return output;
117
177
  }
118
178
  export function pgSearchAdapter(options) {
@@ -125,8 +185,8 @@ export function pgSearchAdapter(options) {
125
185
  if (documents.length === 0)
126
186
  return Ok(undefined);
127
187
  for (const document of documents) {
128
- await options.query(`INSERT INTO ${table} (id, type, slug, title, description, status, categories, brands, text, payload)
129
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)
188
+ await options.query(`INSERT INTO ${table} (id, type, slug, title, description, status, categories, brands, text, attributes, payload)
189
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)
130
190
  ON CONFLICT (id)
131
191
  DO UPDATE SET
132
192
  type = EXCLUDED.type,
@@ -137,6 +197,7 @@ export function pgSearchAdapter(options) {
137
197
  categories = EXCLUDED.categories,
138
198
  brands = EXCLUDED.brands,
139
199
  text = EXCLUDED.text,
200
+ attributes = EXCLUDED.attributes,
140
201
  payload = EXCLUDED.payload`, [
141
202
  document.id,
142
203
  document.type,
@@ -147,6 +208,7 @@ export function pgSearchAdapter(options) {
147
208
  document.categories,
148
209
  document.brands,
149
210
  document.text,
211
+ JSON.stringify(document.attributes ?? {}),
150
212
  JSON.stringify(document.payload ?? {}),
151
213
  ]);
152
214
  }
@@ -182,14 +244,14 @@ export function pgSearchAdapter(options) {
182
244
  const scoreExpr = params.query.trim().length > 0
183
245
  ? `ts_rank(to_tsvector('${dictionary}', text), plainto_tsquery('${dictionary}', $1))`
184
246
  : "0";
185
- const rows = await options.query(`SELECT id, type, slug, title, description, status, categories, brands, text, payload, ${scoreExpr} AS score
247
+ const rows = await options.query(`SELECT id, type, slug, title, description, status, categories, brands, text, attributes, payload, ${scoreExpr} AS score
186
248
  FROM ${table}
187
249
  ${where.sql}
188
250
  ORDER BY score DESC, title ASC
189
251
  LIMIT $${where.values.length + 1}
190
252
  OFFSET $${where.values.length + 2}`, [...where.values, limit, offset]);
191
253
  const countRows = await options.query(`SELECT COUNT(*)::int AS total FROM ${table} ${where.sql}`, where.values);
192
- const facetRows = await options.query(`SELECT id, type, slug, title, description, status, categories, brands, text, payload
254
+ const facetRows = await options.query(`SELECT id, type, slug, title, description, status, categories, brands, text, attributes, payload
193
255
  FROM ${table}
194
256
  ${where.sql}`, where.values);
195
257
  const documents = facetRows.rows.map((row) => toDocument(row));