@directus/api 22.1.0 → 22.2.0
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/app.js +1 -1
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +2 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- 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 +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
- package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
- package/dist/database/helpers/nullable-update/index.d.ts +7 -0
- package/dist/database/helpers/nullable-update/index.js +7 -0
- package/dist/database/helpers/nullable-update/types.d.ts +7 -0
- package/dist/database/helpers/nullable-update/types.js +12 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +20 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +33 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/oracle.js +21 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/postgres.js +23 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
- package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
- package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +23 -13
- 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/extensions/manager.js +2 -2
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +11 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/middleware/respond.js +1 -0
- 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/request/is-denied-ip.js +7 -1
- package/dist/server.js +4 -2
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +66 -25
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/mail/index.js +1 -5
- package/dist/services/meta.js +8 -7
- package/dist/services/notifications.d.ts +0 -4
- package/dist/services/notifications.js +8 -6
- package/dist/services/permissions.js +19 -19
- package/dist/services/server.js +8 -1
- package/dist/services/specifications.js +7 -7
- package/dist/services/users.js +4 -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 +1 -1
- package/dist/utils/get-address.js +6 -1
- package/dist/utils/get-allowed-log-levels.d.ts +3 -0
- package/dist/utils/get-allowed-log-levels.js +11 -0
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/get-schema.js +19 -24
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/controllers/base.d.ts +10 -10
- package/dist/websocket/controllers/base.js +22 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -0
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +18 -0
- package/dist/websocket/controllers/logs.js +50 -0
- package/dist/websocket/controllers/rest.js +3 -1
- package/dist/websocket/handlers/index.d.ts +1 -0
- package/dist/websocket/handlers/index.js +21 -3
- package/dist/websocket/handlers/logs.d.ts +31 -0
- package/dist/websocket/handlers/logs.js +121 -0
- package/dist/websocket/messages.d.ts +26 -0
- package/dist/websocket/messages.js +9 -0
- package/dist/websocket/types.d.ts +7 -0
- package/package.json +27 -26
package/dist/services/fields.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createInspector } from '@directus/schema';
|
|
|
5
5
|
import { addFieldFlag, toArray } from '@directus/utils';
|
|
6
6
|
import { isEqual, isNil, merge } from 'lodash-es';
|
|
7
7
|
import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
|
|
8
|
-
import { ALIAS_TYPES } from '../constants.js';
|
|
8
|
+
import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js';
|
|
9
9
|
import { translateDatabaseError } from '../database/errors/translate.js';
|
|
10
10
|
import { getHelpers } from '../database/helpers/index.js';
|
|
11
11
|
import getDatabase, { getSchemaInspector } from '../database/index.js';
|
|
@@ -384,8 +384,16 @@ export class FieldsService {
|
|
|
384
384
|
}
|
|
385
385
|
if (hookAdjustedField.schema) {
|
|
386
386
|
const existingColumn = await this.columnInfo(collection, hookAdjustedField.field);
|
|
387
|
-
if (
|
|
388
|
-
|
|
387
|
+
if (existingColumn.is_primary_key) {
|
|
388
|
+
if (hookAdjustedField.schema?.is_nullable === true) {
|
|
389
|
+
throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
|
|
390
|
+
}
|
|
391
|
+
if (hookAdjustedField.schema?.is_unique === false) {
|
|
392
|
+
throw new InvalidPayloadError({ reason: 'Primary key must be unique' });
|
|
393
|
+
}
|
|
394
|
+
if (hookAdjustedField.schema?.is_indexed === true) {
|
|
395
|
+
throw new InvalidPayloadError({ reason: 'Primary key cannot be indexed' });
|
|
396
|
+
}
|
|
389
397
|
}
|
|
390
398
|
// Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
|
|
391
399
|
const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
|
|
@@ -621,7 +629,7 @@ export class FieldsService {
|
|
|
621
629
|
}
|
|
622
630
|
}
|
|
623
631
|
}
|
|
624
|
-
addColumnToTable(table, field,
|
|
632
|
+
addColumnToTable(table, field, existing = null) {
|
|
625
633
|
let column;
|
|
626
634
|
// Don't attempt to add a DB column for alias / corrupt fields
|
|
627
635
|
if (field.type === 'alias' || field.type === 'unknown')
|
|
@@ -662,45 +670,78 @@ export class FieldsService {
|
|
|
662
670
|
else {
|
|
663
671
|
throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
|
|
664
672
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
673
|
+
const setDefaultValue = (defaultValue) => {
|
|
674
|
+
const newDefaultValueIsString = typeof defaultValue === 'string';
|
|
675
|
+
const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
|
|
676
|
+
const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
|
|
677
|
+
const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp;
|
|
678
|
+
const newDefaultIsAFunction = newDefaultValueIsString && ALLOWED_DB_DEFAULT_FUNCTIONS.includes(defaultValue);
|
|
679
|
+
const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')');
|
|
680
|
+
if (newDefaultIsSetToCurrentTime) {
|
|
668
681
|
column.defaultTo(this.knex.fn.now());
|
|
669
682
|
}
|
|
670
|
-
else if (
|
|
671
|
-
|
|
672
|
-
field.schema.default_value.includes(')')) {
|
|
673
|
-
const precision = field.schema.default_value.match(REGEX_BETWEEN_PARENS)[1];
|
|
683
|
+
else if (newDefaultIsTimestampWithPrecision) {
|
|
684
|
+
const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1];
|
|
674
685
|
column.defaultTo(this.knex.fn.now(Number(precision)));
|
|
675
686
|
}
|
|
687
|
+
else if (newDefaultIsAFunction) {
|
|
688
|
+
column.defaultTo(this.knex.raw(defaultValue));
|
|
689
|
+
}
|
|
676
690
|
else {
|
|
677
|
-
column.defaultTo(
|
|
691
|
+
column.defaultTo(defaultValue);
|
|
678
692
|
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
693
|
+
};
|
|
694
|
+
// for a new item, set the default value and nullable as provided without any further considerations
|
|
695
|
+
if (!existing) {
|
|
696
|
+
if (field.schema?.default_value !== undefined) {
|
|
697
|
+
setDefaultValue(field.schema.default_value);
|
|
698
|
+
}
|
|
699
|
+
if (field.schema?.is_nullable || field.schema?.is_nullable === undefined) {
|
|
700
|
+
column.nullable();
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
682
703
|
column.notNullable();
|
|
683
704
|
}
|
|
684
705
|
}
|
|
685
706
|
else {
|
|
686
|
-
if
|
|
687
|
-
|
|
707
|
+
// for an existing item: if nullable option changed, we have to provide the default values as well and actually vice versa
|
|
708
|
+
// see https://knexjs.org/guide/schema-builder.html#alter
|
|
709
|
+
// To overwrite a nullable option with the same value this is not possible for Oracle though, hence the DB helper
|
|
710
|
+
if (field.schema?.default_value !== undefined || field.schema?.is_nullable !== undefined) {
|
|
711
|
+
this.helpers.nullableUpdate.updateNullableValue(column, field, existing);
|
|
712
|
+
let defaultValue = null;
|
|
713
|
+
if (field.schema?.default_value !== undefined) {
|
|
714
|
+
defaultValue = field.schema.default_value;
|
|
715
|
+
}
|
|
716
|
+
else if (existing.default_value !== undefined) {
|
|
717
|
+
defaultValue = existing.default_value;
|
|
718
|
+
}
|
|
719
|
+
setDefaultValue(defaultValue);
|
|
688
720
|
}
|
|
689
721
|
}
|
|
690
722
|
if (field.schema?.is_primary_key) {
|
|
691
723
|
column.primary().notNullable();
|
|
692
724
|
}
|
|
693
|
-
else if (
|
|
694
|
-
|
|
695
|
-
|
|
725
|
+
else if (!existing?.is_primary_key) {
|
|
726
|
+
// primary key will already have unique/index constraints
|
|
727
|
+
if (field.schema?.is_unique === true) {
|
|
728
|
+
if (!existing || existing.is_unique === false) {
|
|
729
|
+
column.unique();
|
|
730
|
+
}
|
|
696
731
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
732
|
+
else if (field.schema?.is_unique === false) {
|
|
733
|
+
if (existing && existing.is_unique === true) {
|
|
734
|
+
table.dropUnique([field.field]);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (field.schema?.is_indexed === true && !existing?.is_indexed) {
|
|
738
|
+
column.index();
|
|
739
|
+
}
|
|
740
|
+
else if (field.schema?.is_indexed === false && existing?.is_indexed) {
|
|
741
|
+
table.dropIndex([field.field]);
|
|
701
742
|
}
|
|
702
743
|
}
|
|
703
|
-
if (
|
|
744
|
+
if (existing) {
|
|
704
745
|
column.alter();
|
|
705
746
|
}
|
|
706
747
|
}
|
|
@@ -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,
|
|
@@ -36,11 +36,7 @@ export class MailService {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
async send(options) {
|
|
39
|
-
const payload = await emitter.emitFilter(`email.send`, options, {
|
|
40
|
-
database: getDatabase(),
|
|
41
|
-
schema: null,
|
|
42
|
-
accountability: null,
|
|
43
|
-
});
|
|
39
|
+
const payload = await emitter.emitFilter(`email.send`, options, {});
|
|
44
40
|
if (!payload)
|
|
45
41
|
return null;
|
|
46
42
|
const { template, ...emailOptions } = payload;
|
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,11 +1,7 @@
|
|
|
1
1
|
import type { Notification, PrimaryKey } from '@directus/types';
|
|
2
2
|
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
3
3
|
import { ItemsService } from './items.js';
|
|
4
|
-
import { MailService } from './mail/index.js';
|
|
5
|
-
import { UsersService } from './users.js';
|
|
6
4
|
export declare class NotificationsService extends ItemsService {
|
|
7
|
-
usersService: UsersService;
|
|
8
|
-
mailService: MailService;
|
|
9
5
|
constructor(options: AbstractServiceOptions);
|
|
10
6
|
createOne(data: Partial<Notification>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
11
7
|
sendEmail(data: Partial<Notification>): Promise<void>;
|
|
@@ -10,12 +10,8 @@ import { UsersService } from './users.js';
|
|
|
10
10
|
const env = useEnv();
|
|
11
11
|
const logger = useLogger();
|
|
12
12
|
export class NotificationsService extends ItemsService {
|
|
13
|
-
usersService;
|
|
14
|
-
mailService;
|
|
15
13
|
constructor(options) {
|
|
16
14
|
super('directus_notifications', options);
|
|
17
|
-
this.usersService = new UsersService({ schema: this.schema });
|
|
18
|
-
this.mailService = new MailService({ schema: this.schema, accountability: this.accountability });
|
|
19
15
|
}
|
|
20
16
|
async createOne(data, opts) {
|
|
21
17
|
const response = await super.createOne(data, opts);
|
|
@@ -24,7 +20,8 @@ export class NotificationsService extends ItemsService {
|
|
|
24
20
|
}
|
|
25
21
|
async sendEmail(data) {
|
|
26
22
|
if (data.recipient) {
|
|
27
|
-
const
|
|
23
|
+
const usersService = new UsersService({ schema: this.schema, knex: this.knex });
|
|
24
|
+
const user = await usersService.readOne(data.recipient, {
|
|
28
25
|
fields: ['id', 'email', 'email_notifications', 'role'],
|
|
29
26
|
});
|
|
30
27
|
if (user['email'] && user['email_notifications'] === true) {
|
|
@@ -38,7 +35,12 @@ export class NotificationsService extends ItemsService {
|
|
|
38
35
|
roles,
|
|
39
36
|
ip: null,
|
|
40
37
|
}, this.knex);
|
|
41
|
-
|
|
38
|
+
const mailService = new MailService({
|
|
39
|
+
schema: this.schema,
|
|
40
|
+
knex: this.knex,
|
|
41
|
+
accountability: this.accountability,
|
|
42
|
+
});
|
|
43
|
+
mailService
|
|
42
44
|
.send({
|
|
43
45
|
template: {
|
|
44
46
|
name: 'base',
|
|
@@ -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/services/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { merge } from 'lodash-es';
|
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { performance } from 'perf_hooks';
|
|
7
7
|
import { getCache } from '../cache.js';
|
|
8
|
+
import { RESUMABLE_UPLOADS } from '../constants.js';
|
|
8
9
|
import getDatabase, { hasDatabaseConnection } from '../database/index.js';
|
|
9
10
|
import { useLogger } from '../logger/index.js';
|
|
10
11
|
import getMailer from '../mailer.js';
|
|
@@ -12,8 +13,8 @@ import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
|
|
|
12
13
|
import { rateLimiter } from '../middleware/rate-limiter-ip.js';
|
|
13
14
|
import { SERVER_ONLINE } from '../server.js';
|
|
14
15
|
import { getStorage } from '../storage/index.js';
|
|
16
|
+
import { getAllowedLogLevels } from '../utils/get-allowed-log-levels.js';
|
|
15
17
|
import { SettingsService } from './settings.js';
|
|
16
|
-
import { RESUMABLE_UPLOADS } from '../constants.js';
|
|
17
18
|
const env = useEnv();
|
|
18
19
|
const logger = useLogger();
|
|
19
20
|
export class ServerService {
|
|
@@ -95,6 +96,12 @@ export class ServerService {
|
|
|
95
96
|
info['websocket'].heartbeat = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED'])
|
|
96
97
|
? env['WEBSOCKETS_HEARTBEAT_PERIOD']
|
|
97
98
|
: false;
|
|
99
|
+
info['websocket'].logs =
|
|
100
|
+
toBoolean(env['WEBSOCKETS_LOGS_ENABLED']) && this.accountability.admin
|
|
101
|
+
? {
|
|
102
|
+
allowedLogLevels: getAllowedLogLevels(env['WEBSOCKETS_LOGS_LEVEL'] || 'info'),
|
|
103
|
+
}
|
|
104
|
+
: false;
|
|
98
105
|
}
|
|
99
106
|
else {
|
|
100
107
|
info['websocket'] = false;
|
|
@@ -37,20 +37,20 @@ class OASSpecsService {
|
|
|
37
37
|
this.schema = options.schema;
|
|
38
38
|
}
|
|
39
39
|
async generate(host) {
|
|
40
|
-
let
|
|
40
|
+
let schemaForSpec = this.schema;
|
|
41
41
|
let permissions = [];
|
|
42
42
|
if (this.accountability && this.accountability.admin !== true) {
|
|
43
43
|
const allowedFields = await fetchAllowedFieldMap({
|
|
44
44
|
accountability: this.accountability,
|
|
45
45
|
action: 'read',
|
|
46
|
-
}, { schema, knex: this.knex });
|
|
47
|
-
|
|
48
|
-
const policies = await fetchPolicies(this.accountability, { schema, knex: this.knex });
|
|
49
|
-
permissions = await fetchPermissions({
|
|
46
|
+
}, { schema: this.schema, knex: this.knex });
|
|
47
|
+
schemaForSpec = reduceSchema(this.schema, allowedFields);
|
|
48
|
+
const policies = await fetchPolicies(this.accountability, { schema: this.schema, knex: this.knex });
|
|
49
|
+
permissions = await fetchPermissions({ policies, accountability: this.accountability }, { schema: this.schema, knex: this.knex });
|
|
50
50
|
}
|
|
51
|
-
const tags = await this.generateTags(
|
|
51
|
+
const tags = await this.generateTags(schemaForSpec);
|
|
52
52
|
const paths = await this.generatePaths(permissions, tags);
|
|
53
|
-
const components = await this.generateComponents(
|
|
53
|
+
const components = await this.generateComponents(schemaForSpec, tags);
|
|
54
54
|
const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
|
|
55
55
|
const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
|
|
56
56
|
const spec = {
|
package/dist/services/users.js
CHANGED
|
@@ -102,7 +102,10 @@ export class UsersService extends ItemsService {
|
|
|
102
102
|
*/
|
|
103
103
|
inviteUrl(email, url) {
|
|
104
104
|
const payload = { email, scope: 'invite' };
|
|
105
|
-
const token = jwt.sign(payload, getSecret(), {
|
|
105
|
+
const token = jwt.sign(payload, getSecret(), {
|
|
106
|
+
expiresIn: env['USER_INVITE_TOKEN_TTL'],
|
|
107
|
+
issuer: 'directus',
|
|
108
|
+
});
|
|
106
109
|
return (url ? new Url(url) : new Url(env['PUBLIC_URL']).addPath('admin', 'accept-invite'))
|
|
107
110
|
.setQuery('token', token)
|
|
108
111
|
.toString();
|
|
@@ -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')) {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
+
import { useEnv } from '@directus/env';
|
|
2
3
|
export function getAddress(server) {
|
|
4
|
+
const env = useEnv();
|
|
3
5
|
const address = server.address();
|
|
4
6
|
if (address === null) {
|
|
5
7
|
// Before the 'listening' event has been emitted or after calling server.close()
|
|
6
|
-
|
|
8
|
+
if (env['UNIX_SOCKET_PATH']) {
|
|
9
|
+
return env['UNIX_SOCKET_PATH'];
|
|
10
|
+
}
|
|
11
|
+
return `${env['HOST']}:${env['PORT']}`;
|
|
7
12
|
}
|
|
8
13
|
if (typeof address === 'string') {
|
|
9
14
|
// unix path
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useLogger } from '../logger/index.js';
|
|
2
|
+
const logger = useLogger();
|
|
3
|
+
export const getAllowedLogLevels = (level) => {
|
|
4
|
+
const levelValue = logger.levels.values[level];
|
|
5
|
+
if (levelValue === undefined) {
|
|
6
|
+
throw new Error(`Invalid "${level}" log level`);
|
|
7
|
+
}
|
|
8
|
+
return Object.fromEntries(Object.entries(logger.levels.values)
|
|
9
|
+
.filter(([_, value]) => value >= levelValue)
|
|
10
|
+
.sort((a, b) => a[1] - b[1]));
|
|
11
|
+
};
|
|
@@ -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
|
+
}
|