@directus/api 22.0.0 → 22.1.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 (67) hide show
  1. package/dist/cli/commands/init/questions.d.ts +7 -6
  2. package/dist/cli/commands/init/questions.js +2 -2
  3. package/dist/cli/utils/create-env/index.d.ts +2 -2
  4. package/dist/cli/utils/create-env/index.js +3 -1
  5. package/dist/cli/utils/drivers.js +1 -1
  6. package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
  7. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
  8. package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
  9. package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
  10. package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
  11. package/dist/database/helpers/fn/types.d.ts +6 -3
  12. package/dist/database/helpers/fn/types.js +2 -2
  13. package/dist/database/helpers/index.d.ts +3 -3
  14. package/dist/database/index.d.ts +1 -1
  15. package/dist/database/index.js +2 -2
  16. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
  17. package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
  18. package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
  19. package/dist/database/migrations/20240806A-permissions-policies.js +18 -3
  20. package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
  21. package/dist/database/run-ast/lib/get-db-query.js +9 -5
  22. package/dist/database/run-ast/run-ast.d.ts +2 -2
  23. package/dist/database/run-ast/run-ast.js +14 -7
  24. package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
  25. package/dist/database/run-ast/utils/apply-case-when.js +2 -2
  26. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
  27. package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
  28. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
  29. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
  30. package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
  31. package/dist/permissions/lib/fetch-permissions.js +5 -39
  32. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
  33. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
  34. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
  35. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
  36. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
  37. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
  38. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
  39. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
  40. package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
  41. package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
  42. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
  43. package/dist/permissions/modules/process-payload/process-payload.js +4 -5
  44. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
  46. package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
  47. package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
  48. package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
  49. package/dist/server.js +17 -4
  50. package/dist/services/fields.d.ts +1 -1
  51. package/dist/services/fields.js +22 -19
  52. package/dist/services/import-export.js +2 -2
  53. package/dist/services/items.js +1 -1
  54. package/dist/services/meta.js +8 -7
  55. package/dist/services/permissions.js +19 -19
  56. package/dist/types/database.d.ts +1 -1
  57. package/dist/utils/apply-query.d.ts +3 -3
  58. package/dist/utils/apply-query.js +25 -20
  59. package/dist/utils/get-address.d.ts +5 -0
  60. package/dist/utils/get-address.js +13 -0
  61. package/dist/utils/get-column.d.ts +8 -4
  62. package/dist/utils/get-column.js +10 -2
  63. package/dist/utils/sanitize-query.js +1 -1
  64. package/dist/utils/transaction.js +28 -11
  65. package/dist/websocket/controllers/graphql.js +2 -3
  66. package/dist/websocket/controllers/rest.js +2 -3
  67. package/package.json +17 -16
@@ -1,20 +1,21 @@
1
+ import type { Driver } from '../../../types/index.js';
1
2
  export declare const databaseQuestions: {
2
3
  sqlite3: (({ filepath }: {
3
4
  filepath: string;
4
5
  }) => Record<string, string>)[];
5
- mysql: (({ client }: {
6
- client: string;
6
+ mysql2: (({ client }: {
7
+ client: Exclude<Driver, 'sqlite3'>;
7
8
  }) => Record<string, any>)[];
8
9
  pg: (({ client }: {
9
- client: string;
10
+ client: Exclude<Driver, 'sqlite3'>;
10
11
  }) => Record<string, any>)[];
11
12
  cockroachdb: (({ client }: {
12
- client: string;
13
+ client: Exclude<Driver, 'sqlite3'>;
13
14
  }) => Record<string, any>)[];
14
15
  oracledb: (({ client }: {
15
- client: string;
16
+ client: Exclude<Driver, 'sqlite3'>;
16
17
  }) => Record<string, any>)[];
17
18
  mssql: (({ client }: {
18
- client: string;
19
+ client: Exclude<Driver, 'sqlite3'>;
19
20
  }) => Record<string, any>)[];
20
21
  };
@@ -19,7 +19,7 @@ const port = ({ client }) => ({
19
19
  const ports = {
20
20
  pg: 5432,
21
21
  cockroachdb: 26257,
22
- mysql: 3306,
22
+ mysql2: 3306,
23
23
  oracledb: 1521,
24
24
  mssql: 1433,
25
25
  };
@@ -57,7 +57,7 @@ const ssl = () => ({
57
57
  });
58
58
  export const databaseQuestions = {
59
59
  sqlite3: [filename],
60
- mysql: [host, port, database, user, password],
60
+ mysql2: [host, port, database, user, password],
61
61
  pg: [host, port, database, user, password, ssl],
62
62
  cockroachdb: [host, port, database, user, password, ssl],
63
63
  oracledb: [host, port, database, user, password],
@@ -1,3 +1,3 @@
1
+ import type { Driver } from '../../../types/index.js';
1
2
  import type { Credentials } from '../create-db-connection.js';
2
- import type { drivers } from '../drivers.js';
3
- export default function createEnv(client: keyof typeof drivers, credentials: Credentials, directory: string): Promise<void>;
3
+ export default function createEnv(client: Driver, credentials: Credentials, directory: string): Promise<void>;
@@ -14,12 +14,14 @@ const liquidEngine = new Liquid({
14
14
  });
15
15
  export default async function createEnv(client, credentials, directory) {
16
16
  const { nanoid } = await import('nanoid');
17
+ // For backwards-compatibility, DB_CLIENT is still 'mysql'
18
+ const dbClient = client === 'mysql2' ? 'mysql' : client;
17
19
  const config = {
18
20
  security: {
19
21
  SECRET: nanoid(32),
20
22
  },
21
23
  database: {
22
- DB_CLIENT: client,
24
+ DB_CLIENT: dbClient,
23
25
  },
24
26
  };
25
27
  for (const [key, value] of Object.entries(credentials)) {
@@ -1,7 +1,7 @@
1
1
  export const drivers = {
2
2
  pg: 'PostgreSQL / Redshift',
3
3
  cockroachdb: 'CockroachDB (Beta)',
4
- mysql: 'MySQL / MariaDB / Aurora',
4
+ mysql2: 'MySQL / MariaDB / Aurora',
5
5
  sqlite3: 'SQLite',
6
6
  mssql: 'Microsoft SQL Server',
7
7
  oracledb: 'Oracle Database',
@@ -2,8 +2,8 @@
2
2
  * Generate an AST based on a given collection and query
3
3
  */
4
4
  import { cloneDeep, uniq } from 'lodash-es';
5
- import { fetchAllowedFields } from '../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
6
5
  import { parseFields } from './lib/parse-fields.js';
6
+ import { getAllowedSort } from './utils/get-allowed-sort.js';
7
7
  export async function getAstFromQuery(options, context) {
8
8
  options.query = cloneDeep(options.query);
9
9
  const ast = {
@@ -36,36 +36,7 @@ export async function getAstFromQuery(options, context) {
36
36
  // Prevent fields/deep from showing up in the query object in further use
37
37
  delete options.query.fields;
38
38
  delete options.query.deep;
39
- if (!options.query.sort) {
40
- // We'll default to the primary key for the standard sort output
41
- let sortField = context.schema.collections[options.collection].primary;
42
- // If a custom manual sort field is configured, use that
43
- if (context.schema.collections[options.collection]?.sortField) {
44
- sortField = context.schema.collections[options.collection].sortField;
45
- }
46
- if (options.accountability && options.accountability.admin === false) {
47
- // Verify that the user has access to the sort field
48
- const allowedFields = await fetchAllowedFields({
49
- collection: options.collection,
50
- action: 'read',
51
- accountability: options.accountability,
52
- }, context);
53
- if (allowedFields.length === 0) {
54
- sortField = null;
55
- }
56
- else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
57
- // If the sort field is not allowed, default to the first allowed field
58
- sortField = allowedFields[0];
59
- }
60
- }
61
- // When group by is used, default to the first column provided in the group by clause
62
- if (options.query.group?.[0]) {
63
- sortField = options.query.group[0];
64
- }
65
- if (sortField) {
66
- options.query.sort = [sortField];
67
- }
68
- }
39
+ options.query.sort ??= await getAllowedSort(options, context);
69
40
  // When no group by is supplied, but an aggregate function is used, only a single row will be
70
41
  // returned. In those cases, we'll ignore the sort field altogether
71
42
  if (options.query.aggregate && Object.keys(options.query.aggregate).length && !options.query.group?.[0]) {
@@ -1,6 +1,6 @@
1
1
  import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
- import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../../../types/index.js';
3
+ import type { FieldNode, FunctionFieldNode, NestedCollectionNode, O2MNode } from '../../../types/index.js';
4
4
  export interface ParseFieldsOptions {
5
5
  accountability: Accountability | null;
6
6
  parentCollection: string;
@@ -13,3 +13,4 @@ export interface ParseFieldsContext {
13
13
  knex: Knex;
14
14
  }
15
15
  export declare function parseFields(options: ParseFieldsOptions, context: ParseFieldsContext): Promise<[] | (NestedCollectionNode | FieldNode | FunctionFieldNode)[]>;
16
+ export declare function isO2MNode(node: NestedCollectionNode | null): node is O2MNode;
@@ -3,6 +3,7 @@ import { isEmpty } from 'lodash-es';
3
3
  import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
4
4
  import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
5
5
  import { getRelationType } from '../../../utils/get-relation-type.js';
6
+ import { getAllowedSort } from '../utils/get-allowed-sort.js';
6
7
  import { getDeepQuery } from '../utils/get-deep-query.js';
7
8
  import { getRelatedCollection } from '../utils/get-related-collection.js';
8
9
  import { getRelation } from '../utils/get-relation.js';
@@ -119,7 +120,16 @@ export async function parseFields(options, context) {
119
120
  continue;
120
121
  let child = null;
121
122
  if (relationType === 'a2o') {
122
- const allowedCollections = relation.meta.one_allowed_collections;
123
+ let allowedCollections = relation.meta.one_allowed_collections;
124
+ if (options.accountability && options.accountability.admin === false && policies) {
125
+ const permissions = await fetchPermissions({
126
+ action: 'read',
127
+ collections: allowedCollections,
128
+ policies: policies,
129
+ accountability: options.accountability,
130
+ }, context);
131
+ allowedCollections = allowedCollections.filter((collection) => permissions.some((permission) => permission.collection === collection));
132
+ }
123
133
  child = {
124
134
  type: 'a2o',
125
135
  names: allowedCollections,
@@ -181,8 +191,13 @@ export async function parseFields(options, context) {
181
191
  cases: [],
182
192
  whenCase: [],
183
193
  };
184
- if (relationType === 'o2m' && !child.query.sort) {
185
- child.query.sort = [relation.meta?.sort_field || context.schema.collections[relation.collection].primary];
194
+ if (isO2MNode(child) && !child.query.sort) {
195
+ child.query.sort = await getAllowedSort({ collection: relation.collection, relation, accountability: options.accountability }, context);
196
+ }
197
+ if (isO2MNode(child) && child.query.group && child.query.group[0] !== relation.field) {
198
+ // If a group by is used, the result needs to be grouped by the foreign key of the relation first, so results
199
+ // are correctly grouped under the foreign key when extracting the grouped results from the nested queries.
200
+ child.query.group.unshift(relation.field);
186
201
  }
187
202
  }
188
203
  if (child) {
@@ -198,3 +213,6 @@ export async function parseFields(options, context) {
198
213
  return true;
199
214
  });
200
215
  }
216
+ export function isO2MNode(node) {
217
+ return !!node && node.type === 'o2m';
218
+ }
@@ -0,0 +1,9 @@
1
+ import type { Accountability, Query, Relation } from '@directus/types';
2
+ import type { Context } from '../../../permissions/types.js';
3
+ export type GetAllowedSortFieldOptions = {
4
+ collection: string;
5
+ accountability: Accountability | null;
6
+ query?: Query;
7
+ relation?: Relation;
8
+ };
9
+ export declare function getAllowedSort(options: GetAllowedSortFieldOptions, context: Context): Promise<string[] | null>;
@@ -0,0 +1,35 @@
1
+ import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
2
+ export async function getAllowedSort(options, context) {
3
+ // We'll default to the primary key for the standard sort output
4
+ let sortField = context.schema.collections[options.collection].primary;
5
+ // If a custom manual sort field is configured, use that
6
+ if (context.schema.collections[options.collection]?.sortField) {
7
+ sortField = context.schema.collections[options.collection].sortField;
8
+ }
9
+ // If a sort field is defined on the relation, use that
10
+ if (options.relation?.meta?.sort_field) {
11
+ sortField = options.relation.meta.sort_field;
12
+ }
13
+ if (options.accountability && options.accountability.admin === false) {
14
+ // Verify that the user has access to the sort field
15
+ const allowedFields = await fetchAllowedFields({
16
+ collection: options.collection,
17
+ action: 'read',
18
+ accountability: options.accountability,
19
+ }, context);
20
+ if (allowedFields.length === 0) {
21
+ sortField = null;
22
+ }
23
+ else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
24
+ // If the sort field is not allowed, default to the first allowed field
25
+ sortField = allowedFields[0];
26
+ }
27
+ }
28
+ // When group by is used, default to the first column provided in the group by clause
29
+ if (options.query?.group?.[0]) {
30
+ sortField = options.query.group[0];
31
+ }
32
+ if (sortField)
33
+ return [sortField];
34
+ return null;
35
+ }
@@ -1,11 +1,14 @@
1
- import type { Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import { DatabaseHelper } from '../types.js';
4
4
  export type FnHelperOptions = {
5
5
  type: string | undefined;
6
- query: Query | undefined;
7
- cases: Filter[] | undefined;
8
6
  originalCollectionName: string | undefined;
7
+ relationalCountOptions: {
8
+ query: Query;
9
+ cases: Filter[];
10
+ permissions: Permission[];
11
+ } | undefined;
9
12
  };
10
13
  export declare abstract class FnHelper extends DatabaseHelper {
11
14
  protected schema: SchemaOverview;
@@ -20,7 +20,7 @@ export class FnHelper extends DatabaseHelper {
20
20
  .count('*')
21
21
  .from({ [alias]: relation.collection })
22
22
  .where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));
23
- if (options?.query?.filter) {
23
+ if (options?.relationalCountOptions?.query.filter) {
24
24
  // set the newly aliased collection in the alias map as the default parent collection, indicated by '', for any nested filters
25
25
  const aliasMap = {
26
26
  '': {
@@ -28,7 +28,7 @@ export class FnHelper extends DatabaseHelper {
28
28
  collection: relation.collection,
29
29
  },
30
30
  };
31
- countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, aliasMap, options.cases ?? []).query;
31
+ countQuery = applyFilter(this.knex, this.schema, countQuery, options.relationalCountOptions.query.filter, relation.collection, aliasMap, options.relationalCountOptions.cases, options.relationalCountOptions.permissions).query;
32
32
  }
33
33
  return this.knex.raw('(' + countQuery.toQuery() + ')');
34
34
  }
@@ -8,10 +8,10 @@ import * as sequenceHelpers from './sequence/index.js';
8
8
  import * as numberHelpers from './number/index.js';
9
9
  export declare function getHelpers(database: Knex): {
10
10
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
11
- st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
- schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
11
+ st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
+ schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
13
13
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
14
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
15
15
  };
16
- export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.mysql | fnHelpers.postgres | fnHelpers.mssql | fnHelpers.sqlite | fnHelpers.oracle;
16
+ export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
17
17
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -3,7 +3,7 @@ import type { Knex } from 'knex';
3
3
  import type { DatabaseClient } from '../types/index.js';
4
4
  export default getDatabase;
5
5
  export declare function getDatabase(): Knex;
6
- export declare function getSchemaInspector(): SchemaInspector;
6
+ export declare function getSchemaInspector(database?: Knex): SchemaInspector;
7
7
  /**
8
8
  * Get database version. Value currently exists for MySQL only.
9
9
  *
@@ -143,11 +143,11 @@ export function getDatabase() {
143
143
  });
144
144
  return database;
145
145
  }
146
- export function getSchemaInspector() {
146
+ export function getSchemaInspector(database) {
147
147
  if (inspector) {
148
148
  return inspector;
149
149
  }
150
- const database = getDatabase();
150
+ database ??= getDatabase();
151
151
  inspector = createInspector(database);
152
152
  return inspector;
153
153
  }
@@ -1,5 +1,6 @@
1
1
  import { createInspector } from '@directus/schema';
2
2
  import { useLogger } from '../../logger/index.js';
3
+ import { getDatabaseClient } from '../index.js';
3
4
  /**
4
5
  * Things to keep in mind:
5
6
  *
@@ -99,7 +100,7 @@ export async function up(knex) {
99
100
  * MySQL won't delete the index when you drop the foreign key constraint. Gotta make
100
101
  * sure to clean those up as well
101
102
  */
102
- if (knex.client.constructor.name === 'Client_MySQL') {
103
+ if (getDatabaseClient(knex) === 'mysql') {
103
104
  try {
104
105
  await knex.schema.alterTable(update.table, (table) => {
105
106
  // Knex uses a default convention for index names: `table_column_type`
@@ -140,7 +141,7 @@ export async function down(knex) {
140
141
  * MySQL won't delete the index when you drop the foreign key constraint. Gotta make
141
142
  * sure to clean those up as well
142
143
  */
143
- if (knex.client.constructor.name === 'Client_MySQL') {
144
+ if (getDatabaseClient(knex) === 'mysql') {
144
145
  try {
145
146
  await knex.schema.alterTable(update.table, (table) => {
146
147
  // Knex uses a default convention for index names: `table_column_type`
@@ -1,9 +1,8 @@
1
1
  import { createInspector } from '@directus/schema';
2
2
  import { useLogger } from '../../logger/index.js';
3
- import { getHelpers } from '../helpers/index.js';
3
+ import { getDatabaseClient } from '../index.js';
4
4
  export async function up(knex) {
5
- const helper = getHelpers(knex).schema;
6
- const isMysql = helper.isOneOfClients(['mysql']);
5
+ const isMysql = getDatabaseClient(knex) === 'mysql';
7
6
  if (isMysql) {
8
7
  await dropConstraint(knex);
9
8
  }
@@ -16,8 +15,7 @@ export async function up(knex) {
16
15
  }
17
16
  }
18
17
  export async function down(knex) {
19
- const helper = getHelpers(knex).schema;
20
- const isMysql = helper.isOneOfClients(['mysql']);
18
+ const isMysql = getDatabaseClient(knex) === 'mysql';
21
19
  if (isMysql) {
22
20
  await dropConstraint(knex);
23
21
  }
@@ -1,8 +1,6 @@
1
- import { getHelpers } from '../helpers/index.js';
1
+ import { getDatabaseClient } from '../index.js';
2
2
  export async function up(knex) {
3
- const helper = getHelpers(knex).schema;
4
- const isMysql = helper.isOneOfClients(['mysql']);
5
- if (isMysql) {
3
+ if (getDatabaseClient(knex) === 'mysql') {
6
4
  // Knex creates invalid statement on MySQL, see https://github.com/knex/knex/issues/1888
7
5
  await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `uploaded_on` `created_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
8
6
  }
@@ -20,9 +18,7 @@ export async function down(knex) {
20
18
  await knex.schema.alterTable('directus_files', (table) => {
21
19
  table.dropColumn('uploaded_on');
22
20
  });
23
- const helper = getHelpers(knex).schema;
24
- const isMysql = helper.isOneOfClients(['mysql']);
25
- if (isMysql) {
21
+ if (getDatabaseClient(knex) === 'mysql') {
26
22
  await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `created_on` `uploaded_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
27
23
  }
28
24
  else {
@@ -1,10 +1,12 @@
1
1
  import { processChunk, toBoolean } from '@directus/utils';
2
2
  import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { useLogger } from '../../logger/index.js';
4
5
  import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
5
6
  import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
6
7
  import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
7
8
  import { getSchema } from '../../utils/get-schema.js';
9
+ import { getSchemaInspector } from '../index.js';
8
10
  // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
9
11
  export function mergePermissions(strategy, ...permissions) {
10
12
  const allPermissions = flatten(permissions);
@@ -129,6 +131,7 @@ async function fetchRoleAccess(roles, context) {
129
131
  */
130
132
  const PUBLIC_POLICY_ID = 'abf8a154-5b1c-4a46-ac9c-7300570f4f17';
131
133
  export async function up(knex) {
134
+ const logger = useLogger();
132
135
  /////////////////////////////////////////////////////////////////////////////////////////////////
133
136
  // If the policies table already exists the migration has already run
134
137
  if (await knex.schema.hasTable('directus_policies')) {
@@ -183,10 +186,21 @@ export async function up(knex) {
183
186
  /////////////////////////////////////////////////////////////////////////////////////////////////
184
187
  // Link permissions to policies instead of roles
185
188
  await knex.schema.alterTable('directus_permissions', (table) => {
186
- table.uuid('policy').references('directus_policies.id').onDelete('CASCADE');
187
- // Drop the foreign key constraint here in order to update `null` role to public policy ID
188
- table.dropForeign('role');
189
+ table.uuid('policy');
189
190
  });
191
+ try {
192
+ const inspector = await getSchemaInspector(knex);
193
+ const foreignKeys = await inspector.foreignKeys('directus_permissions');
194
+ const foreignConstraint = foreignKeys.find((foreign) => foreign.foreign_key_table === 'directus_roles' && foreign.column === 'role')
195
+ ?.constraint_name || undefined;
196
+ await knex.schema.alterTable('directus_permissions', (table) => {
197
+ // Drop the foreign key constraint here in order to update `null` role to public policy ID
198
+ table.dropForeign('role', foreignConstraint);
199
+ });
200
+ }
201
+ catch (err) {
202
+ logger.warn('Failed to drop foreign key constraint on `role` column in `directus_permissions` table');
203
+ }
190
204
  await knex('directus_permissions')
191
205
  .update({
192
206
  role: PUBLIC_POLICY_ID,
@@ -198,6 +212,7 @@ export async function up(knex) {
198
212
  await knex.schema.alterTable('directus_permissions', (table) => {
199
213
  table.dropColumns('role');
200
214
  table.dropNullable('policy');
215
+ table.foreign('policy').references('directus_policies.id').onDelete('CASCADE');
201
216
  });
202
217
  /////////////////////////////////////////////////////////////////////////////////////////////////
203
218
  // Setup junction table between roles/users and policies
@@ -1,4 +1,4 @@
1
- import type { Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
4
- export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[]): Knex.QueryBuilder;
4
+ export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): Knex.QueryBuilder;
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
9
9
  import { getNodeAlias } from '../utils/get-field-alias.js';
10
10
  import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
11
11
  import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
12
- export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases) {
12
+ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
13
13
  const aliasMap = Object.create(null);
14
14
  const env = useEnv();
15
- const preProcess = getColumnPreprocessor(knex, schema, table, cases, aliasMap);
15
+ const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
16
16
  const queryCopy = cloneDeep(query);
17
17
  const helpers = getHelpers(knex);
18
18
  const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
@@ -25,7 +25,10 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
25
25
  const groupWhenCases = hasCaseWhen
26
26
  ? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
27
27
  : undefined;
28
- const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, { aliasMap, groupWhenCases }).query;
28
+ const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
29
+ aliasMap,
30
+ groupWhenCases,
31
+ }).query;
29
32
  flatQuery.select(fieldNodes.map((node) => preProcess(node)));
30
33
  withPreprocessBindings(knex, dbQuery);
31
34
  return dbQuery;
@@ -42,7 +45,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
42
45
  hasMultiRelationalSort = sortResult.hasMultiRelationalSort;
43
46
  }
44
47
  }
45
- const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, {
48
+ const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, permissions, {
46
49
  aliasMap,
47
50
  isInnerQuery: true,
48
51
  hasMultiRelationalSort,
@@ -68,6 +71,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
68
71
  cases,
69
72
  table,
70
73
  alias: node.fieldKey,
74
+ permissions,
71
75
  }, { knex, schema });
72
76
  }));
73
77
  }
@@ -159,7 +163,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
159
163
  SELECT ...,
160
164
  CASE WHEN `inner`.<random-prefix>_<alias> > 0 THEN <actual-column> END AS <alias>
161
165
  */
162
- const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, innerCaseWhenAliasPrefix);
166
+ const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, innerCaseWhenAliasPrefix);
163
167
  // To optimize the query we avoid having unnecessary columns in the inner query, that don't have a caseWhen, since
164
168
  // they are selected in the outer query directly
165
169
  dbQuery.select(fieldNodes.map(innerPreprocess).filter((x) => x !== null));
@@ -1,7 +1,7 @@
1
- import type { Item, SchemaOverview } from '@directus/types';
1
+ import type { Accountability, Item, SchemaOverview } from '@directus/types';
2
2
  import type { AST, NestedCollectionNode } from '../../types/ast.js';
3
3
  import type { RunASTOptions } from './types.js';
4
4
  /**
5
5
  * Execute a given AST using Knex. Returns array of items based on requested AST.
6
6
  */
7
- export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, options?: RunASTOptions): Promise<null | Item | Item[]>;
7
+ export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, accountability: Accountability | null, options?: RunASTOptions): Promise<null | Item | Item[]>;
@@ -1,5 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { cloneDeep, merge } from 'lodash-es';
3
+ import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
4
+ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
3
5
  import { PayloadService } from '../../services/payload.js';
4
6
  import getDatabase from '../index.js';
5
7
  import { getDBQuery } from './lib/get-db-query.js';
@@ -10,26 +12,31 @@ import { removeTemporaryFields } from './utils/remove-temporary-fields.js';
10
12
  /**
11
13
  * Execute a given AST using Knex. Returns array of items based on requested AST.
12
14
  */
13
- export async function runAst(originalAST, schema, options) {
15
+ export async function runAst(originalAST, schema, accountability, options) {
14
16
  const ast = cloneDeep(originalAST);
15
17
  const knex = options?.knex || getDatabase();
16
18
  if (ast.type === 'a2o') {
17
19
  const results = {};
18
20
  for (const collection of ast.names) {
19
- results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? []);
21
+ results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? [], accountability);
20
22
  }
21
23
  return results;
22
24
  }
23
25
  else {
24
- return await run(ast.name, ast.children, options?.query || ast.query, ast.cases);
26
+ return await run(ast.name, ast.children, options?.query || ast.query, ast.cases, accountability);
25
27
  }
26
- async function run(collection, children, query, cases) {
28
+ async function run(collection, children, query, cases, accountability) {
27
29
  const env = useEnv();
28
30
  // Retrieve the database columns to select in the current AST
29
31
  const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(schema, collection, children, query);
30
32
  const o2mNodes = nestedCollectionNodes.filter((node) => node.type === 'o2m');
33
+ let permissions = [];
34
+ if (accountability && !accountability.admin) {
35
+ const policies = await fetchPolicies(accountability, { schema, knex });
36
+ permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
37
+ }
31
38
  // The actual knex query builder instance. This is a promise that resolves with the raw items from the db
32
- const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases);
39
+ const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
33
40
  const rawItems = await dbQuery;
34
41
  if (!rawItems)
35
42
  return null;
@@ -72,7 +79,7 @@ export async function runAst(originalAST, schema, options) {
72
79
  page: null,
73
80
  },
74
81
  });
75
- nestedItems = (await runAst(node, schema, { knex, nested: true }));
82
+ nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
76
83
  if (nestedItems) {
77
84
  items = mergeWithParentItems(schema, nestedItems, items, nestedNode, fieldAllowed);
78
85
  }
@@ -86,7 +93,7 @@ export async function runAst(originalAST, schema, options) {
86
93
  const node = merge({}, nestedNode, {
87
94
  query: { limit: -1 },
88
95
  });
89
- nestedItems = (await runAst(node, schema, { knex, nested: true }));
96
+ nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
90
97
  if (nestedItems) {
91
98
  // Merge all fetched nested records with the parent items
92
99
  items = mergeWithParentItems(schema, nestedItems, items, nestedNode, true);
@@ -1,4 +1,4 @@
1
- import type { Filter, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { AliasMap } from '../../../utils/get-column-path.js';
4
4
  export interface ApplyCaseWhenOptions {
@@ -8,9 +8,10 @@ export interface ApplyCaseWhenOptions {
8
8
  cases: Filter[];
9
9
  aliasMap: AliasMap;
10
10
  alias?: string;
11
+ permissions: Permission[];
11
12
  }
12
13
  export interface ApplyCaseWhenContext {
13
14
  knex: Knex;
14
15
  schema: SchemaOverview;
15
16
  }
16
- export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
17
+ export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
@@ -1,7 +1,7 @@
1
1
  import { applyFilter } from '../../../utils/apply-query.js';
2
- export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }, { knex, schema }) {
2
+ export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }, { knex, schema }) {
3
3
  const caseQuery = knex.queryBuilder();
4
- applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases);
4
+ applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases, permissions);
5
5
  const compiler = knex.client.queryCompiler(caseQuery);
6
6
  const sqlParts = [];
7
7
  // Only empty filters, so no where was generated, skip it
@@ -1,4 +1,4 @@
1
- import type { Filter, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { FieldNode, FunctionFieldNode, M2ONode } from '../../../types/ast.js';
4
4
  import type { AliasMap } from '../../../utils/get-column-path.js';
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
6
6
  /** Don't assign an alias to the column but instead return the column as is */
7
7
  noAlias?: boolean;
8
8
  }
9
- export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
9
+ export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
10
10
  export {};