@backstage/plugin-catalog-backend 1.29.0-next.1 → 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,36 @@
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
+
3
34
  ## 1.29.0-next.1
4
35
 
5
36
  ### Minor 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)
230
- ).whereNotNull("final_entities.final_entity");
231
- if (cursor.filter) {
232
- parseFilter(
233
- cursor.filter,
234
- dbQuery,
235
- db,
236
- false,
237
- "final_entities.entity_id"
238
- );
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
+ }
218
+ );
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
@@ -447,13 +428,12 @@ class DefaultEntitiesCatalog {
447
428
  count: this.database.raw("count(*)")
448
429
  }).groupBy(["search.key", "search.original_value"]);
449
430
  if (request.filter) {
450
- parseFilter(
451
- request.filter,
452
- query,
453
- this.database,
454
- false,
455
- "search.entity_id"
456
- );
431
+ applyEntityFilterToQuery.applyEntityFilterToQuery({
432
+ filter: request.filter,
433
+ targetQuery: query,
434
+ onEntityIdField: "search.entity_id",
435
+ knex: this.database
436
+ });
457
437
  }
458
438
  const rows = await query;
459
439
  const facets = {};
@@ -488,21 +468,28 @@ function parseCursorFromRequest(request) {
488
468
  if (util.isQueryEntitiesInitialRequest(request)) {
489
469
  const {
490
470
  filter,
491
- orderFields: sortFields = [defaultSortField],
492
- fullTextFilter
471
+ orderFields: sortFields = [],
472
+ fullTextFilter,
473
+ skipTotalItems = false
493
474
  } = request;
494
- return { filter, orderFields: sortFields, fullTextFilter };
475
+ return { filter, orderFields: sortFields, fullTextFilter, skipTotalItems };
495
476
  }
496
477
  if (util.isQueryEntitiesCursorRequest(request)) {
497
- return request.cursor;
478
+ return {
479
+ ...request.cursor,
480
+ // Doesn't matter here
481
+ skipTotalItems: false
482
+ };
498
483
  }
499
- return {};
484
+ return {
485
+ skipTotalItems: false
486
+ };
500
487
  }
501
488
  function invertOrder(order) {
502
489
  return order === "asc" ? "desc" : "asc";
503
490
  }
504
- function sortFieldsFromRow(row) {
505
- return [row.value, row.entity_id];
491
+ function sortFieldsFromRow(row, sortField) {
492
+ return sortField ? [row?.value, row?.entity_id] : [row?.entity_id];
506
493
  }
507
494
 
508
495
  exports.DefaultEntitiesCatalog = DefaultEntitiesCatalog;