@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
@@ -0,0 +1,8 @@
1
+ import type { FieldOverview } from '@directus/types';
2
+ export declare function getFilterType(fields: Record<string, FieldOverview>, key: string, collection?: string): {
3
+ type: "string" | "boolean" | "json" | "binary" | "time" | "text" | "integer" | "float" | "alias" | "uuid" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
4
+ special?: never;
5
+ } | {
6
+ type: "string" | "boolean" | "json" | "binary" | "time" | "text" | "integer" | "float" | "alias" | "uuid" | "dateTime" | "timestamp" | "bigInteger" | "date" | "decimal" | "hash" | "csv" | "geometry" | "geometry.Point" | "geometry.LineString" | "geometry.Polygon" | "geometry.MultiPoint" | "geometry.MultiLineString" | "geometry.MultiPolygon" | "unknown";
7
+ special: string[];
8
+ };
@@ -0,0 +1,20 @@
1
+ import { parseFilterKey } from '../../../../../utils/parse-filter-key.js';
2
+ import { InvalidQueryError } from '@directus/errors';
3
+ import { getFunctionsForType, getOutputTypeForFunction } from '@directus/utils';
4
+ export function getFilterType(fields, key, collection = 'unknown') {
5
+ const { fieldName, functionName } = parseFilterKey(key);
6
+ const field = fields[fieldName];
7
+ if (!field) {
8
+ throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
9
+ }
10
+ const { type } = field;
11
+ if (functionName) {
12
+ const availableFunctions = getFunctionsForType(type);
13
+ if (!availableFunctions.includes(functionName)) {
14
+ throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
15
+ }
16
+ const functionType = getOutputTypeForFunction(functionName);
17
+ return { type: functionType };
18
+ }
19
+ return { type, special: field.special };
20
+ }
@@ -0,0 +1,8 @@
1
+ import type { Filter, Permission, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AliasMap } from '../../../../../utils/get-column-path.js';
4
+ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[], permissions: Permission[]): {
5
+ query: Knex.QueryBuilder<any, any>;
6
+ hasJoins: boolean;
7
+ hasMultiRelationalFilter: boolean;
8
+ };
@@ -0,0 +1,151 @@
1
+ import { InvalidQueryError } from '@directus/errors';
2
+ import { getCases } from '../../../../../permissions/modules/process-ast/lib/get-cases.js';
3
+ import { getColumnPath } from '../../../../../utils/get-column-path.js';
4
+ import { getRelationInfo } from '../../../../../utils/get-relation-info.js';
5
+ import { getHelpers } from '../../../../helpers/index.js';
6
+ import { addJoin } from '../add-join.js';
7
+ import { getFilterPath } from '../get-filter-path.js';
8
+ import { getOperation } from '../get-operation.js';
9
+ import applyQuery from '../index.js';
10
+ import { getFilterType } from './get-filter-type.js';
11
+ import { applyOperator } from './operator.js';
12
+ import { validateOperator } from './validate-operator.js';
13
+ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases, permissions) {
14
+ const relations = schema.relations;
15
+ let hasJoins = false;
16
+ let hasMultiRelationalFilter = false;
17
+ addJoins(rootQuery, rootFilter, collection);
18
+ addWhereClauses(knex, rootQuery, rootFilter, collection);
19
+ return { query: rootQuery, hasJoins, hasMultiRelationalFilter };
20
+ function addJoins(dbQuery, filter, collection) {
21
+ // eslint-disable-next-line prefer-const
22
+ for (let [key, value] of Object.entries(filter)) {
23
+ if (key === '_or' || key === '_and') {
24
+ // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
25
+ // permission checks, as {} already matches full permissions.
26
+ if (key === '_or' && value.some((subFilter) => Object.keys(subFilter).length === 0)) {
27
+ // But only do so, if the value is not equal to `cases` (since then this is not permission related at all)
28
+ // or the length of value is 1, ie. only the empty filter.
29
+ // If the length is more than one it means that some items (and fields) might now be available, so
30
+ // the joins are required for the case/when construction.
31
+ if (value !== cases || value.length === 1) {
32
+ continue;
33
+ }
34
+ else {
35
+ // Otherwise we can at least filter out all empty filters that would not add joins anyway
36
+ value = value.filter((subFilter) => Object.keys(subFilter).length > 0);
37
+ }
38
+ }
39
+ value.forEach((subFilter) => {
40
+ addJoins(dbQuery, subFilter, collection);
41
+ });
42
+ continue;
43
+ }
44
+ const filterPath = getFilterPath(key, value);
45
+ if (filterPath.length > 1 ||
46
+ (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
47
+ const { hasMultiRelational, isJoinAdded } = addJoin({
48
+ path: filterPath,
49
+ collection,
50
+ knex,
51
+ schema,
52
+ rootQuery,
53
+ aliasMap,
54
+ });
55
+ if (!hasJoins) {
56
+ hasJoins = isJoinAdded;
57
+ }
58
+ if (!hasMultiRelationalFilter) {
59
+ hasMultiRelationalFilter = hasMultiRelational;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ function addWhereClauses(knex, dbQuery, filter, collection, logical = 'and') {
65
+ for (const [key, value] of Object.entries(filter)) {
66
+ if (key === '_or' || key === '_and') {
67
+ // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
68
+ // permission checks, as {} already matches full permissions.
69
+ if (key === '_or' && value.some((subFilter) => Object.keys(subFilter).length === 0)) {
70
+ continue;
71
+ }
72
+ /** @NOTE this callback function isn't called until Knex runs the query */
73
+ dbQuery[logical].where((subQuery) => {
74
+ value.forEach((subFilter) => {
75
+ addWhereClauses(knex, subQuery, subFilter, collection, key === '_and' ? 'and' : 'or');
76
+ });
77
+ });
78
+ continue;
79
+ }
80
+ const filterPath = getFilterPath(key, value);
81
+ /**
82
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
83
+ */
84
+ const pathRoot = filterPath[0].split(':')[0];
85
+ const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
86
+ const operation = getOperation(key, value);
87
+ if (!operation)
88
+ continue;
89
+ const { operator: filterOperator, value: filterValue } = operation;
90
+ if (filterPath.length > 1 ||
91
+ (!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
92
+ if (!relation)
93
+ continue;
94
+ if (relationType === 'o2m' || relationType === 'o2a') {
95
+ let pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
96
+ if (relationType === 'o2a') {
97
+ pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
98
+ }
99
+ const childKey = Object.keys(value)?.[0];
100
+ if (childKey === '_none' || childKey === '_some') {
101
+ const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
102
+ const field = relation.field;
103
+ const collection = relation.collection;
104
+ const column = `${collection}.${field}`;
105
+ subQueryKnex
106
+ .select({ [field]: column })
107
+ .from(collection)
108
+ .whereNotNull(column);
109
+ applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
110
+ };
111
+ const { cases: subCases } = getCases(relation.collection, permissions, []);
112
+ if (childKey === '_none') {
113
+ dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
114
+ continue;
115
+ }
116
+ else if (childKey === '_some') {
117
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
118
+ continue;
119
+ }
120
+ }
121
+ }
122
+ if (filterPath.includes('_none') || filterPath.includes('_some')) {
123
+ throw new InvalidQueryError({
124
+ reason: `"${filterPath.includes('_none') ? '_none' : '_some'}" can only be used with top level relational alias field`,
125
+ });
126
+ }
127
+ const { columnPath, targetCollection, addNestedPkField } = getColumnPath({
128
+ path: filterPath,
129
+ collection,
130
+ relations,
131
+ aliasMap,
132
+ schema,
133
+ });
134
+ if (addNestedPkField) {
135
+ filterPath.push(addNestedPkField);
136
+ }
137
+ if (!columnPath)
138
+ continue;
139
+ const { type, special } = getFilterType(schema.collections[targetCollection].fields, filterPath.at(-1), targetCollection);
140
+ validateOperator(type, filterOperator, special);
141
+ applyOperator(knex, dbQuery, schema, columnPath, filterOperator, filterValue, logical, targetCollection);
142
+ }
143
+ else {
144
+ const { type, special } = getFilterType(schema.collections[collection].fields, filterPath[0], collection);
145
+ validateOperator(type, filterOperator, special);
146
+ const aliasedCollection = aliasMap['']?.alias || collection;
147
+ applyOperator(knex, dbQuery, schema, `${aliasedCollection}.${filterPath[0]}`, filterOperator, filterValue, logical, collection);
148
+ }
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,3 @@
1
+ import type { SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ export declare function applyOperator(knex: Knex, dbQuery: Knex.QueryBuilder, schema: SchemaOverview, key: string, operator: string, compareValue: any, logical?: 'and' | 'or', originalCollectionName?: string): void;
@@ -0,0 +1,175 @@
1
+ import { getColumn } from '../../../utils/get-column.js';
2
+ import { getOutputTypeForFunction } from '@directus/utils';
3
+ import { getHelpers } from '../../../../helpers/index.js';
4
+ export function applyOperator(knex, dbQuery, schema, key, operator, compareValue, logical = 'and', originalCollectionName) {
5
+ const helpers = getHelpers(knex);
6
+ const [table, column] = key.split('.');
7
+ // Is processed through Knex.Raw, so should be safe to string-inject into these where queries
8
+ const selectionRaw = getColumn(knex, table, column, false, schema, { originalCollectionName });
9
+ // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad..
10
+ // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated
11
+ // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`)
12
+ if ((operator === '_null' && compareValue !== false) ||
13
+ (operator === '_nnull' && compareValue === false) ||
14
+ (operator === '_eq' && compareValue === null)) {
15
+ dbQuery[logical].whereNull(selectionRaw);
16
+ return;
17
+ }
18
+ if ((operator === '_nnull' && compareValue !== false) ||
19
+ (operator === '_null' && compareValue === false) ||
20
+ (operator === '_neq' && compareValue === null)) {
21
+ dbQuery[logical].whereNotNull(selectionRaw);
22
+ return;
23
+ }
24
+ if ((operator === '_empty' && compareValue !== false) || (operator === '_nempty' && compareValue === false)) {
25
+ dbQuery[logical].andWhere((query) => {
26
+ query.whereNull(key).orWhere(key, '=', '');
27
+ });
28
+ }
29
+ if ((operator === '_nempty' && compareValue !== false) || (operator === '_empty' && compareValue === false)) {
30
+ dbQuery[logical].andWhere((query) => {
31
+ query.whereNotNull(key).andWhere(key, '!=', '');
32
+ });
33
+ }
34
+ // The following fields however, require a value to be run. If no value is passed, we
35
+ // ignore them. This allows easier use in GraphQL, where you wouldn't be able to
36
+ // conditionally build out your filter structure (#4471)
37
+ if (compareValue === undefined)
38
+ return;
39
+ if (Array.isArray(compareValue)) {
40
+ // Tip: when using a `[Type]` type in GraphQL, but don't provide the variable, it'll be
41
+ // reported as [undefined].
42
+ // We need to remove any undefined values, as they are useless
43
+ compareValue = compareValue.filter((val) => val !== undefined);
44
+ }
45
+ // Cast filter value (compareValue) based on function used
46
+ if (column.includes('(') && column.includes(')')) {
47
+ const functionName = column.split('(')[0];
48
+ const type = getOutputTypeForFunction(functionName);
49
+ if (['integer', 'float', 'decimal'].includes(type)) {
50
+ compareValue = Array.isArray(compareValue) ? compareValue.map(Number) : Number(compareValue);
51
+ }
52
+ }
53
+ // Cast filter value (compareValue) based on type of field being filtered against
54
+ const [collection, field] = key.split('.');
55
+ const mappedCollection = (originalCollectionName || collection);
56
+ if (mappedCollection in schema.collections && field in schema.collections[mappedCollection].fields) {
57
+ const type = schema.collections[mappedCollection].fields[field].type;
58
+ if (['date', 'dateTime', 'time', 'timestamp'].includes(type)) {
59
+ if (Array.isArray(compareValue)) {
60
+ compareValue = compareValue.map((val) => helpers.date.parse(val));
61
+ }
62
+ else {
63
+ compareValue = helpers.date.parse(compareValue);
64
+ }
65
+ }
66
+ if (['integer', 'float', 'decimal'].includes(type)) {
67
+ if (Array.isArray(compareValue)) {
68
+ compareValue = compareValue.map((val) => Number(val));
69
+ }
70
+ else {
71
+ compareValue = Number(compareValue);
72
+ }
73
+ }
74
+ }
75
+ if (operator === '_eq') {
76
+ dbQuery[logical].where(selectionRaw, '=', compareValue);
77
+ }
78
+ if (operator === '_neq') {
79
+ dbQuery[logical].whereNot(selectionRaw, compareValue);
80
+ }
81
+ if (operator === '_ieq') {
82
+ dbQuery[logical].whereRaw(`LOWER(??) = ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
83
+ }
84
+ if (operator === '_nieq') {
85
+ dbQuery[logical].whereRaw(`LOWER(??) <> ?`, [selectionRaw, `${compareValue.toLowerCase()}`]);
86
+ }
87
+ if (operator === '_contains') {
88
+ dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
89
+ }
90
+ if (operator === '_ncontains') {
91
+ dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
92
+ }
93
+ if (operator === '_icontains') {
94
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
95
+ }
96
+ if (operator === '_nicontains') {
97
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}%`]);
98
+ }
99
+ if (operator === '_starts_with') {
100
+ dbQuery[logical].where(key, 'like', `${compareValue}%`);
101
+ }
102
+ if (operator === '_nstarts_with') {
103
+ dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
104
+ }
105
+ if (operator === '_istarts_with') {
106
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
107
+ }
108
+ if (operator === '_nistarts_with') {
109
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `${compareValue.toLowerCase()}%`]);
110
+ }
111
+ if (operator === '_ends_with') {
112
+ dbQuery[logical].where(key, 'like', `%${compareValue}`);
113
+ }
114
+ if (operator === '_nends_with') {
115
+ dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
116
+ }
117
+ if (operator === '_iends_with') {
118
+ dbQuery[logical].whereRaw(`LOWER(??) LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
119
+ }
120
+ if (operator === '_niends_with') {
121
+ dbQuery[logical].whereRaw(`LOWER(??) NOT LIKE ?`, [selectionRaw, `%${compareValue.toLowerCase()}`]);
122
+ }
123
+ if (operator === '_gt') {
124
+ dbQuery[logical].where(selectionRaw, '>', compareValue);
125
+ }
126
+ if (operator === '_gte') {
127
+ dbQuery[logical].where(selectionRaw, '>=', compareValue);
128
+ }
129
+ if (operator === '_lt') {
130
+ dbQuery[logical].where(selectionRaw, '<', compareValue);
131
+ }
132
+ if (operator === '_lte') {
133
+ dbQuery[logical].where(selectionRaw, '<=', compareValue);
134
+ }
135
+ if (operator === '_in') {
136
+ let value = compareValue;
137
+ if (typeof value === 'string')
138
+ value = value.split(',');
139
+ dbQuery[logical].whereIn(selectionRaw, value);
140
+ }
141
+ if (operator === '_nin') {
142
+ let value = compareValue;
143
+ if (typeof value === 'string')
144
+ value = value.split(',');
145
+ dbQuery[logical].whereNotIn(selectionRaw, value);
146
+ }
147
+ if (operator === '_between') {
148
+ let value = compareValue;
149
+ if (typeof value === 'string')
150
+ value = value.split(',');
151
+ if (value.length !== 2)
152
+ return;
153
+ dbQuery[logical].whereBetween(selectionRaw, value);
154
+ }
155
+ if (operator === '_nbetween') {
156
+ let value = compareValue;
157
+ if (typeof value === 'string')
158
+ value = value.split(',');
159
+ if (value.length !== 2)
160
+ return;
161
+ dbQuery[logical].whereNotBetween(selectionRaw, value);
162
+ }
163
+ if (operator == '_intersects') {
164
+ dbQuery[logical].whereRaw(helpers.st.intersects(key, compareValue));
165
+ }
166
+ if (operator == '_nintersects') {
167
+ dbQuery[logical].whereRaw(helpers.st.nintersects(key, compareValue));
168
+ }
169
+ if (operator == '_intersects_bbox') {
170
+ dbQuery[logical].whereRaw(helpers.st.intersects_bbox(key, compareValue));
171
+ }
172
+ if (operator == '_nintersects_bbox') {
173
+ dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
174
+ }
175
+ }
@@ -0,0 +1,2 @@
1
+ import type { Type } from '@directus/types';
2
+ export declare function validateOperator(type: Type, filterOperator: string, special?: string[]): void;
@@ -0,0 +1,18 @@
1
+ import { InvalidQueryError } from '@directus/errors';
2
+ import { getFilterOperatorsForType } from '@directus/utils';
3
+ export function validateOperator(type, filterOperator, special) {
4
+ if (filterOperator.startsWith('_')) {
5
+ filterOperator = filterOperator.slice(1);
6
+ }
7
+ if (!getFilterOperatorsForType(type).includes(filterOperator)) {
8
+ throw new InvalidQueryError({
9
+ reason: `"${type}" field type does not contain the "_${filterOperator}" filter operator`,
10
+ });
11
+ }
12
+ if (special?.includes('conceal') &&
13
+ !getFilterOperatorsForType('hash').includes(filterOperator)) {
14
+ throw new InvalidQueryError({
15
+ reason: `Field with "conceal" special does not allow the "_${filterOperator}" filter operator`,
16
+ });
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ export declare function getFilterPath(key: string, value: Record<string, any>): string[];
@@ -0,0 +1,13 @@
1
+ import { isObject } from '@directus/utils';
2
+ export function getFilterPath(key, value) {
3
+ const path = [key];
4
+ const childKey = Object.keys(value)[0];
5
+ if (!childKey || (childKey.startsWith('_') === true && !['_none', '_some'].includes(childKey))) {
6
+ return path;
7
+ }
8
+ const nestedValue = Object.values(value)[0];
9
+ if (isObject(value)) {
10
+ path.push(...getFilterPath(childKey, nestedValue));
11
+ }
12
+ return path;
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns null or the operation information form a FieldFilter
3
+ */
4
+ export declare function getOperation(key: string, value: unknown): {
5
+ operator: string;
6
+ value: unknown;
7
+ } | null;
@@ -0,0 +1,19 @@
1
+ import { isObject } from '@directus/utils';
2
+ /**
3
+ * Returns null or the operation information form a FieldFilter
4
+ */
5
+ export function getOperation(key, value) {
6
+ if (key === '_and' || key === '_or')
7
+ return null;
8
+ if (key.startsWith('_') && key !== '_none' && key !== '_some') {
9
+ return { operator: key, value };
10
+ }
11
+ else if (!isObject(value)) {
12
+ return { operator: '_eq', value };
13
+ }
14
+ const childKey = Object.keys(value)[0];
15
+ if (childKey) {
16
+ return getOperation(childKey, Object.values(value)[0]);
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,20 @@
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AliasMap } from '../../../../utils/get-column-path.js';
4
+ export declare const generateAlias: (size?: number) => string;
5
+ type ApplyQueryOptions = {
6
+ aliasMap?: AliasMap;
7
+ isInnerQuery?: boolean;
8
+ hasMultiRelationalSort?: boolean | undefined;
9
+ groupWhenCases?: number[][] | undefined;
10
+ groupColumnPositions?: number[] | undefined;
11
+ };
12
+ /**
13
+ * Apply the Query to a given Knex query builder instance
14
+ */
15
+ export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], permissions: Permission[], options?: ApplyQueryOptions): {
16
+ query: Knex.QueryBuilder<any, any>;
17
+ hasJoins: boolean;
18
+ hasMultiRelationalFilter: boolean;
19
+ };
20
+ export {};
@@ -0,0 +1,92 @@
1
+ import { customAlphabet } from 'nanoid/non-secure';
2
+ import { getHelpers } from '../../../helpers/index.js';
3
+ import { applyCaseWhen } from '../../utils/apply-case-when.js';
4
+ import { getColumn } from '../../utils/get-column.js';
5
+ import { applyLimit, applyOffset } from './pagination.js';
6
+ import { joinFilterWithCases } from './join-filter-with-cases.js';
7
+ import { applySort } from './sort.js';
8
+ import { applyFilter } from './filter/index.js';
9
+ import { applySearch } from './search.js';
10
+ import { applyAggregate } from './aggregate.js';
11
+ export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
12
+ /**
13
+ * Apply the Query to a given Knex query builder instance
14
+ */
15
+ export default function applyQuery(knex, collection, dbQuery, query, schema, cases, permissions, options) {
16
+ const aliasMap = options?.aliasMap ?? Object.create(null);
17
+ let hasJoins = false;
18
+ let hasMultiRelationalFilter = false;
19
+ applyLimit(knex, dbQuery, query.limit);
20
+ if (query.offset) {
21
+ applyOffset(knex, dbQuery, query.offset);
22
+ }
23
+ if (query.page && query.limit && query.limit !== -1) {
24
+ applyOffset(knex, dbQuery, query.limit * (query.page - 1));
25
+ }
26
+ if (query.sort && !options?.isInnerQuery && !options?.hasMultiRelationalSort) {
27
+ const sortResult = applySort(knex, schema, dbQuery, query.sort, query.aggregate, collection, aliasMap);
28
+ if (!hasJoins) {
29
+ hasJoins = sortResult.hasJoins;
30
+ }
31
+ }
32
+ // `cases` are the permissions cases that are required for the current data set. We're
33
+ // dynamically adding those into the filters that the user provided to enforce the permission
34
+ // rules. You should be able to read an item if one or more of the cases matches. The actual case
35
+ // is reused in the column selection case/when to dynamically return or nullify the field values
36
+ // you're actually allowed to read
37
+ const filter = joinFilterWithCases(query.filter, cases);
38
+ if (filter) {
39
+ const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases, permissions);
40
+ if (!hasJoins) {
41
+ hasJoins = filterResult.hasJoins;
42
+ }
43
+ hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
44
+ }
45
+ if (query.group) {
46
+ const helpers = getHelpers(knex);
47
+ const rawColumns = query.group.map((column) => getColumn(knex, collection, column, false, schema));
48
+ let columns;
49
+ if (options?.groupWhenCases) {
50
+ if (helpers.capabilities.supportsColumnPositionInGroupBy() && options.groupColumnPositions) {
51
+ // This can be streamlined for databases that support reusing the alias in group by expressions
52
+ columns = query.group.map((column, index) => options.groupColumnPositions[index] !== undefined ? knex.raw(options.groupColumnPositions[index]) : column);
53
+ }
54
+ else {
55
+ // Reconstruct the columns with the case/when logic
56
+ columns = rawColumns.map((column, index) => applyCaseWhen({
57
+ columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
58
+ column,
59
+ aliasMap,
60
+ cases,
61
+ table: collection,
62
+ permissions,
63
+ }, {
64
+ knex,
65
+ schema,
66
+ }));
67
+ }
68
+ if (query.sort && query.sort.length === 1 && query.sort[0] === query.group[0]) {
69
+ // Special case, where the sort query is injected by the group by operation
70
+ dbQuery.clear('order');
71
+ let order = 'asc';
72
+ if (query.sort[0].startsWith('-')) {
73
+ order = 'desc';
74
+ }
75
+ // @ts-expect-error (orderBy does not accept Knex.Raw for some reason, even though it is handled correctly)
76
+ // https://github.com/knex/knex/issues/5711
77
+ dbQuery.orderBy([{ column: columns[0], order }]);
78
+ }
79
+ }
80
+ else {
81
+ columns = rawColumns;
82
+ }
83
+ dbQuery.groupBy(columns);
84
+ }
85
+ if (query.search) {
86
+ applySearch(knex, schema, dbQuery, query.search, collection, aliasMap, permissions);
87
+ }
88
+ if (query.aggregate) {
89
+ applyAggregate(schema, dbQuery, query.aggregate, collection, hasJoins);
90
+ }
91
+ return { query: dbQuery, hasJoins, hasMultiRelationalFilter };
92
+ }
@@ -0,0 +1,2 @@
1
+ import type { Filter } from '@directus/types';
2
+ export declare function joinFilterWithCases(filter: Filter | null | undefined, cases: Filter[]): Filter | null;
@@ -0,0 +1,12 @@
1
+ export function joinFilterWithCases(filter, cases) {
2
+ if (cases.length > 0 && !filter) {
3
+ return { _or: cases };
4
+ }
5
+ else if (filter && cases.length === 0) {
6
+ return filter ?? null;
7
+ }
8
+ else if (filter && cases.length > 0) {
9
+ return { _and: [filter, { _or: cases }] };
10
+ }
11
+ return null;
12
+ }
@@ -0,0 +1,3 @@
1
+ import { MockClient } from 'knex-mock-client';
2
+ export declare class Client_SQLite3 extends MockClient {
3
+ }
@@ -0,0 +1,4 @@
1
+ import { MockClient } from 'knex-mock-client';
2
+ // in order for the helpers to know the client type
3
+ export class Client_SQLite3 extends MockClient {
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
3
+ export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
@@ -0,0 +1,11 @@
1
+ import { getHelpers } from '../../../helpers/index.js';
2
+ export function applyLimit(knex, rootQuery, limit) {
3
+ if (typeof limit === 'number') {
4
+ getHelpers(knex).schema.applyLimit(rootQuery, limit);
5
+ }
6
+ }
7
+ export function applyOffset(knex, rootQuery, offset) {
8
+ if (typeof offset === 'number') {
9
+ getHelpers(knex).schema.applyOffset(rootQuery, offset);
10
+ }
11
+ }
@@ -0,0 +1,4 @@
1
+ import type { Permission, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AliasMap } from '../../../../utils/get-column-path.js';
4
+ export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string, aliasMap: AliasMap, permissions: Permission[]): void;