@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
@@ -621,7 +621,7 @@ export class FieldsService {
621
621
  }
622
622
  }
623
623
  }
624
- addColumnToTable(table, field, alter = null) {
624
+ addColumnToTable(table, field, existing = null) {
625
625
  let column;
626
626
  // Don't attempt to add a DB column for alias / corrupt fields
627
627
  if (field.type === 'alias' || field.type === 'unknown')
@@ -662,45 +662,48 @@ export class FieldsService {
662
662
  else {
663
663
  throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
664
664
  }
665
- if (field.schema?.default_value !== undefined) {
666
- if (typeof field.schema.default_value === 'string' &&
667
- (field.schema.default_value.toLowerCase() === 'now()' || field.schema.default_value === 'CURRENT_TIMESTAMP')) {
665
+ const defaultValue = field.schema?.default_value !== undefined ? field.schema?.default_value : existing?.default_value;
666
+ if (defaultValue) {
667
+ const newDefaultValueIsString = typeof defaultValue === 'string';
668
+ const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
669
+ const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
670
+ const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp;
671
+ const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')');
672
+ if (newDefaultIsSetToCurrentTime) {
668
673
  column.defaultTo(this.knex.fn.now());
669
674
  }
670
- else if (typeof field.schema.default_value === 'string' &&
671
- field.schema.default_value.includes('CURRENT_TIMESTAMP(') &&
672
- field.schema.default_value.includes(')')) {
673
- const precision = field.schema.default_value.match(REGEX_BETWEEN_PARENS)[1];
675
+ else if (newDefaultIsTimestampWithPrecision) {
676
+ const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1];
674
677
  column.defaultTo(this.knex.fn.now(Number(precision)));
675
678
  }
676
679
  else {
677
- column.defaultTo(field.schema.default_value);
680
+ column.defaultTo(defaultValue);
678
681
  }
679
682
  }
680
- if (field.schema?.is_nullable === false) {
681
- if (!alter || alter.is_nullable === true) {
682
- column.notNullable();
683
- }
683
+ else {
684
+ column.defaultTo(null);
685
+ }
686
+ const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
687
+ if (isNullable) {
688
+ column.nullable();
684
689
  }
685
690
  else {
686
- if (!alter || alter.is_nullable === false) {
687
- column.nullable();
688
- }
691
+ column.notNullable();
689
692
  }
690
693
  if (field.schema?.is_primary_key) {
691
694
  column.primary().notNullable();
692
695
  }
693
696
  else if (field.schema?.is_unique === true) {
694
- if (!alter || alter.is_unique === false) {
697
+ if (!existing || existing.is_unique === false) {
695
698
  column.unique();
696
699
  }
697
700
  }
698
701
  else if (field.schema?.is_unique === false) {
699
- if (alter && alter.is_unique === true) {
702
+ if (existing && existing.is_unique === true) {
700
703
  table.dropUnique([field.field]);
701
704
  }
702
705
  }
703
- if (alter) {
706
+ if (existing) {
704
707
  column.alter();
705
708
  }
706
709
  }
@@ -133,6 +133,8 @@ export class ImportService {
133
133
  };
134
134
  const PapaOptions = {
135
135
  header: true,
136
+ // Trim whitespaces in headers, including the byte order mark (BOM) zero-width no-break space
137
+ transformHeader: (header) => header.trim(),
136
138
  transform,
137
139
  };
138
140
  return new Promise((resolve, reject) => {
@@ -304,7 +306,6 @@ export class ExportService {
304
306
  const savedFile = await filesService.uploadOne(createReadStream(tmpFile.path), fileWithDefaults);
305
307
  if (this.accountability?.user) {
306
308
  const notificationsService = new NotificationsService({
307
- accountability: this.accountability,
308
309
  schema: this.schema,
309
310
  });
310
311
  const usersService = new UsersService({
@@ -333,7 +334,6 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
333
334
  logger.error(err, `Couldn't export ${collection}: ${err.message}`);
334
335
  if (this.accountability?.user) {
335
336
  const notificationsService = new NotificationsService({
336
- accountability: this.accountability,
337
337
  schema: this.schema,
338
338
  });
339
339
  await notificationsService.createOne({
@@ -370,7 +370,7 @@ export class ItemsService {
370
370
  knex: this.knex,
371
371
  });
372
372
  ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
373
- const records = await runAst(ast, this.schema, {
373
+ const records = await runAst(ast, this.schema, this.accountability, {
374
374
  knex: this.knex,
375
375
  // GraphQL requires relational keys to be returned regardless
376
376
  stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
@@ -45,14 +45,14 @@ export class MetaService {
45
45
  action: 'read',
46
46
  policies,
47
47
  accountability: this.accountability,
48
- ...(collection ? { collections: [collection] } : {}),
49
48
  }, context);
50
- const rules = dedupeAccess(permissions);
49
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
50
+ const rules = dedupeAccess(collectionPermissions);
51
51
  const cases = rules.map(({ rule }) => rule);
52
52
  const filter = {
53
53
  _or: cases,
54
54
  };
55
- const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases);
55
+ const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions);
56
56
  hasJoins = result.hasJoins;
57
57
  }
58
58
  if (hasJoins) {
@@ -70,6 +70,7 @@ export class MetaService {
70
70
  let filter = query.filter || {};
71
71
  let hasJoins = false;
72
72
  let cases = [];
73
+ let permissions = [];
73
74
  if (this.accountability && this.accountability.admin === false) {
74
75
  const context = { knex: this.knex, schema: this.schema };
75
76
  await validateAccess({
@@ -78,13 +79,13 @@ export class MetaService {
78
79
  collection,
79
80
  }, context);
80
81
  const policies = await fetchPolicies(this.accountability, context);
81
- const permissions = await fetchPermissions({
82
+ permissions = await fetchPermissions({
82
83
  action: 'read',
83
84
  policies,
84
85
  accountability: this.accountability,
85
- ...(collection ? { collections: [collection] } : {}),
86
86
  }, context);
87
- const rules = dedupeAccess(permissions);
87
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
88
+ const rules = dedupeAccess(collectionPermissions);
88
89
  cases = rules.map(({ rule }) => rule);
89
90
  const permissionsFilter = {
90
91
  _or: cases,
@@ -97,7 +98,7 @@ export class MetaService {
97
98
  }
98
99
  }
99
100
  if (Object.keys(filter).length > 0) {
100
- ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases));
101
+ ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions));
101
102
  }
102
103
  if (query.search) {
103
104
  applySearch(this.knex, this.schema, dbQuery, query.search, collection);
@@ -1,5 +1,8 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
+ import { uniq } from 'lodash-es';
2
3
  import { clearSystemCache } from '../cache.js';
4
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
5
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
3
6
  import { withAppMinimalPermissions } from '../permissions/lib/with-app-minimal-permissions.js';
4
7
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
5
8
  import { ItemsService } from './items.js';
@@ -105,27 +108,24 @@ export class PermissionsService extends ItemsService {
105
108
  .catch(() => { });
106
109
  }));
107
110
  if (schema?.singleton && itemPermissions.update.access) {
108
- const query = {
109
- filter: {
110
- _and: [
111
- ...(this.accountability?.role ? [{ role: { _eq: this.accountability.role } }] : []),
112
- { collection: { _eq: collection } },
113
- { action: { _eq: updateAction } },
114
- ],
115
- },
116
- fields: ['presets', 'fields'],
117
- };
118
- try {
119
- const result = await this.readByQuery(query);
120
- const permission = result[0];
121
- if (permission) {
122
- itemPermissions.update.presets = permission['presets'];
123
- itemPermissions.update.fields = permission['fields'];
111
+ const context = { schema: this.schema, knex: this.knex };
112
+ const policies = await fetchPolicies(this.accountability, context);
113
+ const permissions = await fetchPermissions({ policies, accountability: this.accountability, action: updateAction, collections: [collection] }, context);
114
+ let fields = [];
115
+ let presets = {};
116
+ for (const permission of permissions) {
117
+ if (permission.fields && fields[0] !== '*') {
118
+ fields = uniq([...fields, ...permission.fields]);
119
+ if (fields.includes('*')) {
120
+ fields = ['*'];
121
+ }
122
+ }
123
+ if (permission.presets) {
124
+ presets = { ...(presets ?? {}), ...permission.presets };
124
125
  }
125
126
  }
126
- catch {
127
- // No permission
128
- }
127
+ itemPermissions.update.fields = fields;
128
+ itemPermissions.update.presets = presets;
129
129
  }
130
130
  return itemPermissions;
131
131
  }
@@ -1,3 +1,3 @@
1
- export type Driver = 'mysql' | 'pg' | 'cockroachdb' | 'sqlite3' | 'oracledb' | 'mssql';
1
+ export type Driver = 'mysql2' | 'pg' | 'cockroachdb' | 'sqlite3' | 'oracledb' | 'mssql';
2
2
  export declare const DatabaseClients: readonly ["mysql", "postgres", "cockroachdb", "sqlite", "oracle", "mssql", "redshift"];
3
3
  export type DatabaseClient = (typeof DatabaseClients)[number];
@@ -1,4 +1,4 @@
1
- import type { Aggregate, Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Aggregate, Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { AliasMap } from './get-column-path.js';
4
4
  export declare const generateAlias: (size?: number | undefined) => string;
@@ -11,7 +11,7 @@ type ApplyQueryOptions = {
11
11
  /**
12
12
  * Apply the Query to a given Knex query builder instance
13
13
  */
14
- export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], options?: ApplyQueryOptions): {
14
+ export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], permissions: Permission[], options?: ApplyQueryOptions): {
15
15
  query: Knex.QueryBuilder<any, any>;
16
16
  hasJoins: boolean;
17
17
  hasMultiRelationalFilter: boolean;
@@ -34,7 +34,7 @@ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery:
34
34
  };
35
35
  export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
36
36
  export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
37
- export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[]): {
37
+ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[], permissions: Permission[]): {
38
38
  query: Knex.QueryBuilder<any, any>;
39
39
  hasJoins: boolean;
40
40
  hasMultiRelationalFilter: boolean;
@@ -5,6 +5,7 @@ import { clone, isPlainObject } from 'lodash-es';
5
5
  import { customAlphabet } from 'nanoid/non-secure';
6
6
  import { getHelpers } from '../database/helpers/index.js';
7
7
  import { applyCaseWhen } from '../database/run-ast/utils/apply-case-when.js';
8
+ import { getCases } from '../permissions/modules/process-ast/lib/get-cases.js';
8
9
  import { getColumnPath } from './get-column-path.js';
9
10
  import { getColumn } from './get-column.js';
10
11
  import { getRelationInfo } from './get-relation-info.js';
@@ -15,7 +16,7 @@ export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
15
16
  /**
16
17
  * Apply the Query to a given Knex query builder instance
17
18
  */
18
- export default function applyQuery(knex, collection, dbQuery, query, schema, cases, options) {
19
+ export default function applyQuery(knex, collection, dbQuery, query, schema, cases, permissions, options) {
19
20
  const aliasMap = options?.aliasMap ?? Object.create(null);
20
21
  let hasJoins = false;
21
22
  let hasMultiRelationalFilter = false;
@@ -42,7 +43,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
42
43
  // you're actually allowed to read
43
44
  const filter = joinFilterWithCases(query.filter, cases);
44
45
  if (filter) {
45
- const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases);
46
+ const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases, permissions);
46
47
  if (!hasJoins) {
47
48
  hasJoins = filterResult.hasJoins;
48
49
  }
@@ -58,6 +59,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
58
59
  aliasMap,
59
60
  cases,
60
61
  table: collection,
62
+ permissions,
61
63
  }, {
62
64
  knex,
63
65
  schema,
@@ -261,7 +263,7 @@ export function applyOffset(knex, rootQuery, offset) {
261
263
  getHelpers(knex).schema.applyOffset(rootQuery, offset);
262
264
  }
263
265
  }
264
- export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases) {
266
+ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases, permissions) {
265
267
  const helpers = getHelpers(knex);
266
268
  const relations = schema.relations;
267
269
  let hasJoins = false;
@@ -349,24 +351,27 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
349
351
  if (relationType === 'o2a') {
350
352
  pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
351
353
  }
352
- const subQueryBuilder = (filter) => (subQueryKnex) => {
353
- const field = relation.field;
354
- const collection = relation.collection;
355
- const column = `${collection}.${field}`;
356
- subQueryKnex
357
- .select({ [field]: column })
358
- .from(collection)
359
- .whereNotNull(column);
360
- applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases);
361
- };
362
354
  const childKey = Object.keys(value)?.[0];
363
- if (childKey === '_none') {
364
- dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0]));
365
- continue;
366
- }
367
- else if (childKey === '_some') {
368
- dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0]));
369
- continue;
355
+ if (childKey === '_none' || childKey === '_some') {
356
+ const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
357
+ const field = relation.field;
358
+ const collection = relation.collection;
359
+ const column = `${collection}.${field}`;
360
+ subQueryKnex
361
+ .select({ [field]: column })
362
+ .from(collection)
363
+ .whereNotNull(column);
364
+ applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
365
+ };
366
+ const { cases: subCases } = getCases(relation.collection, permissions, []);
367
+ if (childKey === '_none') {
368
+ dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
369
+ continue;
370
+ }
371
+ else if (childKey === '_some') {
372
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
373
+ continue;
374
+ }
370
375
  }
371
376
  }
372
377
  if (filterPath.includes('_none') || filterPath.includes('_some')) {
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node/http.js" />
3
+ /// <reference types="pino-http" />
4
+ import * as http from 'http';
5
+ export declare function getAddress(server: http.Server): string | undefined;
@@ -0,0 +1,13 @@
1
+ import * as http from 'http';
2
+ export function getAddress(server) {
3
+ const address = server.address();
4
+ if (address === null) {
5
+ // Before the 'listening' event has been emitted or after calling server.close()
6
+ return;
7
+ }
8
+ if (typeof address === 'string') {
9
+ // unix path
10
+ return address;
11
+ }
12
+ return `${address.address}:${address.port}`;
13
+ }
@@ -1,10 +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
- type GetColumnOptions = {
4
- query?: Query | undefined;
5
- cases?: Filter[];
3
+ type FunctionColumnOptions = {
4
+ query: Query;
5
+ cases: Filter[];
6
+ permissions: Permission[];
7
+ };
8
+ type OriginalCollectionName = {
6
9
  originalCollectionName?: string | undefined;
7
10
  };
11
+ type GetColumnOptions = OriginalCollectionName | (FunctionColumnOptions & OriginalCollectionName);
8
12
  /**
9
13
  * Return column prefixed by table. If column includes functions (like `year(date_created)`), the
10
14
  * column is replaced with the appropriate SQL
@@ -29,8 +29,13 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
29
29
  }
30
30
  const result = fn[functionName](table, columnName, {
31
31
  type,
32
- query: options?.query,
33
- cases: options?.cases,
32
+ relationalCountOptions: isFunctionColumnOptions(options)
33
+ ? {
34
+ query: options.query,
35
+ cases: options.cases,
36
+ permissions: options.permissions,
37
+ }
38
+ : undefined,
34
39
  originalCollectionName: options?.originalCollectionName,
35
40
  });
36
41
  if (alias) {
@@ -47,3 +52,6 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
47
52
  }
48
53
  return knex.ref(`${table}.${column}`);
49
54
  }
55
+ function isFunctionColumnOptions(options) {
56
+ return !!options && 'query' in options;
57
+ }
@@ -95,7 +95,7 @@ function sanitizeAggregate(rawAggregate) {
95
95
  aggregate = parseJSON(rawAggregate);
96
96
  }
97
97
  catch {
98
- logger.warn('Invalid value passed for filter query parameter.');
98
+ logger.warn('Invalid value passed for aggregate query parameter.');
99
99
  }
100
100
  }
101
101
  for (const [operation, fields] of Object.entries(aggregate)) {
@@ -1,3 +1,4 @@
1
+ import { isObject } from '@directus/utils';
1
2
  import {} from 'knex';
2
3
  import { getDatabaseClient } from '../database/index.js';
3
4
  import { useLogger } from '../logger/index.js';
@@ -18,16 +19,7 @@ export const transaction = async (knex, handler) => {
18
19
  }
19
20
  catch (error) {
20
21
  const client = getDatabaseClient(knex);
21
- /**
22
- * This error code indicates that the transaction failed due to another
23
- * concurrent or recent transaction attempting to write to the same data.
24
- * This can usually be solved by restarting the transaction on client-side
25
- * after a short delay, so that it is executed against the latest state.
26
- *
27
- * @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
28
- */
29
- const COCKROACH_RETRY_ERROR_CODE = '40001';
30
- if (client !== 'cockroachdb' || error?.code !== COCKROACH_RETRY_ERROR_CODE)
22
+ if (!shouldRetryTransaction(client, error))
31
23
  throw error;
32
24
  const MAX_ATTEMPTS = 3;
33
25
  const BASE_DELAY = 100;
@@ -40,7 +32,7 @@ export const transaction = async (knex, handler) => {
40
32
  return await knex.transaction((trx) => handler(trx));
41
33
  }
42
34
  catch (error) {
43
- if (error?.code !== COCKROACH_RETRY_ERROR_CODE)
35
+ if (!shouldRetryTransaction(client, error))
44
36
  throw error;
45
37
  }
46
38
  }
@@ -50,3 +42,28 @@ export const transaction = async (knex, handler) => {
50
42
  }
51
43
  }
52
44
  };
45
+ function shouldRetryTransaction(client, error) {
46
+ /**
47
+ * This error code indicates that the transaction failed due to another
48
+ * concurrent or recent transaction attempting to write to the same data.
49
+ * This can usually be solved by restarting the transaction on client-side
50
+ * after a short delay, so that it is executed against the latest state.
51
+ *
52
+ * @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
53
+ */
54
+ const COCKROACH_RETRY_ERROR_CODE = '40001';
55
+ /**
56
+ * SQLITE_BUSY is an error code returned by SQLite when an operation can't be
57
+ * performed due to a locked database file. This often arises due to multiple
58
+ * processes trying to simultaneously access the database, causing potential
59
+ * data inconsistencies. There are a few mechanisms to handle this case,
60
+ * one of which is to retry the complete transaction again
61
+ * on client-side after a short delay.
62
+ *
63
+ * @link https://www.sqlite.org/rescode.html#busy
64
+ */
65
+ const SQLITE_BUSY_ERROR_CODE = 'SQLITE_BUSY';
66
+ return (isObject(error) &&
67
+ ((client === 'cockroachdb' && error['code'] === COCKROACH_RETRY_ERROR_CODE) ||
68
+ (client === 'sqlite' && error['code'] === SQLITE_BUSY_ERROR_CODE)));
69
+ }
@@ -1,9 +1,9 @@
1
- import { useEnv } from '@directus/env';
2
1
  import { CloseCode, MessageType, makeServer } from 'graphql-ws';
3
2
  import { useLogger } from '../../logger/index.js';
4
3
  import { bindPubSub } from '../../services/graphql/subscription.js';
5
4
  import { GraphQLService } from '../../services/index.js';
6
5
  import { getSchema } from '../../utils/get-schema.js';
6
+ import { getAddress } from '../../utils/get-address.js';
7
7
  import { authenticateConnection } from '../authenticate.js';
8
8
  import { handleWebSocketError } from '../errors.js';
9
9
  import { ConnectionParams, WebSocketMessage } from '../messages.js';
@@ -14,7 +14,6 @@ export class GraphQLSubscriptionController extends SocketController {
14
14
  gql;
15
15
  constructor(httpServer) {
16
16
  super(httpServer, 'WEBSOCKETS_GRAPHQL');
17
- const env = useEnv();
18
17
  this.server.on('connection', (ws, auth) => {
19
18
  this.bindEvents(this.createClient(ws, auth));
20
19
  });
@@ -31,7 +30,7 @@ export class GraphQLSubscriptionController extends SocketController {
31
30
  },
32
31
  });
33
32
  bindPubSub();
34
- logger.info(`GraphQL Subscriptions started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
33
+ logger.info(`GraphQL Subscriptions started at ws://${getAddress(httpServer)}${this.endpoint}`);
35
34
  }
36
35
  bindEvents(client) {
37
36
  const closedHandler = this.gql.opened({
@@ -1,7 +1,7 @@
1
- import { useEnv } from '@directus/env';
2
1
  import { parseJSON } from '@directus/utils';
3
2
  import emitter from '../../emitter.js';
4
3
  import { useLogger } from '../../logger/index.js';
4
+ import { getAddress } from '../../utils/get-address.js';
5
5
  import { WebSocketError, handleWebSocketError } from '../errors.js';
6
6
  import { WebSocketMessage } from '../messages.js';
7
7
  import SocketController from './base.js';
@@ -9,11 +9,10 @@ const logger = useLogger();
9
9
  export class WebSocketController extends SocketController {
10
10
  constructor(httpServer) {
11
11
  super(httpServer, 'WEBSOCKETS_REST');
12
- const env = useEnv();
13
12
  this.server.on('connection', (ws, auth) => {
14
13
  this.bindEvents(this.createClient(ws, auth));
15
14
  });
16
- logger.info(`WebSocket Server started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
15
+ logger.info(`WebSocket Server started at ws://${getAddress(httpServer)}${this.endpoint}`);
17
16
  }
18
17
  bindEvents(client) {
19
18
  client.on('parsed-message', async (message) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "22.0.0",
3
+ "version": "22.1.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -99,7 +99,7 @@
99
99
  "graphql-ws": "5.16.0",
100
100
  "helmet": "7.1.0",
101
101
  "icc": "3.0.0",
102
- "inquirer": "9.3.5",
102
+ "inquirer": "9.3.6",
103
103
  "ioredis": "5.4.1",
104
104
  "ip-matching": "2.1.2",
105
105
  "isolated-vm": "4.7.2",
@@ -143,35 +143,35 @@
143
143
  "sharp": "0.33.4",
144
144
  "snappy": "7.2.2",
145
145
  "stream-json": "1.8.0",
146
- "tar": "7.4.2",
146
+ "tar": "7.4.3",
147
147
  "tsx": "4.16.5",
148
148
  "wellknown": "0.5.0",
149
149
  "ws": "8.18.0",
150
150
  "zod": "3.23.8",
151
- "zod-validation-error": "3.3.0",
152
- "@directus/constants": "12.0.0",
153
- "@directus/app": "13.0.0",
154
- "@directus/env": "2.0.0",
151
+ "zod-validation-error": "3.3.1",
152
+ "@directus/app": "13.1.0",
155
153
  "@directus/errors": "1.0.0",
156
- "@directus/format-title": "11.0.0",
157
- "@directus/extensions": "2.0.0",
158
- "@directus/extensions-sdk": "12.0.0",
154
+ "@directus/env": "3.0.0",
155
+ "@directus/constants": "12.0.0",
159
156
  "@directus/extensions-registry": "2.0.0",
160
- "@directus/pressure": "2.0.0",
157
+ "@directus/extensions": "2.0.0",
158
+ "@directus/extensions-sdk": "12.0.1",
159
+ "@directus/format-title": "11.0.0",
161
160
  "@directus/memory": "2.0.0",
162
161
  "@directus/schema": "12.0.0",
162
+ "@directus/pressure": "2.0.0",
163
163
  "@directus/specs": "11.0.0",
164
164
  "@directus/storage": "11.0.0",
165
165
  "@directus/storage-driver-azure": "11.0.0",
166
- "@directus/storage-driver-gcs": "11.0.0",
167
- "@directus/storage-driver-local": "11.0.0",
168
166
  "@directus/storage-driver-cloudinary": "11.0.0",
167
+ "@directus/storage-driver-gcs": "11.0.0",
169
168
  "@directus/storage-driver-s3": "11.0.0",
169
+ "@directus/storage-driver-local": "11.0.0",
170
170
  "@directus/storage-driver-supabase": "2.0.0",
171
171
  "@directus/system-data": "2.0.0",
172
- "@directus/validation": "1.0.0",
173
172
  "@directus/utils": "12.0.0",
174
- "directus": "11.0.0"
173
+ "@directus/validation": "1.0.0",
174
+ "directus": "11.0.2"
175
175
  },
176
176
  "devDependencies": {
177
177
  "@ngneat/falso": "7.2.0",
@@ -209,11 +209,12 @@
209
209
  "@vitest/coverage-v8": "1.5.3",
210
210
  "copyfiles": "2.4.1",
211
211
  "form-data": "4.0.0",
212
+ "get-port": "7.1.0",
212
213
  "knex-mock-client": "2.0.1",
213
214
  "typescript": "5.4.5",
214
215
  "vitest": "1.5.3",
215
- "@directus/random": "1.0.0",
216
216
  "@directus/tsconfig": "2.0.0",
217
+ "@directus/random": "1.0.0",
217
218
  "@directus/types": "12.0.0"
218
219
  },
219
220
  "optionalDependencies": {