@backstage/plugin-catalog-backend 1.28.1-next.0 → 1.29.0-next.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.29.0-next.2
4
+
5
+ ### Minor Changes
6
+
7
+ - c1307b4: Implement `/entities` in terms of `queryEntities` to not run into memory and performance problems on large catalogs
8
+
9
+ ### Patch Changes
10
+
11
+ - dfc8b41: Updated dependency `@opentelemetry/api` to `^1.9.0`.
12
+ - 8013c9c: Perform the by-query count inlined with the main query
13
+ - feba9ee: Use a join based strategy for filtering, when having small page sizes
14
+ - 1fdb48e: Use a faster count method on pg when computing some metrics
15
+ - 0c33465: Implement `/entities/by-name/:kind/:namespace/:name` using `getEntitiesByRefs`
16
+ - d93390d: When parsing filters, do not make redundant `anyOf` and `allOf` nodes when there's only a single entry within them
17
+ - 24ecea8: Avoid extra ordering in by-query when the user doesn't ask for it
18
+ - Updated dependencies
19
+ - @backstage/backend-plugin-api@1.1.0-next.2
20
+ - @backstage/plugin-permission-node@0.8.6-next.2
21
+ - @backstage/backend-openapi-utils@0.4.0-next.2
22
+ - @backstage/errors@1.2.6-next.0
23
+ - @backstage/plugin-catalog-node@1.15.0-next.2
24
+ - @backstage/plugin-events-node@0.4.6-next.2
25
+ - @backstage/plugin-search-backend-module-catalog@0.2.6-next.2
26
+ - @backstage/catalog-client@1.9.0-next.2
27
+ - @backstage/catalog-model@1.7.2-next.0
28
+ - @backstage/config@1.3.1-next.0
29
+ - @backstage/integration@1.16.0-next.1
30
+ - @backstage/types@1.2.0
31
+ - @backstage/plugin-catalog-common@1.1.2-next.0
32
+ - @backstage/plugin-permission-common@0.8.3-next.0
33
+
34
+ ## 1.29.0-next.1
35
+
36
+ ### Minor Changes
37
+
38
+ - 384e494: Internal updates to generated code.
39
+ - 1d0bc11: Fetch all facets in a single database query
40
+
41
+ ### Patch Changes
42
+
43
+ - e4aab10: Fix a bug where sometimes the `by-query` endpoint could return nulls for entities that were not yet stitched.
44
+ - 5c9cc05: Use native fetch instead of node-fetch
45
+ - Updated dependencies
46
+ - @backstage/plugin-catalog-node@1.15.0-next.1
47
+ - @backstage/catalog-client@1.9.0-next.1
48
+ - @backstage/backend-plugin-api@1.1.0-next.1
49
+ - @backstage/plugin-permission-node@0.8.6-next.1
50
+ - @backstage/plugin-search-backend-module-catalog@0.2.6-next.1
51
+ - @backstage/backend-openapi-utils@0.3.1-next.1
52
+ - @backstage/catalog-model@1.7.1
53
+ - @backstage/config@1.3.0
54
+ - @backstage/errors@1.2.5
55
+ - @backstage/integration@1.16.0-next.0
56
+ - @backstage/types@1.2.0
57
+ - @backstage/plugin-catalog-common@1.1.1
58
+ - @backstage/plugin-events-node@0.4.6-next.1
59
+ - @backstage/plugin-permission-common@0.8.2
60
+
3
61
  ## 1.28.1-next.0
4
62
 
5
63
  ### Patch Changes
@@ -64,18 +64,36 @@ function initDatabaseMetrics(knex) {
64
64
  registered_locations: meter.createObservableGauge("catalog_registered_locations_count", {
65
65
  description: "Total amount of registered locations in the catalog"
66
66
  }).addCallback(async (gauge) => {
67
- const total = await knex("locations").count({
68
- count: "*"
69
- });
70
- gauge.observe(Number(total[0].count));
67
+ if (knex.client.config.client === "pg") {
68
+ const total = await knex.raw(`
69
+ SELECT reltuples::bigint AS estimate
70
+ FROM pg_class
71
+ WHERE oid = 'locations'::regclass;
72
+ `);
73
+ gauge.observe(Number(total.rows[0].estimate));
74
+ } else {
75
+ const total = await knex("locations").count({
76
+ count: "*"
77
+ });
78
+ gauge.observe(Number(total[0].count));
79
+ }
71
80
  }),
72
81
  relations: meter.createObservableGauge("catalog_relations_count", {
73
82
  description: "Total amount of relations between entities"
74
83
  }).addCallback(async (gauge) => {
75
- const total = await knex("relations").count({
76
- count: "*"
77
- });
78
- gauge.observe(Number(total[0].count));
84
+ if (knex.client.config.client === "pg") {
85
+ const total = await knex.raw(`
86
+ SELECT reltuples::bigint AS estimate
87
+ FROM pg_class
88
+ WHERE oid = 'relations'::regclass;
89
+ `);
90
+ gauge.observe(Number(total.rows[0].estimate));
91
+ } else {
92
+ const total = await knex("relations").count({
93
+ count: "*"
94
+ });
95
+ gauge.observe(Number(total[0].count));
96
+ }
79
97
  })
80
98
  };
81
99
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs.js","sources":["../../src/database/metrics.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport { createGaugeMetric } from '../util/metrics';\nimport { DbRelationsRow, DbLocationsRow, DbSearchRow } from './tables';\nimport { metrics } from '@opentelemetry/api';\n\nexport function initDatabaseMetrics(knex: Knex) {\n const seenProm = new Set<string>();\n const seen = new Set<string>();\n const meter = metrics.getMeter('default');\n return {\n entities_count_prom: createGaugeMetric({\n name: 'catalog_entities_count',\n help: 'Total amount of entities in the catalog. DEPRECATED: Please use opentelemetry metrics instead.',\n labelNames: ['kind'],\n async collect() {\n const results = await knex<DbSearchRow>('search')\n .where('key', '=', 'kind')\n .whereNotNull('value')\n .select({ kind: 'value', count: knex.raw('count(*)') })\n .groupBy('value');\n\n results.forEach(({ kind, count }) => {\n seenProm.add(kind);\n this.set({ kind }, Number(count));\n });\n\n // Set all the entities that were not seenProm to 0 and delete them from the seenProm set.\n seenProm.forEach(kind => {\n if (!results.some(r => r.kind === kind)) {\n this.set({ kind }, 0);\n seenProm.delete(kind);\n }\n });\n },\n }),\n registered_locations_prom: createGaugeMetric({\n name: 'catalog_registered_locations_count',\n help: 'Total amount of registered locations in the catalog. DEPRECATED: Please use opentelemetry metrics instead.',\n async collect() {\n const total = await knex<DbLocationsRow>('locations').count({\n count: '*',\n });\n this.set(Number(total[0].count));\n },\n }),\n relations_prom: createGaugeMetric({\n name: 'catalog_relations_count',\n help: 'Total amount of relations between entities. DEPRECATED: Please use opentelemetry metrics instead.',\n async collect() {\n const total = await knex<DbRelationsRow>('relations').count({\n count: '*',\n });\n this.set(Number(total[0].count));\n },\n }),\n entities_count: meter\n .createObservableGauge('catalog_entities_count', {\n description: 'Total amount of entities in the catalog',\n })\n .addCallback(async gauge => {\n const results = await knex<DbSearchRow>('search')\n .where('key', '=', 'kind')\n .whereNotNull('value')\n .select({ kind: 'value', count: knex.raw('count(*)') })\n .groupBy('value');\n\n results.forEach(({ kind, count }) => {\n seen.add(kind);\n gauge.observe(Number(count), { kind });\n });\n\n // Set all the entities that were not seen to 0 and delete them from the seen set.\n seen.forEach(kind => {\n if (!results.some(r => r.kind === kind)) {\n gauge.observe(0, { kind });\n seen.delete(kind);\n }\n });\n }),\n registered_locations: meter\n .createObservableGauge('catalog_registered_locations_count', {\n description: 'Total amount of registered locations in the catalog',\n })\n .addCallback(async gauge => {\n const total = await knex<DbLocationsRow>('locations').count({\n count: '*',\n });\n gauge.observe(Number(total[0].count));\n }),\n relations: meter\n .createObservableGauge('catalog_relations_count', {\n description: 'Total amount of relations between entities',\n })\n .addCallback(async gauge => {\n const total = await knex<DbRelationsRow>('relations').count({\n count: '*',\n });\n gauge.observe(Number(total[0].count));\n }),\n };\n}\n"],"names":["metrics","createGaugeMetric"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs.js","sources":["../../src/database/metrics.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport { createGaugeMetric } from '../util/metrics';\nimport { DbRelationsRow, DbLocationsRow, DbSearchRow } from './tables';\nimport { metrics } from '@opentelemetry/api';\n\nexport function initDatabaseMetrics(knex: Knex) {\n const seenProm = new Set<string>();\n const seen = new Set<string>();\n const meter = metrics.getMeter('default');\n return {\n entities_count_prom: createGaugeMetric({\n name: 'catalog_entities_count',\n help: 'Total amount of entities in the catalog. DEPRECATED: Please use opentelemetry metrics instead.',\n labelNames: ['kind'],\n async collect() {\n const results = await knex<DbSearchRow>('search')\n .where('key', '=', 'kind')\n .whereNotNull('value')\n .select({ kind: 'value', count: knex.raw('count(*)') })\n .groupBy('value');\n\n results.forEach(({ kind, count }) => {\n seenProm.add(kind);\n this.set({ kind }, Number(count));\n });\n\n // Set all the entities that were not seenProm to 0 and delete them from the seenProm set.\n seenProm.forEach(kind => {\n if (!results.some(r => r.kind === kind)) {\n this.set({ kind }, 0);\n seenProm.delete(kind);\n }\n });\n },\n }),\n registered_locations_prom: createGaugeMetric({\n name: 'catalog_registered_locations_count',\n help: 'Total amount of registered locations in the catalog. DEPRECATED: Please use opentelemetry metrics instead.',\n async collect() {\n const total = await knex<DbLocationsRow>('locations').count({\n count: '*',\n });\n this.set(Number(total[0].count));\n },\n }),\n relations_prom: createGaugeMetric({\n name: 'catalog_relations_count',\n help: 'Total amount of relations between entities. DEPRECATED: Please use opentelemetry metrics instead.',\n async collect() {\n const total = await knex<DbRelationsRow>('relations').count({\n count: '*',\n });\n this.set(Number(total[0].count));\n },\n }),\n entities_count: meter\n .createObservableGauge('catalog_entities_count', {\n description: 'Total amount of entities in the catalog',\n })\n .addCallback(async gauge => {\n const results = await knex<DbSearchRow>('search')\n .where('key', '=', 'kind')\n .whereNotNull('value')\n .select({ kind: 'value', count: knex.raw('count(*)') })\n .groupBy('value');\n\n results.forEach(({ kind, count }) => {\n seen.add(kind);\n gauge.observe(Number(count), { kind });\n });\n\n // Set all the entities that were not seen to 0 and delete them from the seen set.\n seen.forEach(kind => {\n if (!results.some(r => r.kind === kind)) {\n gauge.observe(0, { kind });\n seen.delete(kind);\n }\n });\n }),\n registered_locations: meter\n .createObservableGauge('catalog_registered_locations_count', {\n description: 'Total amount of registered locations in the catalog',\n })\n .addCallback(async gauge => {\n if (knex.client.config.client === 'pg') {\n // https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql\n const total = await knex.raw(`\n SELECT reltuples::bigint AS estimate\n FROM pg_class\n WHERE oid = 'locations'::regclass;\n `);\n gauge.observe(Number(total.rows[0].estimate));\n } else {\n const total = await knex<DbLocationsRow>('locations').count({\n count: '*',\n });\n gauge.observe(Number(total[0].count));\n }\n }),\n relations: meter\n .createObservableGauge('catalog_relations_count', {\n description: 'Total amount of relations between entities',\n })\n .addCallback(async gauge => {\n if (knex.client.config.client === 'pg') {\n // https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql\n const total = await knex.raw(`\n SELECT reltuples::bigint AS estimate\n FROM pg_class\n WHERE oid = 'relations'::regclass;\n `);\n gauge.observe(Number(total.rows[0].estimate));\n } else {\n const total = await knex<DbRelationsRow>('relations').count({\n count: '*',\n });\n gauge.observe(Number(total[0].count));\n }\n }),\n };\n}\n"],"names":["metrics","createGaugeMetric"],"mappings":"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"}
@@ -5,12 +5,9 @@ var errors = require('@backstage/errors');
5
5
  var lodash = require('lodash');
6
6
  var zod = require('zod');
7
7
  var util = require('./util.cjs.js');
8
+ var applyEntityFilterToQuery = require('./request/applyEntityFilterToQuery.cjs.js');
8
9
 
9
- const defaultSortField = {
10
- field: "metadata.uid",
11
- order: "asc"
12
- };
13
- const DEFAULT_LIMIT = 20;
10
+ const DEFAULT_LIMIT = 200;
14
11
  function parsePagination(input) {
15
12
  if (!input) {
16
13
  return {};
@@ -46,52 +43,6 @@ function stringifyPagination(input) {
46
43
  const base64 = Buffer.from(json, "utf8").toString("base64");
47
44
  return base64;
48
45
  }
49
- function addCondition(queryBuilder, db, filter, negate = false, entityIdField = "entity_id") {
50
- const key = filter.key.toLowerCase();
51
- const values = filter.values?.map((v) => v.toLowerCase());
52
- const matchQuery = db("search").select("search.entity_id").where({ key }).andWhere(function keyFilter() {
53
- if (values?.length === 1) {
54
- this.where({ value: values.at(0) });
55
- } else if (values) {
56
- this.andWhere("value", "in", values);
57
- }
58
- });
59
- queryBuilder.andWhere(entityIdField, negate ? "not in" : "in", matchQuery);
60
- }
61
- function isEntitiesSearchFilter(filter) {
62
- return filter.hasOwnProperty("key");
63
- }
64
- function isOrEntityFilter(filter) {
65
- return filter.hasOwnProperty("anyOf");
66
- }
67
- function isNegationEntityFilter(filter) {
68
- return filter.hasOwnProperty("not");
69
- }
70
- function parseFilter(filter, query, db, negate = false, entityIdField = "entity_id") {
71
- if (isNegationEntityFilter(filter)) {
72
- return parseFilter(filter.not, query, db, !negate, entityIdField);
73
- }
74
- if (isEntitiesSearchFilter(filter)) {
75
- return query.andWhere(function filterFunction() {
76
- addCondition(this, db, filter, negate, entityIdField);
77
- });
78
- }
79
- return query[negate ? "andWhereNot" : "andWhere"](function filterFunction() {
80
- if (isOrEntityFilter(filter)) {
81
- for (const subFilter of filter.anyOf ?? []) {
82
- this.orWhere(
83
- (subQuery) => parseFilter(subFilter, subQuery, db, false, entityIdField)
84
- );
85
- }
86
- } else {
87
- for (const subFilter of filter.allOf ?? []) {
88
- this.andWhere(
89
- (subQuery) => parseFilter(subFilter, subQuery, db, false, entityIdField)
90
- );
91
- }
92
- }
93
- });
94
- }
95
46
  class DefaultEntitiesCatalog {
96
47
  database;
97
48
  logger;
@@ -103,6 +54,7 @@ class DefaultEntitiesCatalog {
103
54
  }
104
55
  async entities(request) {
105
56
  const db = this.database;
57
+ const { limit, offset } = parsePagination(request?.pagination);
106
58
  let entitiesQuery = db("final_entities").select("final_entities.*");
107
59
  request?.order?.forEach(({ field }, index) => {
108
60
  const alias = `order_${index}`;
@@ -115,13 +67,13 @@ class DefaultEntitiesCatalog {
115
67
  });
116
68
  entitiesQuery = entitiesQuery.whereNotNull("final_entities.final_entity");
117
69
  if (request?.filter) {
118
- entitiesQuery = parseFilter(
119
- request.filter,
120
- entitiesQuery,
121
- db,
122
- false,
123
- "final_entities.entity_id"
124
- );
70
+ entitiesQuery = applyEntityFilterToQuery.applyEntityFilterToQuery({
71
+ filter: request.filter,
72
+ targetQuery: entitiesQuery,
73
+ onEntityIdField: "final_entities.entity_id",
74
+ knex: db,
75
+ strategy: limit !== void 0 && limit <= 500 ? "join" : "in"
76
+ });
125
77
  }
126
78
  request?.order?.forEach(({ order }, index) => {
127
79
  if (db.client.config.client === "pg") {
@@ -140,7 +92,6 @@ class DefaultEntitiesCatalog {
140
92
  } else {
141
93
  entitiesQuery.orderBy("final_entities.entity_id", "asc");
142
94
  }
143
- const { limit, offset } = parsePagination(request?.pagination);
144
95
  if (limit !== void 0) {
145
96
  entitiesQuery = entitiesQuery.limit(limit + 1);
146
97
  }
@@ -165,17 +116,7 @@ class DefaultEntitiesCatalog {
165
116
  if (request?.fields) {
166
117
  entities = entities.map((e) => request.fields(e));
167
118
  }
168
- for (const entity of entities) {
169
- if (entity.relations) {
170
- for (const relation of entity.relations) {
171
- if (!relation.targetRef && relation.target) {
172
- relation.targetRef = catalogModel.stringifyEntityRef(relation.target);
173
- } else if (!relation.target && relation.targetRef) {
174
- relation.target = catalogModel.parseEntityRef(relation.targetRef);
175
- }
176
- }
177
- }
178
- }
119
+ util.expandLegacyCompoundRelationRefsInResponse(entities);
179
120
  return {
180
121
  entities,
181
122
  pageInfo
@@ -189,13 +130,12 @@ class DefaultEntitiesCatalog {
189
130
  entity: "final_entities.final_entity"
190
131
  }).whereIn("final_entities.entity_ref", chunk);
191
132
  if (request?.filter) {
192
- query = parseFilter(
193
- request.filter,
194
- query,
195
- this.database,
196
- false,
197
- "final_entities.entity_id"
198
- );
133
+ query = applyEntityFilterToQuery.applyEntityFilterToQuery({
134
+ filter: request.filter,
135
+ targetQuery: query,
136
+ onEntityIdField: "final_entities.entity_id",
137
+ knex: this.database
138
+ });
199
139
  }
200
140
  for (const row of await query) {
201
141
  lookup.set(row.entityRef, row.entity ? JSON.parse(row.entity) : null);
@@ -208,97 +148,137 @@ class DefaultEntitiesCatalog {
208
148
  return { items };
209
149
  }
210
150
  async queryEntities(request) {
211
- const db = this.database;
212
151
  const limit = request.limit ?? DEFAULT_LIMIT;
213
152
  const cursor = {
214
- orderFields: [defaultSortField],
153
+ orderFields: [],
215
154
  isPrevious: false,
216
155
  ...parseCursorFromRequest(request)
217
156
  };
157
+ const shouldComputeTotalItems = cursor.totalItems === void 0 && !cursor.skipTotalItems;
218
158
  const isFetchingBackwards = cursor.isPrevious;
219
159
  if (cursor.orderFields.length > 1) {
220
160
  this.logger.warn(`Only one sort field is supported, ignoring the rest`);
221
161
  }
222
- const sortField = {
223
- ...defaultSortField,
224
- ...cursor.orderFields[0]
225
- };
226
- const [prevItemOrderFieldValue, prevItemUid] = cursor.orderFieldValues || [];
227
- const dbQuery = db("final_entities").leftOuterJoin(
228
- "search",
229
- (qb) => qb.on("search.entity_id", "final_entities.entity_id").andOnVal("search.key", sortField.field)
162
+ const sortField = cursor.orderFields.at(0);
163
+ const dbQuery = this.database.with(
164
+ "filtered",
165
+ ["entity_id", "final_entity", ...sortField ? ["value"] : []],
166
+ (inner) => {
167
+ inner.from("final_entities").whereNotNull("final_entity");
168
+ if (sortField) {
169
+ inner.leftOuterJoin(
170
+ "search",
171
+ (qb) => qb.on("search.entity_id", "final_entities.entity_id").andOnVal("search.key", sortField.field)
172
+ ).select({
173
+ entity_id: "final_entities.entity_id",
174
+ final_entity: "final_entities.final_entity",
175
+ value: "search.value"
176
+ });
177
+ } else {
178
+ inner.select({
179
+ entity_id: "final_entities.entity_id",
180
+ final_entity: "final_entities.final_entity"
181
+ });
182
+ }
183
+ if (cursor.filter) {
184
+ applyEntityFilterToQuery.applyEntityFilterToQuery({
185
+ filter: cursor.filter,
186
+ targetQuery: inner,
187
+ onEntityIdField: "final_entities.entity_id",
188
+ knex: this.database,
189
+ strategy: limit <= 500 ? "join" : "in"
190
+ });
191
+ }
192
+ const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim();
193
+ const textFilterFields = cursor.fullTextFilter?.fields ?? [
194
+ sortField?.field || "metadata.uid"
195
+ ];
196
+ if (normalizedFullTextFilterTerm) {
197
+ if (textFilterFields.length === 1 && textFilterFields[0] === sortField?.field) {
198
+ inner.andWhereRaw(
199
+ "search.value like ?",
200
+ `%${normalizedFullTextFilterTerm.toLocaleLowerCase("en-US")}%`
201
+ );
202
+ } else {
203
+ const matchQuery = this.database("search").select("search.entity_id").whereIn(
204
+ "search.key",
205
+ textFilterFields.map((field) => field.toLocaleLowerCase("en-US"))
206
+ ).andWhere(function keyFilter() {
207
+ this.andWhereRaw(
208
+ "search.value like ?",
209
+ `%${normalizedFullTextFilterTerm.toLocaleLowerCase(
210
+ "en-US"
211
+ )}%`
212
+ );
213
+ });
214
+ inner.andWhere("final_entities.entity_id", "in", matchQuery);
215
+ }
216
+ }
217
+ }
230
218
  );
231
- if (cursor.filter) {
232
- parseFilter(
233
- cursor.filter,
234
- dbQuery,
235
- db,
236
- false,
237
- "final_entities.entity_id"
238
- );
219
+ if (shouldComputeTotalItems) {
220
+ dbQuery.with(
221
+ "filtered_count",
222
+ ["count"],
223
+ (inner) => inner.from("filtered").count("*", { as: "count" })
224
+ ).fromRaw("filtered_count, filtered").select("count", "filtered.*");
225
+ } else {
226
+ dbQuery.from("filtered").select("*");
239
227
  }
240
- const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim();
241
- const textFilterFields = cursor.fullTextFilter?.fields ?? [sortField.field];
242
- if (normalizedFullTextFilterTerm) {
243
- if (textFilterFields.length === 1 && textFilterFields[0] === sortField.field) {
244
- dbQuery.andWhereRaw(
245
- "value like ?",
246
- `%${normalizedFullTextFilterTerm.toLocaleLowerCase("en-US")}%`
247
- );
248
- } else {
249
- const matchQuery = db("search").select("search.entity_id").whereIn(
250
- "key",
251
- textFilterFields.map((field) => field.toLocaleLowerCase("en-US"))
252
- ).andWhere(function keyFilter() {
253
- this.andWhereRaw(
254
- "value like ?",
255
- `%${normalizedFullTextFilterTerm.toLocaleLowerCase("en-US")}%`
228
+ const isOrderingDescending = sortField?.order === "desc";
229
+ if (cursor.orderFieldValues) {
230
+ if (cursor.orderFieldValues.length === 2) {
231
+ const [first, second] = cursor.orderFieldValues;
232
+ dbQuery.andWhere(function nested() {
233
+ this.where(
234
+ "filtered.value",
235
+ isFetchingBackwards !== isOrderingDescending ? "<" : ">",
236
+ first
237
+ ).orWhere("filtered.value", "=", first).andWhere(
238
+ "filtered.entity_id",
239
+ isFetchingBackwards !== isOrderingDescending ? "<" : ">",
240
+ second
256
241
  );
257
242
  });
258
- dbQuery.andWhere("final_entities.entity_id", "in", matchQuery);
243
+ } else if (cursor.orderFieldValues.length === 1) {
244
+ const [first] = cursor.orderFieldValues;
245
+ dbQuery.andWhere("entity_id", isFetchingBackwards ? "<" : ">", first);
259
246
  }
260
247
  }
261
- const countQuery = dbQuery.clone();
262
- const isOrderingDescending = sortField.order === "desc";
263
- if (prevItemOrderFieldValue) {
264
- dbQuery.andWhere(function nested() {
265
- this.where(
266
- "value",
267
- isFetchingBackwards !== isOrderingDescending ? "<" : ">",
268
- prevItemOrderFieldValue
269
- ).orWhere("value", "=", prevItemOrderFieldValue).andWhere(
270
- "final_entities.entity_id",
271
- isFetchingBackwards !== isOrderingDescending ? "<" : ">",
272
- prevItemUid
273
- );
274
- });
248
+ let order = sortField?.order ?? "asc";
249
+ if (isFetchingBackwards) {
250
+ order = invertOrder(order);
275
251
  }
276
- if (db.client.config.client === "pg") {
252
+ if (this.database.client.config.client === "pg") {
277
253
  dbQuery.orderBy([
254
+ ...sortField ? [
255
+ {
256
+ column: "filtered.value",
257
+ order,
258
+ nulls: "last"
259
+ }
260
+ ] : [],
278
261
  {
279
- column: "search.value",
280
- order: isFetchingBackwards ? invertOrder(sortField.order) : sortField.order,
281
- nulls: "last"
282
- },
283
- {
284
- column: "final_entities.entity_id",
285
- order: isFetchingBackwards ? invertOrder(sortField.order) : sortField.order
262
+ column: "filtered.entity_id",
263
+ order
286
264
  }
287
265
  ]);
288
266
  } else {
289
267
  dbQuery.orderBy([
268
+ ...sortField ? [
269
+ {
270
+ column: "filtered.value",
271
+ order: void 0,
272
+ nulls: "last"
273
+ },
274
+ {
275
+ column: "filtered.value",
276
+ order
277
+ }
278
+ ] : [],
290
279
  {
291
- column: "search.value",
292
- order: void 0,
293
- nulls: "last"
294
- },
295
- {
296
- column: "search.value",
297
- order: isFetchingBackwards ? invertOrder(sortField.order) : sortField.order
298
- },
299
- {
300
- column: "final_entities.entity_id",
301
- order: isFetchingBackwards ? invertOrder(sortField.order) : sortField.order
280
+ column: "filtered.entity_id",
281
+ order
302
282
  }
303
283
  ]);
304
284
  }
@@ -306,16 +286,17 @@ class DefaultEntitiesCatalog {
306
286
  dbQuery.offset(request.offset);
307
287
  }
308
288
  dbQuery.limit(isFetchingBackwards ? limit : limit + 1);
309
- countQuery.count("final_entities.entity_id", { as: "count" });
310
- const [rows, [{ count }]] = await Promise.all([
311
- limit > 0 ? dbQuery : [],
312
- // for performance reasons we invoke the countQuery
313
- // only on the first request.
314
- // The result is then embedded into the cursor
315
- // for subsequent requests.
316
- typeof cursor.totalItems === "undefined" ? countQuery : [{ count: cursor.totalItems }]
317
- ]);
318
- const totalItems = Number(count);
289
+ const rows = shouldComputeTotalItems || limit > 0 ? await dbQuery : [];
290
+ let totalItems;
291
+ if (cursor.totalItems !== void 0) {
292
+ totalItems = cursor.totalItems;
293
+ } else if (cursor.skipTotalItems) {
294
+ totalItems = 0;
295
+ } else if (rows.length) {
296
+ totalItems = Number(rows[0].count);
297
+ } else {
298
+ totalItems = 0;
299
+ }
319
300
  if (isFetchingBackwards) {
320
301
  rows.reverse();
321
302
  }
@@ -326,20 +307,20 @@ class DefaultEntitiesCatalog {
326
307
  const isInitialRequest = cursor.firstSortFieldValues === void 0;
327
308
  const firstRow = rows[0];
328
309
  const lastRow = rows[rows.length - 1];
329
- const firstSortFieldValues = cursor.firstSortFieldValues || [
330
- firstRow?.value,
331
- firstRow?.entity_id
332
- ];
310
+ const firstSortFieldValues = cursor.firstSortFieldValues || sortFieldsFromRow(firstRow, sortField);
333
311
  const nextCursor = hasMoreResults ? {
334
312
  ...cursor,
335
- orderFieldValues: sortFieldsFromRow(lastRow),
313
+ orderFieldValues: sortFieldsFromRow(lastRow, sortField),
336
314
  firstSortFieldValues,
337
315
  isPrevious: false,
338
316
  totalItems
339
317
  } : void 0;
340
- const prevCursor = !isInitialRequest && rows.length > 0 && !lodash.isEqual(sortFieldsFromRow(firstRow), cursor.firstSortFieldValues) ? {
318
+ const prevCursor = !isInitialRequest && rows.length > 0 && !lodash.isEqual(
319
+ sortFieldsFromRow(firstRow, sortField),
320
+ cursor.firstSortFieldValues
321
+ ) ? {
341
322
  ...cursor,
342
- orderFieldValues: sortFieldsFromRow(firstRow),
323
+ orderFieldValues: sortFieldsFromRow(firstRow, sortField),
343
324
  firstSortFieldValues: cursor.firstSortFieldValues,
344
325
  isPrevious: true,
345
326
  totalItems
@@ -438,17 +419,29 @@ class DefaultEntitiesCatalog {
438
419
  };
439
420
  }
440
421
  async facets(request) {
422
+ const query = this.database("search").whereIn(
423
+ "search.key",
424
+ request.facets.map((f) => f.toLocaleLowerCase("en-US"))
425
+ ).whereNotNull("search.original_value").select({
426
+ facet: "search.key",
427
+ value: "search.original_value",
428
+ count: this.database.raw("count(*)")
429
+ }).groupBy(["search.key", "search.original_value"]);
430
+ if (request.filter) {
431
+ applyEntityFilterToQuery.applyEntityFilterToQuery({
432
+ filter: request.filter,
433
+ targetQuery: query,
434
+ onEntityIdField: "search.entity_id",
435
+ knex: this.database
436
+ });
437
+ }
438
+ const rows = await query;
441
439
  const facets = {};
442
- const db = this.database;
443
440
  for (const facet of request.facets) {
444
- const dbQuery = db("search").where("search.key", facet.toLocaleLowerCase("en-US")).whereNotNull("search.original_value").select({ value: "search.original_value", count: db.raw("count(*)") }).groupBy("search.original_value");
445
- if (request?.filter) {
446
- parseFilter(request.filter, dbQuery, db, false, "search.entity_id");
447
- }
448
- const result = await dbQuery;
449
- facets[facet] = result.map((data) => ({
450
- value: String(data.value),
451
- count: Number(data.count)
441
+ const facetLowercase = facet.toLocaleLowerCase("en-US");
442
+ facets[facet] = rows.filter((row) => row.facet === facetLowercase).map((row) => ({
443
+ value: String(row.value),
444
+ count: Number(row.count)
452
445
  }));
453
446
  }
454
447
  return { facets };
@@ -475,21 +468,28 @@ function parseCursorFromRequest(request) {
475
468
  if (util.isQueryEntitiesInitialRequest(request)) {
476
469
  const {
477
470
  filter,
478
- orderFields: sortFields = [defaultSortField],
479
- fullTextFilter
471
+ orderFields: sortFields = [],
472
+ fullTextFilter,
473
+ skipTotalItems = false
480
474
  } = request;
481
- return { filter, orderFields: sortFields, fullTextFilter };
475
+ return { filter, orderFields: sortFields, fullTextFilter, skipTotalItems };
482
476
  }
483
477
  if (util.isQueryEntitiesCursorRequest(request)) {
484
- return request.cursor;
478
+ return {
479
+ ...request.cursor,
480
+ // Doesn't matter here
481
+ skipTotalItems: false
482
+ };
485
483
  }
486
- return {};
484
+ return {
485
+ skipTotalItems: false
486
+ };
487
487
  }
488
488
  function invertOrder(order) {
489
489
  return order === "asc" ? "desc" : "asc";
490
490
  }
491
- function sortFieldsFromRow(row) {
492
- return [row.value, row.entity_id];
491
+ function sortFieldsFromRow(row, sortField) {
492
+ return sortField ? [row?.value, row?.entity_id] : [row?.entity_id];
493
493
  }
494
494
 
495
495
  exports.DefaultEntitiesCatalog = DefaultEntitiesCatalog;