@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.
- package/dist/cli/commands/init/questions.d.ts +7 -6
- package/dist/cli/commands/init/questions.js +2 -2
- package/dist/cli/utils/create-env/index.d.ts +2 -2
- package/dist/cli/utils/create-env/index.js +3 -1
- package/dist/cli/utils/drivers.js +1 -1
- package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
- package/dist/database/helpers/fn/types.d.ts +6 -3
- package/dist/database/helpers/fn/types.js +2 -2
- package/dist/database/helpers/index.d.ts +3 -3
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
- package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
- package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
- package/dist/database/migrations/20240806A-permissions-policies.js +18 -3
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +9 -5
- package/dist/database/run-ast/run-ast.d.ts +2 -2
- package/dist/database/run-ast/run-ast.js +14 -7
- package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
- package/dist/database/run-ast/utils/apply-case-when.js +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
- package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
- package/dist/permissions/lib/fetch-permissions.js +5 -39
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
- package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
- package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
- package/dist/permissions/modules/process-payload/process-payload.js +4 -5
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
- package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
- package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
- package/dist/server.js +17 -4
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +22 -19
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/meta.js +8 -7
- package/dist/services/permissions.js +19 -19
- package/dist/types/database.d.ts +1 -1
- package/dist/utils/apply-query.d.ts +3 -3
- package/dist/utils/apply-query.js +25 -20
- package/dist/utils/get-address.d.ts +5 -0
- package/dist/utils/get-address.js +13 -0
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/transaction.js +28 -11
- package/dist/websocket/controllers/graphql.js +2 -3
- package/dist/websocket/controllers/rest.js +2 -3
- package/package.json +17 -16
package/dist/services/fields.js
CHANGED
|
@@ -621,7 +621,7 @@ export class FieldsService {
|
|
|
621
621
|
}
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
|
-
addColumnToTable(table, field,
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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 (
|
|
671
|
-
|
|
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(
|
|
680
|
+
column.defaultTo(defaultValue);
|
|
678
681
|
}
|
|
679
682
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
702
|
+
if (existing && existing.is_unique === true) {
|
|
700
703
|
table.dropUnique([field.field]);
|
|
701
704
|
}
|
|
702
705
|
}
|
|
703
|
-
if (
|
|
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({
|
package/dist/services/items.js
CHANGED
|
@@ -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,
|
package/dist/services/meta.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
127
|
+
itemPermissions.update.fields = fields;
|
|
128
|
+
itemPermissions.update.presets = presets;
|
|
129
129
|
}
|
|
130
130
|
return itemPermissions;
|
|
131
131
|
}
|
package/dist/types/database.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type Driver = '
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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,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
|
|
4
|
-
query
|
|
5
|
-
cases
|
|
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
|
package/dist/utils/get-column.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
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://${
|
|
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://${
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
152
|
-
"@directus/
|
|
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/
|
|
157
|
-
"@directus/
|
|
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/
|
|
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": "
|
|
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": {
|