@directus/api 26.0.1 → 27.0.1

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.
Files changed (90) hide show
  1. package/dist/cli/utils/create-db-connection.js +3 -5
  2. package/dist/controllers/files.js +1 -1
  3. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +2 -2
  4. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  5. package/dist/database/get-ast-from-query/utils/get-related-collection.js +2 -2
  6. package/dist/database/helpers/fn/types.js +2 -1
  7. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -0
  8. package/dist/database/helpers/schema/dialects/mssql.js +6 -0
  9. package/dist/database/helpers/schema/dialects/mysql.d.ts +0 -1
  10. package/dist/database/helpers/schema/dialects/mysql.js +0 -11
  11. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -0
  12. package/dist/database/helpers/schema/dialects/oracle.js +6 -0
  13. package/dist/database/helpers/schema/types.d.ts +2 -1
  14. package/dist/database/helpers/schema/types.js +6 -4
  15. package/dist/database/index.d.ts +0 -6
  16. package/dist/database/index.js +12 -28
  17. package/dist/database/migrations/20210225A-add-relations-sort-field.js +3 -1
  18. package/dist/database/migrations/20210510A-restructure-relations.js +8 -8
  19. package/dist/database/migrations/20210924A-add-collection-organization.js +6 -1
  20. package/dist/database/migrations/20210927A-replace-fields-group.js +3 -1
  21. package/dist/database/migrations/20211118A-add-notifications.js +3 -1
  22. package/dist/database/migrations/20211211A-add-shares.js +7 -1
  23. package/dist/database/migrations/20230823A-add-content-versioning.js +3 -1
  24. package/dist/database/migrations/20240909A-separate-comments.js +3 -1
  25. package/dist/database/run-ast/lib/apply-query/add-join.d.ts +54 -0
  26. package/dist/database/run-ast/lib/apply-query/add-join.js +86 -0
  27. package/dist/database/run-ast/lib/apply-query/aggregate.d.ts +3 -0
  28. package/dist/database/run-ast/lib/apply-query/aggregate.js +24 -0
  29. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +8 -0
  30. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +20 -0
  31. package/dist/database/run-ast/lib/apply-query/filter/index.d.ts +8 -0
  32. package/dist/database/run-ast/lib/apply-query/filter/index.js +151 -0
  33. package/dist/database/run-ast/lib/apply-query/filter/operator.d.ts +3 -0
  34. package/dist/database/run-ast/lib/apply-query/filter/operator.js +175 -0
  35. package/dist/database/run-ast/lib/apply-query/filter/validate-operator.d.ts +2 -0
  36. package/dist/database/run-ast/lib/apply-query/filter/validate-operator.js +18 -0
  37. package/dist/database/run-ast/lib/apply-query/get-filter-path.d.ts +1 -0
  38. package/dist/database/run-ast/lib/apply-query/get-filter-path.js +13 -0
  39. package/dist/database/run-ast/lib/apply-query/get-operation.d.ts +7 -0
  40. package/dist/database/run-ast/lib/apply-query/get-operation.js +19 -0
  41. package/dist/database/run-ast/lib/apply-query/index.d.ts +20 -0
  42. package/dist/database/run-ast/lib/apply-query/index.js +92 -0
  43. package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.d.ts +2 -0
  44. package/dist/database/run-ast/lib/apply-query/join-filter-with-cases.js +12 -0
  45. package/dist/database/run-ast/lib/apply-query/mock.d.ts +3 -0
  46. package/dist/database/run-ast/lib/apply-query/mock.js +4 -0
  47. package/dist/database/run-ast/lib/apply-query/pagination.d.ts +3 -0
  48. package/dist/database/run-ast/lib/apply-query/pagination.js +11 -0
  49. package/dist/database/run-ast/lib/apply-query/search.d.ts +4 -0
  50. package/dist/database/run-ast/lib/apply-query/search.js +78 -0
  51. package/dist/database/run-ast/lib/apply-query/sort.d.ts +19 -0
  52. package/dist/database/run-ast/lib/apply-query/sort.js +87 -0
  53. package/dist/database/run-ast/lib/get-db-query.js +7 -5
  54. package/dist/database/run-ast/run-ast.js +1 -1
  55. package/dist/database/run-ast/utils/apply-case-when.js +1 -1
  56. package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
  57. package/dist/{utils → database/run-ast/utils}/get-column.js +1 -1
  58. package/dist/database/run-ast/utils/get-field-alias.js +1 -1
  59. package/dist/database/run-ast/utils/remove-temporary-fields.js +1 -1
  60. package/dist/database/seeds/01-collections.yaml +2 -2
  61. package/dist/database/seeds/04-fields.yaml +2 -2
  62. package/dist/database/seeds/05-activity.yaml +1 -1
  63. package/dist/database/seeds/08-permissions.yaml +1 -1
  64. package/dist/database/seeds/09-presets.yaml +1 -1
  65. package/dist/database/seeds/10-relations.yaml +8 -8
  66. package/dist/database/seeds/11-revisions.yaml +1 -1
  67. package/dist/database/seeds/run.js +8 -1
  68. package/dist/flows.js +8 -1
  69. package/dist/metrics/lib/create-metrics.js +13 -7
  70. package/dist/operations/condition/index.js +7 -4
  71. package/dist/operations/exec/index.js +2 -1
  72. package/dist/permissions/utils/default-permission.d.ts +7 -0
  73. package/dist/permissions/utils/default-permission.js +7 -0
  74. package/dist/services/collections.js +22 -18
  75. package/dist/services/fields.js +2 -5
  76. package/dist/services/payload.d.ts +6 -6
  77. package/dist/services/payload.js +47 -13
  78. package/dist/services/server.js +7 -2
  79. package/dist/services/specifications.js +2 -2
  80. package/dist/utils/get-relation-info.d.ts +1 -1
  81. package/dist/utils/get-relation-info.js +2 -4
  82. package/dist/websocket/handlers/items.js +5 -0
  83. package/package.json +23 -21
  84. package/dist/database/get-ast-from-query/utils/get-relation.d.ts +0 -2
  85. package/dist/database/get-ast-from-query/utils/get-relation.js +0 -7
  86. package/dist/utils/apply-query.d.ts +0 -46
  87. package/dist/utils/apply-query.js +0 -771
  88. /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.d.ts +0 -0
  89. /package/dist/{utils → database/run-ast/utils}/apply-function-to-column-name.js +0 -0
  90. /package/dist/{utils → database/run-ast/utils}/get-column.d.ts +0 -0
@@ -1,771 +0,0 @@
1
- import { NUMERIC_TYPES } from '@directus/constants';
2
- import { InvalidQueryError } from '@directus/errors';
3
- import { getFilterOperatorsForType, getFunctionsForType, getOutputTypeForFunction, isIn } from '@directus/utils';
4
- import { clone, isPlainObject } from 'lodash-es';
5
- import { customAlphabet } from 'nanoid/non-secure';
6
- import { getHelpers } from '../database/helpers/index.js';
7
- import { applyCaseWhen } from '../database/run-ast/utils/apply-case-when.js';
8
- import { getCases } from '../permissions/modules/process-ast/lib/get-cases.js';
9
- import { getColumnPath } from './get-column-path.js';
10
- import { getColumn } from './get-column.js';
11
- import { getRelationInfo } from './get-relation-info.js';
12
- import { isValidUuid } from './is-valid-uuid.js';
13
- import { parseFilterKey } from './parse-filter-key.js';
14
- import { parseNumericString } from './parse-numeric-string.js';
15
- export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
16
- /**
17
- * Apply the Query to a given Knex query builder instance
18
- */
19
- export default function applyQuery(knex, collection, dbQuery, query, schema, cases, permissions, options) {
20
- const aliasMap = options?.aliasMap ?? Object.create(null);
21
- let hasJoins = false;
22
- let hasMultiRelationalFilter = false;
23
- applyLimit(knex, dbQuery, query.limit);
24
- if (query.offset) {
25
- applyOffset(knex, dbQuery, query.offset);
26
- }
27
- if (query.page && query.limit && query.limit !== -1) {
28
- applyOffset(knex, dbQuery, query.limit * (query.page - 1));
29
- }
30
- if (query.sort && !options?.isInnerQuery && !options?.hasMultiRelationalSort) {
31
- const sortResult = applySort(knex, schema, dbQuery, query, collection, aliasMap);
32
- if (!hasJoins) {
33
- hasJoins = sortResult.hasJoins;
34
- }
35
- }
36
- // `cases` are the permissions cases that are required for the current data set. We're
37
- // dynamically adding those into the filters that the user provided to enforce the permission
38
- // rules. You should be able to read an item if one or more of the cases matches. The actual case
39
- // is reused in the column selection case/when to dynamically return or nullify the field values
40
- // you're actually allowed to read
41
- const filter = joinFilterWithCases(query.filter, cases);
42
- if (filter) {
43
- const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases, permissions);
44
- if (!hasJoins) {
45
- hasJoins = filterResult.hasJoins;
46
- }
47
- hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
48
- }
49
- if (query.group) {
50
- const helpers = getHelpers(knex);
51
- const rawColumns = query.group.map((column) => getColumn(knex, collection, column, false, schema));
52
- let columns;
53
- if (options?.groupWhenCases) {
54
- if (helpers.capabilities.supportsColumnPositionInGroupBy() && options.groupColumnPositions) {
55
- // This can be streamlined for databases that support reusing the alias in group by expressions
56
- columns = query.group.map((column, index) => options.groupColumnPositions[index] !== undefined ? knex.raw(options.groupColumnPositions[index]) : column);
57
- }
58
- else {
59
- // Reconstruct the columns with the case/when logic
60
- columns = rawColumns.map((column, index) => applyCaseWhen({
61
- columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
62
- column,
63
- aliasMap,
64
- cases,
65
- table: collection,
66
- permissions,
67
- }, {
68
- knex,
69
- schema,
70
- }));
71
- }
72
- if (query.sort && query.sort.length === 1 && query.sort[0] === query.group[0]) {
73
- // Special case, where the sort query is injected by the group by operation
74
- dbQuery.clear('order');
75
- let order = 'asc';
76
- if (query.sort[0].startsWith('-')) {
77
- order = 'desc';
78
- }
79
- // @ts-expect-error (orderBy does not accept Knex.Raw for some reason, even though it is handled correctly)
80
- // https://github.com/knex/knex/issues/5711
81
- dbQuery.orderBy([{ column: columns[0], order }]);
82
- }
83
- }
84
- else {
85
- columns = rawColumns;
86
- }
87
- dbQuery.groupBy(columns);
88
- }
89
- if (query.search) {
90
- applySearch(knex, schema, dbQuery, query.search, collection, aliasMap, permissions);
91
- }
92
- if (query.aggregate) {
93
- applyAggregate(schema, dbQuery, query.aggregate, collection, hasJoins);
94
- }
95
- return { query: dbQuery, hasJoins, hasMultiRelationalFilter };
96
- }
97
- function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, knex }) {
98
- let hasMultiRelational = false;
99
- let isJoinAdded = false;
100
- path = clone(path);
101
- followRelation(path);
102
- return { hasMultiRelational, isJoinAdded };
103
- function followRelation(pathParts, parentCollection = collection, parentFields) {
104
- /**
105
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
106
- */
107
- const pathRoot = pathParts[0].split(':')[0];
108
- const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
109
- if (!relation) {
110
- return;
111
- }
112
- const existingAlias = parentFields
113
- ? aliasMap[`${parentFields}.${pathParts[0]}`]?.alias
114
- : aliasMap[pathParts[0]]?.alias;
115
- if (!existingAlias) {
116
- const alias = generateAlias();
117
- const aliasKey = parentFields ? `${parentFields}.${pathParts[0]}` : pathParts[0];
118
- const aliasedParentCollection = aliasMap[parentFields ?? '']?.alias || parentCollection;
119
- aliasMap[aliasKey] = { alias, collection: '' };
120
- if (relationType === 'm2o') {
121
- rootQuery.leftJoin({ [alias]: relation.related_collection }, `${aliasedParentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
122
- aliasMap[aliasKey].collection = relation.related_collection;
123
- isJoinAdded = true;
124
- }
125
- else if (relationType === 'a2o') {
126
- const pathScope = pathParts[0].split(':')[1];
127
- if (!pathScope) {
128
- throw new InvalidQueryError({
129
- reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
130
- });
131
- }
132
- rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
133
- joinClause
134
- .onVal(`${aliasedParentCollection}.${relation.meta.one_collection_field}`, '=', pathScope)
135
- .andOn(`${aliasedParentCollection}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${alias}.${schema.collections[pathScope].primary}`));
136
- });
137
- aliasMap[aliasKey].collection = pathScope;
138
- isJoinAdded = true;
139
- }
140
- else if (relationType === 'o2a') {
141
- rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
142
- joinClause
143
- .onVal(`${alias}.${relation.meta.one_collection_field}`, '=', parentCollection)
144
- .andOn(`${alias}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${aliasedParentCollection}.${schema.collections[parentCollection].primary}`));
145
- });
146
- aliasMap[aliasKey].collection = relation.collection;
147
- hasMultiRelational = true;
148
- isJoinAdded = true;
149
- }
150
- else if (relationType === 'o2m') {
151
- rootQuery.leftJoin({ [alias]: relation.collection }, `${aliasedParentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
152
- aliasMap[aliasKey].collection = relation.collection;
153
- hasMultiRelational = true;
154
- isJoinAdded = true;
155
- }
156
- }
157
- let parent;
158
- if (relationType === 'm2o') {
159
- parent = relation.related_collection;
160
- }
161
- else if (relationType === 'a2o') {
162
- const pathScope = pathParts[0].split(':')[1];
163
- if (!pathScope) {
164
- throw new InvalidQueryError({
165
- reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
166
- });
167
- }
168
- parent = pathScope;
169
- }
170
- else {
171
- parent = relation.collection;
172
- }
173
- if (pathParts.length > 1) {
174
- followRelation(pathParts.slice(1), parent, `${parentFields ? parentFields + '.' : ''}${pathParts[0]}`);
175
- }
176
- }
177
- }
178
- export function applySort(knex, schema, rootQuery, query, collection, aliasMap, returnRecords = false) {
179
- const rootSort = query.sort;
180
- const aggregate = query?.aggregate;
181
- const relations = schema.relations;
182
- let hasJoins = false;
183
- let hasMultiRelationalSort = false;
184
- const sortRecords = rootSort.map((sortField) => {
185
- const column = sortField.split('.');
186
- let order = 'asc';
187
- if (sortField.startsWith('-')) {
188
- order = 'desc';
189
- }
190
- if (column[0].startsWith('-')) {
191
- column[0] = column[0].substring(1);
192
- }
193
- // Is the column name one of the aggregate functions used in the query if there is any?
194
- if (Object.keys(aggregate ?? {}).includes(column[0])) {
195
- // If so, return the column name without the order prefix
196
- const operation = column[0];
197
- // Get the field for the aggregate function
198
- const field = column[1];
199
- // If the operation is countAll there is no field.
200
- if (operation === 'countAll') {
201
- return {
202
- order,
203
- column: 'countAll',
204
- };
205
- }
206
- // If the operation is a root count there is no field.
207
- if (operation === 'count' && (field === '*' || !field)) {
208
- return {
209
- order,
210
- column: 'count',
211
- };
212
- }
213
- // Return the column name with the operation and field name
214
- return {
215
- order,
216
- column: returnRecords ? column[0] : `${operation}->${field}`,
217
- };
218
- }
219
- if (column.length === 1) {
220
- const pathRoot = column[0].split(':')[0];
221
- const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
222
- if (!relation || ['m2o', 'a2o'].includes(relationType ?? '')) {
223
- return {
224
- order,
225
- column: returnRecords ? column[0] : getColumn(knex, collection, column[0], false, schema),
226
- };
227
- }
228
- }
229
- const { hasMultiRelational, isJoinAdded } = addJoin({
230
- path: column,
231
- collection,
232
- aliasMap,
233
- rootQuery,
234
- schema,
235
- relations,
236
- knex,
237
- });
238
- const { columnPath } = getColumnPath({
239
- path: column,
240
- collection,
241
- aliasMap,
242
- relations,
243
- schema,
244
- });
245
- const [alias, field] = columnPath.split('.');
246
- if (!hasJoins) {
247
- hasJoins = isJoinAdded;
248
- }
249
- if (!hasMultiRelationalSort) {
250
- hasMultiRelationalSort = hasMultiRelational;
251
- }
252
- return {
253
- order,
254
- column: returnRecords ? columnPath : getColumn(knex, alias, field, false, schema),
255
- };
256
- });
257
- if (returnRecords)
258
- return { sortRecords, hasJoins, hasMultiRelationalSort };
259
- // Clears the order if any, eg: from MSSQL offset
260
- rootQuery.clear('order');
261
- rootQuery.orderBy(sortRecords);
262
- return { hasJoins, hasMultiRelationalSort };
263
- }
264
- export function applyLimit(knex, rootQuery, limit) {
265
- if (typeof limit === 'number') {
266
- getHelpers(knex).schema.applyLimit(rootQuery, limit);
267
- }
268
- }
269
- export function applyOffset(knex, rootQuery, offset) {
270
- if (typeof offset === 'number') {
271
- getHelpers(knex).schema.applyOffset(rootQuery, offset);
272
- }
273
- }
274
- export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases, permissions) {
275
- const helpers = getHelpers(knex);
276
- const relations = schema.relations;
277
- let hasJoins = false;
278
- let hasMultiRelationalFilter = false;
279
- addJoins(rootQuery, rootFilter, collection);
280
- addWhereClauses(knex, rootQuery, rootFilter, collection);
281
- return { query: rootQuery, hasJoins, hasMultiRelationalFilter };
282
- function addJoins(dbQuery, filter, collection) {
283
- // eslint-disable-next-line prefer-const
284
- for (let [key, value] of Object.entries(filter)) {
285
- if (key === '_or' || key === '_and') {
286
- // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
287
- // permission checks, as {} already matches full permissions.
288
- if (key === '_or' && value.some((subFilter) => Object.keys(subFilter).length === 0)) {
289
- // But only do so, if the value is not equal to `cases` (since then this is not permission related at all)
290
- // or the length of value is 1, ie. only the empty filter.
291
- // If the length is more than one it means that some items (and fields) might now be available, so
292
- // the joins are required for the case/when construction.
293
- if (value !== cases || value.length === 1) {
294
- continue;
295
- }
296
- else {
297
- // Otherwise we can at least filter out all empty filters that would not add joins anyway
298
- value = value.filter((subFilter) => Object.keys(subFilter).length > 0);
299
- }
300
- }
301
- value.forEach((subFilter) => {
302
- addJoins(dbQuery, subFilter, collection);
303
- });
304
- continue;
305
- }
306
- const filterPath = getFilterPath(key, value);
307
- if (filterPath.length > 1 ||
308
- (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
309
- const { hasMultiRelational, isJoinAdded } = addJoin({
310
- path: filterPath,
311
- collection,
312
- knex,
313
- schema,
314
- relations,
315
- rootQuery,
316
- aliasMap,
317
- });
318
- if (!hasJoins) {
319
- hasJoins = isJoinAdded;
320
- }
321
- if (!hasMultiRelationalFilter) {
322
- hasMultiRelationalFilter = hasMultiRelational;
323
- }
324
- }
325
- }
326
- }
327
- function addWhereClauses(knex, dbQuery, filter, collection, logical = 'and') {
328
- for (const [key, value] of Object.entries(filter)) {
329
- if (key === '_or' || key === '_and') {
330
- // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
331
- // permission checks, as {} already matches full permissions.
332
- if (key === '_or' && value.some((subFilter) => Object.keys(subFilter).length === 0)) {
333
- continue;
334
- }
335
- /** @NOTE this callback function isn't called until Knex runs the query */
336
- dbQuery[logical].where((subQuery) => {
337
- value.forEach((subFilter) => {
338
- addWhereClauses(knex, subQuery, subFilter, collection, key === '_and' ? 'and' : 'or');
339
- });
340
- });
341
- continue;
342
- }
343
- const filterPath = getFilterPath(key, value);
344
- /**
345
- * For A2M fields, the path can contain an optional collection scope <field>:<scope>
346
- */
347
- const pathRoot = filterPath[0].split(':')[0];
348
- const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
349
- const operation = getOperation(key, value);
350
- if (!operation)
351
- continue;
352
- const { operator: filterOperator, value: filterValue } = operation;
353
- if (filterPath.length > 1 ||
354
- (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
355
- if (!relation)
356
- continue;
357
- if (relationType === 'o2m' || relationType === 'o2a') {
358
- let pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
359
- if (relationType === 'o2a') {
360
- pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
361
- }
362
- const childKey = Object.keys(value)?.[0];
363
- if (childKey === '_none' || childKey === '_some') {
364
- const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
365
- const field = relation.field;
366
- const collection = relation.collection;
367
- const column = `${collection}.${field}`;
368
- subQueryKnex
369
- .select({ [field]: column })
370
- .from(collection)
371
- .whereNotNull(column);
372
- applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
373
- };
374
- const { cases: subCases } = getCases(relation.collection, permissions, []);
375
- if (childKey === '_none') {
376
- dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
377
- continue;
378
- }
379
- else if (childKey === '_some') {
380
- dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
381
- continue;
382
- }
383
- }
384
- }
385
- if (filterPath.includes('_none') || filterPath.includes('_some')) {
386
- throw new InvalidQueryError({
387
- reason: `"${filterPath.includes('_none') ? '_none' : '_some'}" can only be used with top level relational alias field`,
388
- });
389
- }
390
- const { columnPath, targetCollection, addNestedPkField } = getColumnPath({
391
- path: filterPath,
392
- collection,
393
- relations,
394
- aliasMap,
395
- schema,
396
- });
397
- if (addNestedPkField) {
398
- filterPath.push(addNestedPkField);
399
- }
400
- if (!columnPath)
401
- continue;
402
- const { type, special } = getFilterType(schema.collections[targetCollection].fields, filterPath.at(-1), targetCollection);
403
- validateFilterOperator(type, filterOperator, special);
404
- applyFilterToQuery(columnPath, filterOperator, filterValue, logical, targetCollection);
405
- }
406
- else {
407
- const { type, special } = getFilterType(schema.collections[collection].fields, filterPath[0], collection);
408
- validateFilterOperator(type, filterOperator, special);
409
- const aliasedCollection = aliasMap['']?.alias || collection;
410
- applyFilterToQuery(`${aliasedCollection}.${filterPath[0]}`, filterOperator, filterValue, logical, collection);
411
- }
412
- }
413
- function getFilterType(fields, key, collection = 'unknown') {
414
- const { fieldName, functionName } = parseFilterKey(key);
415
- const field = fields[fieldName];
416
- if (!field) {
417
- throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
418
- }
419
- const { type } = field;
420
- if (functionName) {
421
- const availableFunctions = getFunctionsForType(type);
422
- if (!availableFunctions.includes(functionName)) {
423
- throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
424
- }
425
- const functionType = getOutputTypeForFunction(functionName);
426
- return { type: functionType };
427
- }
428
- return { type, special: field.special };
429
- }
430
- function validateFilterOperator(type, filterOperator, special) {
431
- if (filterOperator.startsWith('_')) {
432
- filterOperator = filterOperator.slice(1);
433
- }
434
- if (!getFilterOperatorsForType(type).includes(filterOperator)) {
435
- throw new InvalidQueryError({
436
- reason: `"${type}" field type does not contain the "_${filterOperator}" filter operator`,
437
- });
438
- }
439
- if (special?.includes('conceal') &&
440
- !getFilterOperatorsForType('hash').includes(filterOperator)) {
441
- throw new InvalidQueryError({
442
- reason: `Field with "conceal" special does not allow the "_${filterOperator}" filter operator`,
443
- });
444
- }
445
- }
446
- function applyFilterToQuery(key, operator, compareValue, logical = 'and', originalCollectionName) {
447
- const [table, column] = key.split('.');
448
- // Is processed through Knex.Raw, so should be safe to string-inject into these where queries
449
- const selectionRaw = getColumn(knex, table, column, false, schema, { originalCollectionName });
450
- // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad..
451
- // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated
452
- // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`)
453
- if ((operator === '_null' && compareValue !== false) ||
454
- (operator === '_nnull' && compareValue === false) ||
455
- (operator === '_eq' && compareValue === null)) {
456
- dbQuery[logical].whereNull(selectionRaw);
457
- return;
458
- }
459
- if ((operator === '_nnull' && compareValue !== false) ||
460
- (operator === '_null' && compareValue === false) ||
461
- (operator === '_neq' && compareValue === null)) {
462
- dbQuery[logical].whereNotNull(selectionRaw);
463
- return;
464
- }
465
- if ((operator === '_empty' && compareValue !== false) || (operator === '_nempty' && compareValue === false)) {
466
- dbQuery[logical].andWhere((query) => {
467
- query.whereNull(key).orWhere(key, '=', '');
468
- });
469
- }
470
- if ((operator === '_nempty' && compareValue !== false) || (operator === '_empty' && compareValue === false)) {
471
- dbQuery[logical].andWhere((query) => {
472
- query.whereNotNull(key).andWhere(key, '!=', '');
473
- });
474
- }
475
- // The following fields however, require a value to be run. If no value is passed, we
476
- // ignore them. This allows easier use in GraphQL, where you wouldn't be able to
477
- // conditionally build out your filter structure (#4471)
478
- if (compareValue === undefined)
479
- return;
480
- if (Array.isArray(compareValue)) {
481
- // Tip: when using a `[Type]` type in GraphQL, but don't provide the variable, it'll be
482
- // reported as [undefined].
483
- // We need to remove any undefined values, as they are useless
484
- compareValue = compareValue.filter((val) => val !== undefined);
485
- }
486
- // Cast filter value (compareValue) based on function used
487
- if (column.includes('(') && column.includes(')')) {
488
- const functionName = column.split('(')[0];
489
- const type = getOutputTypeForFunction(functionName);
490
- if (['integer', 'float', 'decimal'].includes(type)) {
491
- compareValue = Array.isArray(compareValue) ? compareValue.map(Number) : Number(compareValue);
492
- }
493
- }
494
- // Cast filter value (compareValue) based on type of field being filtered against
495
- const [collection, field] = key.split('.');
496
- const mappedCollection = (originalCollectionName || collection);
497
- if (mappedCollection in schema.collections && field in schema.collections[mappedCollection].fields) {
498
- const type = schema.collections[mappedCollection].fields[field].type;
499
- if (['date', 'dateTime', 'time', 'timestamp'].includes(type)) {
500
- if (Array.isArray(compareValue)) {
501
- compareValue = compareValue.map((val) => helpers.date.parse(val));
502
- }
503
- else {
504
- compareValue = helpers.date.parse(compareValue);
505
- }
506
- }
507
- if (['integer', 'float', 'decimal'].includes(type)) {
508
- if (Array.isArray(compareValue)) {
509
- compareValue = compareValue.map((val) => Number(val));
510
- }
511
- else {
512
- compareValue = Number(compareValue);
513
- }
514
- }
515
- }
516
- if (operator === '_eq') {
517
- dbQuery[logical].where(selectionRaw, '=', compareValue);
518
- }
519
- if (operator === '_neq') {
520
- dbQuery[logical].whereNot(selectionRaw, compareValue);
521
- }
522
- if (operator === '_ieq') {
523
- dbQuery[logical].whereRaw(`LOWER(??) = ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
524
- }
525
- if (operator === '_nieq') {
526
- dbQuery[logical].whereRaw(`LOWER(??) <> ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
527
- }
528
- if (operator === '_contains') {
529
- dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
530
- }
531
- if (operator === '_ncontains') {
532
- dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
533
- }
534
- if (operator === '_icontains') {
535
- dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
536
- }
537
- if (operator === '_nicontains') {
538
- dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
539
- }
540
- if (operator === '_starts_with') {
541
- dbQuery[logical].where(key, 'like', `${compareValue}%`);
542
- }
543
- if (operator === '_nstarts_with') {
544
- dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
545
- }
546
- if (operator === '_istarts_with') {
547
- dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
548
- }
549
- if (operator === '_nistarts_with') {
550
- dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
551
- }
552
- if (operator === '_ends_with') {
553
- dbQuery[logical].where(key, 'like', `%${compareValue}`);
554
- }
555
- if (operator === '_nends_with') {
556
- dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
557
- }
558
- if (operator === '_iends_with') {
559
- dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
560
- }
561
- if (operator === '_niends_with') {
562
- dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
563
- }
564
- if (operator === '_gt') {
565
- dbQuery[logical].where(selectionRaw, '>', compareValue);
566
- }
567
- if (operator === '_gte') {
568
- dbQuery[logical].where(selectionRaw, '>=', compareValue);
569
- }
570
- if (operator === '_lt') {
571
- dbQuery[logical].where(selectionRaw, '<', compareValue);
572
- }
573
- if (operator === '_lte') {
574
- dbQuery[logical].where(selectionRaw, '<=', compareValue);
575
- }
576
- if (operator === '_in') {
577
- let value = compareValue;
578
- if (typeof value === 'string')
579
- value = value.split(',');
580
- dbQuery[logical].whereIn(selectionRaw, value);
581
- }
582
- if (operator === '_nin') {
583
- let value = compareValue;
584
- if (typeof value === 'string')
585
- value = value.split(',');
586
- dbQuery[logical].whereNotIn(selectionRaw, value);
587
- }
588
- if (operator === '_between') {
589
- let value = compareValue;
590
- if (typeof value === 'string')
591
- value = value.split(',');
592
- if (value.length !== 2)
593
- return;
594
- dbQuery[logical].whereBetween(selectionRaw, value);
595
- }
596
- if (operator === '_nbetween') {
597
- let value = compareValue;
598
- if (typeof value === 'string')
599
- value = value.split(',');
600
- if (value.length !== 2)
601
- return;
602
- dbQuery[logical].whereNotBetween(selectionRaw, value);
603
- }
604
- if (operator == '_intersects') {
605
- dbQuery[logical].whereRaw(helpers.st.intersects(key, compareValue));
606
- }
607
- if (operator == '_nintersects') {
608
- dbQuery[logical].whereRaw(helpers.st.nintersects(key, compareValue));
609
- }
610
- if (operator == '_intersects_bbox') {
611
- dbQuery[logical].whereRaw(helpers.st.intersects_bbox(key, compareValue));
612
- }
613
- if (operator == '_nintersects_bbox') {
614
- dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
615
- }
616
- }
617
- }
618
- }
619
- export function applySearch(knex, schema, dbQuery, searchQuery, collection, aliasMap, permissions) {
620
- const { number: numberHelper } = getHelpers(knex);
621
- const allowedFields = new Set(permissions.filter((p) => p.collection === collection).flatMap((p) => p.fields ?? []));
622
- let fields = Object.entries(schema.collections[collection].fields);
623
- const { cases, caseMap } = getCases(collection, permissions, []);
624
- // Add field restrictions if non-admin and "everything" is not allowed
625
- if (cases.length !== 0 && !allowedFields.has('*')) {
626
- fields = fields.filter((field) => allowedFields.has(field[0]));
627
- }
628
- dbQuery.andWhere(function (queryBuilder) {
629
- let needsFallbackCondition = true;
630
- fields.forEach(([name, field]) => {
631
- // only account for when cases when full access is not given
632
- const whenCases = allowedFields.has('*') ? [] : (caseMap[name] ?? []).map((caseIndex) => cases[caseIndex]);
633
- const fieldType = getFieldType(field);
634
- if (fieldType !== null) {
635
- needsFallbackCondition = false;
636
- }
637
- else {
638
- return;
639
- }
640
- if (cases.length !== 0 && whenCases?.length !== 0) {
641
- queryBuilder.orWhere((subQuery) => {
642
- addSearchCondition(subQuery, name, fieldType, 'and');
643
- applyFilter(knex, schema, subQuery, { _or: whenCases }, collection, aliasMap, cases, permissions);
644
- });
645
- }
646
- else {
647
- addSearchCondition(queryBuilder, name, fieldType, 'or');
648
- }
649
- });
650
- if (needsFallbackCondition) {
651
- queryBuilder.orWhereRaw('1 = 0');
652
- }
653
- });
654
- function addSearchCondition(queryBuilder, name, fieldType, logical) {
655
- if (fieldType === null) {
656
- return;
657
- }
658
- if (fieldType === 'string') {
659
- queryBuilder[logical].whereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
660
- }
661
- else if (fieldType === 'numeric') {
662
- numberHelper.addSearchCondition(queryBuilder, collection, name, parseNumericString(searchQuery), logical);
663
- }
664
- else if (fieldType === 'uuid') {
665
- queryBuilder[logical].where({ [`${collection}.${name}`]: searchQuery });
666
- }
667
- }
668
- function getFieldType(field) {
669
- if (['text', 'string'].includes(field.type)) {
670
- return 'string';
671
- }
672
- if (isNumericField(field)) {
673
- const number = parseNumericString(searchQuery);
674
- if (number === null) {
675
- return null;
676
- }
677
- if (numberHelper.isNumberValid(number, field)) {
678
- return 'numeric';
679
- }
680
- }
681
- if (field.type === 'uuid' && isValidUuid(searchQuery)) {
682
- return 'uuid';
683
- }
684
- return null;
685
- }
686
- }
687
- export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins) {
688
- for (const [operation, fields] of Object.entries(aggregate)) {
689
- if (!fields)
690
- continue;
691
- for (const field of fields) {
692
- if (operation === 'avg') {
693
- dbQuery.avg(`${collection}.${field}`, { as: `avg->${field}` });
694
- }
695
- if (operation === 'avgDistinct') {
696
- dbQuery.avgDistinct(`${collection}.${field}`, { as: `avgDistinct->${field}` });
697
- }
698
- if (operation === 'countAll') {
699
- dbQuery.count('*', { as: 'countAll' });
700
- }
701
- if (operation === 'count') {
702
- if (field === '*') {
703
- dbQuery.count('*', { as: 'count' });
704
- }
705
- else {
706
- dbQuery.count(`${collection}.${field}`, { as: `count->${field}` });
707
- }
708
- }
709
- if (operation === 'countDistinct') {
710
- if (!hasJoins && schema.collections[collection]?.primary === field) {
711
- // Optimize to count as primary keys are unique
712
- dbQuery.count(`${collection}.${field}`, { as: `countDistinct->${field}` });
713
- }
714
- else {
715
- dbQuery.countDistinct(`${collection}.${field}`, { as: `countDistinct->${field}` });
716
- }
717
- }
718
- if (operation === 'sum') {
719
- dbQuery.sum(`${collection}.${field}`, { as: `sum->${field}` });
720
- }
721
- if (operation === 'sumDistinct') {
722
- dbQuery.sumDistinct(`${collection}.${field}`, { as: `sumDistinct->${field}` });
723
- }
724
- if (operation === 'min') {
725
- dbQuery.min(`${collection}.${field}`, { as: `min->${field}` });
726
- }
727
- if (operation === 'max') {
728
- dbQuery.max(`${collection}.${field}`, { as: `max->${field}` });
729
- }
730
- }
731
- }
732
- }
733
- export function joinFilterWithCases(filter, cases) {
734
- if (cases.length > 0 && !filter) {
735
- return { _or: cases };
736
- }
737
- else if (filter && cases.length === 0) {
738
- return filter ?? null;
739
- }
740
- else if (filter && cases.length > 0) {
741
- return { _and: [filter, { _or: cases }] };
742
- }
743
- return null;
744
- }
745
- function getFilterPath(key, value) {
746
- const path = [key];
747
- const childKey = Object.keys(value)[0];
748
- if (!childKey || (childKey.startsWith('_') === true && !['_none', '_some'].includes(childKey))) {
749
- return path;
750
- }
751
- if (isPlainObject(value)) {
752
- path.push(...getFilterPath(childKey, Object.values(value)[0]));
753
- }
754
- return path;
755
- }
756
- function getOperation(key, value) {
757
- if (key.startsWith('_') && !['_and', '_or', '_none', '_some'].includes(key)) {
758
- return { operator: key, value };
759
- }
760
- else if (!isPlainObject(value)) {
761
- return { operator: '_eq', value };
762
- }
763
- const childKey = Object.keys(value)[0];
764
- if (childKey) {
765
- return getOperation(childKey, Object.values(value)[0]);
766
- }
767
- return null;
768
- }
769
- function isNumericField(field) {
770
- return isIn(field.type, NUMERIC_TYPES);
771
- }