@directus/api 25.0.0 → 26.0.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 +3 -3
- package/dist/auth/drivers/oauth2.d.ts +2 -0
- package/dist/auth/drivers/oauth2.js +40 -2
- package/dist/auth/drivers/openid.js +8 -1
- package/dist/controllers/access.js +2 -2
- package/dist/controllers/comments.js +2 -2
- package/dist/controllers/dashboards.js +2 -2
- package/dist/controllers/files.js +2 -2
- package/dist/controllers/flows.js +2 -2
- package/dist/controllers/folders.js +2 -2
- package/dist/controllers/items.js +2 -2
- package/dist/controllers/notifications.js +2 -2
- package/dist/controllers/operations.js +2 -2
- package/dist/controllers/panels.js +2 -2
- package/dist/controllers/permissions.js +2 -2
- package/dist/controllers/policies.js +2 -2
- package/dist/controllers/presets.js +2 -2
- package/dist/controllers/roles.js +2 -2
- package/dist/controllers/shares.js +2 -2
- package/dist/controllers/translations.js +2 -2
- package/dist/controllers/users.js +2 -2
- package/dist/controllers/utils.js +8 -3
- package/dist/controllers/versions.js +2 -2
- package/dist/controllers/webhooks.js +1 -1
- package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
- package/dist/database/helpers/capabilities/dialects/default.js +3 -0
- package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
- package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
- package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
- package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
- package/dist/database/helpers/capabilities/index.d.ts +7 -0
- package/dist/database/helpers/capabilities/index.js +7 -0
- package/dist/database/helpers/capabilities/types.d.ts +11 -0
- package/dist/database/helpers/capabilities/types.js +15 -0
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/postgres.js +0 -4
- package/dist/database/index.js +1 -1
- package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
- package/dist/database/migrations/20250224A-visual-editor.js +35 -0
- package/dist/database/run-ast/lib/get-db-query.js +16 -4
- package/dist/logger/index.js +3 -3
- package/dist/middleware/sanitize-query.js +17 -7
- package/dist/middleware/validate-batch.js +1 -1
- package/dist/operations/item-delete/index.js +1 -1
- package/dist/operations/item-read/index.js +1 -1
- package/dist/operations/item-update/index.js +1 -1
- package/dist/permissions/lib/fetch-permissions.js +6 -4
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
- package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
- package/dist/permissions/modules/process-payload/process-payload.js +13 -4
- package/dist/permissions/types.d.ts +2 -1
- package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
- package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
- package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
- package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
- package/dist/rate-limiter.js +1 -1
- package/dist/services/assets.js +12 -2
- package/dist/services/authentication.js +2 -2
- package/dist/services/collections.js +39 -3
- package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
- package/dist/services/fields/build-collection-and-field-relations.js +55 -0
- package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
- package/dist/services/fields/get-collection-meta-updates.js +72 -0
- package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
- package/dist/services/fields/get-collection-relation-list.js +28 -0
- package/dist/services/fields.js +17 -12
- package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
- package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-field-type.js +51 -0
- package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/query.js +4 -4
- package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
- package/dist/services/graphql/resolvers/system-admin.js +207 -199
- package/dist/services/graphql/resolvers/system.d.ts +1 -7
- package/dist/services/graphql/resolvers/system.js +12 -113
- package/dist/services/graphql/schema/index.js +1 -1
- package/dist/services/graphql/schema/parse-query.d.ts +2 -2
- package/dist/services/graphql/schema/parse-query.js +6 -6
- package/dist/services/graphql/schema/read.d.ts +2 -2
- package/dist/services/graphql/schema/read.js +86 -2
- package/dist/services/graphql/schema-cache.d.ts +2 -2
- package/dist/services/graphql/schema-cache.js +1 -3
- package/dist/services/graphql/subscription.d.ts +3 -3
- package/dist/services/graphql/subscription.js +3 -3
- package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
- package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
- package/dist/services/items.d.ts +1 -0
- package/dist/services/items.js +30 -16
- package/dist/services/meta.js +4 -2
- package/dist/services/payload.d.ts +1 -0
- package/dist/services/payload.js +32 -17
- package/dist/services/shares.js +1 -1
- package/dist/services/specifications.js +10 -5
- package/dist/services/tus/lockers.d.ts +1 -1
- package/dist/services/tus/lockers.js +6 -5
- package/dist/services/tus/server.js +24 -0
- package/dist/services/users.js +1 -0
- package/dist/types/services.d.ts +2 -0
- package/dist/utils/apply-query.d.ts +1 -0
- package/dist/utils/apply-query.js +42 -31
- package/dist/utils/generate-hash.js +1 -1
- package/dist/utils/get-config-from-env.d.ts +6 -1
- package/dist/utils/get-config-from-env.js +16 -11
- package/dist/utils/get-graphql-type.js +3 -1
- package/dist/utils/is-login-redirect-allowed.js +2 -0
- package/dist/utils/redact-object.js +5 -1
- package/dist/utils/sanitize-query.d.ts +5 -2
- package/dist/utils/sanitize-query.js +34 -9
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/handlers/items.js +4 -4
- package/dist/websocket/handlers/subscribe.js +2 -2
- package/dist/websocket/messages.d.ts +7 -7
- package/dist/websocket/messages.js +1 -1
- package/package.json +58 -58
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDatabaseClient } from '../index.js';
|
|
2
|
+
import * as capabilitiesHelpers from './capabilities/index.js';
|
|
2
3
|
import * as dateHelpers from './date/index.js';
|
|
3
4
|
import * as fnHelpers from './fn/index.js';
|
|
4
5
|
import * as geometryHelpers from './geometry/index.js';
|
|
@@ -13,6 +14,7 @@ export function getHelpers(database) {
|
|
|
13
14
|
schema: new schemaHelpers[client](database),
|
|
14
15
|
sequence: new sequenceHelpers[client](database),
|
|
15
16
|
number: new numberHelpers[client](database),
|
|
17
|
+
capabilities: new capabilitiesHelpers[client](database),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
export function getFunctions(database, schema) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { KNEX_TYPES } from '@directus/constants';
|
|
2
2
|
import { type Knex } from 'knex';
|
|
3
|
-
import type { Options, SortRecord
|
|
3
|
+
import type { Options, SortRecord } from '../types.js';
|
|
4
4
|
import { SchemaHelper } from '../types.js';
|
|
5
5
|
export declare class SchemaHelperCockroachDb extends SchemaHelper {
|
|
6
6
|
changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
|
|
7
7
|
constraintName(existingName: string): string;
|
|
8
8
|
getDatabaseSize(): Promise<number | null>;
|
|
9
|
-
prepQueryParams(queryParams: Sql): Sql;
|
|
10
9
|
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
11
10
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {} from 'knex';
|
|
2
2
|
import { SchemaHelper } from '../types.js';
|
|
3
3
|
import { useEnv } from '@directus/env';
|
|
4
|
-
import { prepQueryParams } from '../utils/prep-query-params.js';
|
|
5
4
|
const env = useEnv();
|
|
6
5
|
export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
7
6
|
async changeToType(table, column, type, options = {}) {
|
|
@@ -29,9 +28,6 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
29
28
|
return null;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
|
-
prepQueryParams(queryParams) {
|
|
33
|
-
return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
|
|
34
|
-
}
|
|
35
31
|
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
|
|
36
32
|
if (hasRelationalSort) {
|
|
37
33
|
/*
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
|
-
import { SchemaHelper, type SortRecord
|
|
2
|
+
import { SchemaHelper, type SortRecord } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperPostgres extends SchemaHelper {
|
|
4
4
|
generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
|
|
5
5
|
getDatabaseSize(): Promise<number | null>;
|
|
6
|
-
prepQueryParams(queryParams: Sql): Sql;
|
|
7
6
|
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
8
7
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { getDefaultIndexName } from '../../../../utils/get-default-index-name.js';
|
|
3
3
|
import { SchemaHelper } from '../types.js';
|
|
4
|
-
import { prepQueryParams } from '../utils/prep-query-params.js';
|
|
5
4
|
const env = useEnv();
|
|
6
5
|
export class SchemaHelperPostgres extends SchemaHelper {
|
|
7
6
|
generateIndexName(type, collection, fields) {
|
|
@@ -16,9 +15,6 @@ export class SchemaHelperPostgres extends SchemaHelper {
|
|
|
16
15
|
return null;
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
|
-
prepQueryParams(queryParams) {
|
|
20
|
-
return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
|
|
21
|
-
}
|
|
22
18
|
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
|
|
23
19
|
if (hasRelationalSort) {
|
|
24
20
|
/*
|
package/dist/database/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export function getDatabase() {
|
|
|
27
27
|
const env = useEnv();
|
|
28
28
|
const logger = useLogger();
|
|
29
29
|
const metrics = useMetrics();
|
|
30
|
-
const { client, version, searchPath, connectionString, pool: poolConfig = {}, ...connectionConfig } = getConfigFromEnv('DB_',
|
|
30
|
+
const { client, version, searchPath, connectionString, pool: poolConfig = {}, ...connectionConfig } = getConfigFromEnv('DB_', { omitPrefix: 'DB_EXCLUDE_TABLES' });
|
|
31
31
|
const requiredEnvVars = ['DB_CLIENT'];
|
|
32
32
|
switch (client) {
|
|
33
33
|
case 'sqlite3':
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
3
|
+
table.json('visual_editor_urls').nullable();
|
|
4
|
+
});
|
|
5
|
+
await updateModuleBar(knex, (moduleBar) => {
|
|
6
|
+
if (moduleBar.find(({ id }) => id === 'visual'))
|
|
7
|
+
return;
|
|
8
|
+
const visualEditorModule = {
|
|
9
|
+
type: 'module',
|
|
10
|
+
id: 'visual',
|
|
11
|
+
enabled: false,
|
|
12
|
+
};
|
|
13
|
+
const contentModuleIndex = moduleBar.findIndex(({ id }) => id === 'content');
|
|
14
|
+
moduleBar.splice(contentModuleIndex + 1, 0, visualEditorModule);
|
|
15
|
+
return moduleBar;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function down(knex) {
|
|
19
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
20
|
+
table.dropColumns('visual_editor_urls');
|
|
21
|
+
});
|
|
22
|
+
await updateModuleBar(knex, (moduleBar) => moduleBar.filter(({ id }) => id !== 'visual'));
|
|
23
|
+
}
|
|
24
|
+
async function updateModuleBar(knex, modify) {
|
|
25
|
+
const result = await knex('directus_settings').select('module_bar', 'id').first();
|
|
26
|
+
if (result && result.module_bar) {
|
|
27
|
+
const moduleBar = typeof result.module_bar === 'string' ? JSON.parse(result.module_bar) : result.module_bar;
|
|
28
|
+
const updatedModuleBar = modify(moduleBar);
|
|
29
|
+
if (!updatedModuleBar)
|
|
30
|
+
return;
|
|
31
|
+
await knex('directus_settings')
|
|
32
|
+
.update({ module_bar: JSON.stringify(updatedModuleBar) })
|
|
33
|
+
.where('id', result.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -21,16 +21,28 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
|
|
|
21
21
|
// Queries with aggregates and groupBy will not have duplicate result
|
|
22
22
|
if (queryCopy.aggregate || queryCopy.group) {
|
|
23
23
|
const flatQuery = knex.from(table);
|
|
24
|
+
const fieldNodeMap = Object.fromEntries(fieldNodes.map((node, index) => [
|
|
25
|
+
node.fieldKey,
|
|
26
|
+
[node, index],
|
|
27
|
+
]));
|
|
28
|
+
const groupFieldNodes = queryCopy.group?.map((field) => fieldNodeMap[field][0]) ?? [];
|
|
24
29
|
// Map the group fields to their respective field nodes
|
|
25
|
-
const groupWhenCases = hasCaseWhen
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const groupWhenCases = hasCaseWhen ? groupFieldNodes.map((node) => node.whenCase ?? []) : undefined;
|
|
31
|
+
// Determine the number of aggregates that will be selected
|
|
32
|
+
const aggregateCount = Object.entries(queryCopy.aggregate ?? {}).reduce((acc, [_, fields]) => acc + fields.length, 0);
|
|
33
|
+
// Map the group field to their respective select column positions (1 based, offset by the number of aggregate terms that are applied in applyQuery)
|
|
34
|
+
// The positions need to be offset by the number of aggregate terms, since the aggregate terms are selected first
|
|
35
|
+
const groupColumnPositions = queryCopy.group?.map((field) => fieldNodeMap[field][1] + 1 + aggregateCount) ?? [];
|
|
28
36
|
const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
|
|
29
37
|
aliasMap,
|
|
30
38
|
groupWhenCases,
|
|
39
|
+
groupColumnPositions,
|
|
31
40
|
}).query;
|
|
32
41
|
flatQuery.select(fieldNodes.map((node) => preProcess(node)));
|
|
33
|
-
|
|
42
|
+
if (helpers.capabilities.supportsDeduplicationOfParameters() &&
|
|
43
|
+
!helpers.capabilities.supportsColumnPositionInGroupBy()) {
|
|
44
|
+
withPreprocessBindings(knex, dbQuery);
|
|
45
|
+
}
|
|
34
46
|
return dbQuery;
|
|
35
47
|
}
|
|
36
48
|
const primaryKey = schema.collections[table].primary;
|
package/dist/logger/index.js
CHANGED
|
@@ -43,7 +43,7 @@ export const createLogger = () => {
|
|
|
43
43
|
censor: REDACTED_TEXT,
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
|
-
const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
|
|
46
|
+
const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' });
|
|
47
47
|
// Expose custom log levels into formatter function
|
|
48
48
|
if (loggerEnvConfig['levels']) {
|
|
49
49
|
const customLogLevels = {};
|
|
@@ -91,8 +91,8 @@ export const createLogger = () => {
|
|
|
91
91
|
};
|
|
92
92
|
export const createExpressLogger = () => {
|
|
93
93
|
const env = useEnv();
|
|
94
|
-
const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP',
|
|
95
|
-
const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
|
|
94
|
+
const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', { omitPrefix: 'LOGGER_HTTP_LOGGER' });
|
|
95
|
+
const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' });
|
|
96
96
|
const httpLoggerOptions = {
|
|
97
97
|
level: env['LOG_LEVEL'] || 'info',
|
|
98
98
|
redact: {
|
|
@@ -4,16 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
6
6
|
import { validateQuery } from '../utils/validate-query.js';
|
|
7
|
-
const sanitizeQueryMiddleware = (req, _res, next) => {
|
|
7
|
+
const sanitizeQueryMiddleware = async (req, _res, next) => {
|
|
8
8
|
req.sanitizedQuery = {};
|
|
9
9
|
if (!req.query)
|
|
10
10
|
return;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// Skip sanitization and validation if query is empty
|
|
12
|
+
if (Object.keys(req.query).length === 0) {
|
|
13
|
+
Object.freeze(req.sanitizedQuery);
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
req.sanitizedQuery = await sanitizeQuery({
|
|
18
|
+
fields: req.query['fields'] || '*',
|
|
19
|
+
...req.query,
|
|
20
|
+
}, req.schema, req.accountability || null);
|
|
21
|
+
Object.freeze(req.sanitizedQuery);
|
|
22
|
+
validateQuery(req.sanitizedQuery);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return next(error);
|
|
26
|
+
}
|
|
17
27
|
return next();
|
|
18
28
|
};
|
|
19
29
|
export default sanitizeQueryMiddleware;
|
|
@@ -18,7 +18,7 @@ export const validateBatch = (scope) => asyncHandler(async (req, _res, next) =>
|
|
|
18
18
|
}
|
|
19
19
|
// In reads, the query in the body should override the query params for searching
|
|
20
20
|
if (scope === 'read' && req.body.query) {
|
|
21
|
-
req.sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
21
|
+
req.sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
|
|
22
22
|
validateQuery(req.sanitizedQuery);
|
|
23
23
|
}
|
|
24
24
|
// Every cRUD action has either keys or query
|
|
@@ -26,7 +26,7 @@ export default defineOperationApi({
|
|
|
26
26
|
knex: database,
|
|
27
27
|
});
|
|
28
28
|
const queryObject = query ? optionToObject(query) : {};
|
|
29
|
-
const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
|
|
29
|
+
const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
|
|
30
30
|
let result;
|
|
31
31
|
if (!key || (Array.isArray(key) && key.length === 0)) {
|
|
32
32
|
result = await itemsService.deleteByQuery(sanitizedQueryObject, { emitEvents: !!emitEvents });
|
|
@@ -26,7 +26,7 @@ export default defineOperationApi({
|
|
|
26
26
|
knex: database,
|
|
27
27
|
});
|
|
28
28
|
const queryObject = query ? optionToObject(query) : {};
|
|
29
|
-
const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
|
|
29
|
+
const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
|
|
30
30
|
let result;
|
|
31
31
|
if (!key || (Array.isArray(key) && key.length === 0)) {
|
|
32
32
|
result = await itemsService.readByQuery(sanitizedQueryObject, { emitEvents: !!emitEvents });
|
|
@@ -27,7 +27,7 @@ export default defineOperationApi({
|
|
|
27
27
|
});
|
|
28
28
|
const payloadObject = optionToObject(payload) ?? null;
|
|
29
29
|
const queryObject = query ? optionToObject(query) : {};
|
|
30
|
-
const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
|
|
30
|
+
const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
|
|
31
31
|
if (!payloadObject) {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractRequiredDynamicVariableContextForPermissions } from '../utils/extract-required-dynamic-variable-context.js';
|
|
2
|
+
import { fetchDynamicVariableData } from '../utils/fetch-dynamic-variable-data.js';
|
|
2
3
|
import { fetchRawPermissions } from '../utils/fetch-raw-permissions.js';
|
|
3
|
-
import { processPermissions } from '../utils/process-permissions.js';
|
|
4
4
|
import { getPermissionsForShare } from '../utils/get-permissions-for-share.js';
|
|
5
|
+
import { processPermissions } from '../utils/process-permissions.js';
|
|
5
6
|
export async function fetchPermissions(options, context) {
|
|
6
7
|
const permissions = await fetchRawPermissions({ ...options, bypassMinimalAppPermissions: options.bypassDynamicVariableProcessing ?? false }, context);
|
|
7
8
|
if (options.accountability && !options.bypassDynamicVariableProcessing) {
|
|
8
|
-
const
|
|
9
|
+
const dynamicVariableContext = extractRequiredDynamicVariableContextForPermissions(permissions);
|
|
10
|
+
const permissionsContext = await fetchDynamicVariableData({
|
|
9
11
|
accountability: options.accountability,
|
|
10
12
|
policies: options.policies,
|
|
11
|
-
|
|
13
|
+
dynamicVariableContext,
|
|
12
14
|
}, context);
|
|
13
15
|
// Replace dynamic variables with their actual values
|
|
14
16
|
const processedPermissions = processPermissions({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
|
-
import { validatePayload } from '@directus/utils';
|
|
2
|
+
import { parseFilter, validatePayload } from '@directus/utils';
|
|
3
3
|
import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
|
|
4
4
|
import { assign, difference, uniq } from 'lodash-es';
|
|
5
5
|
import { fetchPermissions } from '../../lib/fetch-permissions.js';
|
|
6
6
|
import { fetchPolicies } from '../../lib/fetch-policies.js';
|
|
7
7
|
import { isFieldNullable } from './lib/is-field-nullable.js';
|
|
8
|
+
import { fetchDynamicVariableData } from '../../utils/fetch-dynamic-variable-data.js';
|
|
9
|
+
import { extractRequiredDynamicVariableContext } from '../../utils/extract-required-dynamic-variable-context.js';
|
|
8
10
|
/**
|
|
9
11
|
* @note this only validates the top-level fields. The expectation is that this function is called
|
|
10
12
|
* for each level of nested insert separately
|
|
@@ -12,8 +14,8 @@ import { isFieldNullable } from './lib/is-field-nullable.js';
|
|
|
12
14
|
export async function processPayload(options, context) {
|
|
13
15
|
let permissions;
|
|
14
16
|
let permissionValidationRules = [];
|
|
17
|
+
const policies = await fetchPolicies(options.accountability, context);
|
|
15
18
|
if (!options.accountability.admin) {
|
|
16
|
-
const policies = await fetchPolicies(options.accountability, context);
|
|
17
19
|
permissions = await fetchPermissions({ action: options.action, policies, collections: [options.collection], accountability: options.accountability }, context);
|
|
18
20
|
if (permissions.length === 0) {
|
|
19
21
|
throw new ForbiddenError({
|
|
@@ -53,7 +55,14 @@ export async function processPayload(options, context) {
|
|
|
53
55
|
},
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
const permissionContext = extractRequiredDynamicVariableContext(field.validation);
|
|
59
|
+
const filterContext = await fetchDynamicVariableData({
|
|
60
|
+
accountability: options.accountability,
|
|
61
|
+
policies,
|
|
62
|
+
dynamicVariableContext: permissionContext,
|
|
63
|
+
}, context);
|
|
64
|
+
const validationFilter = parseFilter(field.validation, options.accountability, filterContext);
|
|
65
|
+
fieldValidationRules.push(validationFilter);
|
|
57
66
|
}
|
|
58
67
|
const presets = (permissions ?? []).map((permission) => permission.presets);
|
|
59
68
|
const payloadWithPresets = assign({}, ...presets, options.payload);
|
|
@@ -67,7 +76,7 @@ export async function processPayload(options, context) {
|
|
|
67
76
|
if (validationRules.length > 0) {
|
|
68
77
|
const validationErrors = [];
|
|
69
78
|
validationErrors.push(...validatePayload({ _and: validationRules }, payloadWithPresets)
|
|
70
|
-
.map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
|
|
79
|
+
.map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details, options.nested))))
|
|
71
80
|
.flat());
|
|
72
81
|
if (validationErrors.length > 0)
|
|
73
82
|
throw validationErrors;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
export interface Context {
|
|
4
4
|
schema: SchemaOverview;
|
|
5
5
|
knex: Knex;
|
|
6
|
+
accountability?: Accountability;
|
|
6
7
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Permission } from '@directus/types';
|
|
2
|
-
export interface
|
|
2
|
+
export interface DynamicVariableContext {
|
|
3
3
|
$CURRENT_USER: Set<string>;
|
|
4
4
|
$CURRENT_ROLE: Set<string>;
|
|
5
5
|
$CURRENT_ROLES: Set<string>;
|
|
6
6
|
$CURRENT_POLICIES: Set<string>;
|
|
7
7
|
}
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function extractRequiredDynamicVariableContextForPermissions(permissions: Permission[]): DynamicVariableContext;
|
|
9
|
+
export declare function extractRequiredDynamicVariableContext(val: any): DynamicVariableContext;
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { deepMap } from '@directus/utils';
|
|
2
|
-
export function
|
|
3
|
-
|
|
2
|
+
export function extractRequiredDynamicVariableContextForPermissions(permissions) {
|
|
3
|
+
let permissionContext = {
|
|
4
4
|
$CURRENT_USER: new Set(),
|
|
5
5
|
$CURRENT_ROLE: new Set(),
|
|
6
6
|
$CURRENT_ROLES: new Set(),
|
|
7
7
|
$CURRENT_POLICIES: new Set(),
|
|
8
8
|
};
|
|
9
9
|
for (const permission of permissions) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.permissions));
|
|
11
|
+
permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.validation));
|
|
12
|
+
permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.presets));
|
|
13
13
|
}
|
|
14
14
|
return permissionContext;
|
|
15
|
+
}
|
|
16
|
+
export function extractRequiredDynamicVariableContext(val) {
|
|
17
|
+
const permissionContext = {
|
|
18
|
+
$CURRENT_USER: new Set(),
|
|
19
|
+
$CURRENT_ROLE: new Set(),
|
|
20
|
+
$CURRENT_ROLES: new Set(),
|
|
21
|
+
$CURRENT_POLICIES: new Set(),
|
|
22
|
+
};
|
|
23
|
+
deepMap(val, extractPermissionData);
|
|
24
|
+
return permissionContext;
|
|
15
25
|
function extractPermissionData(val) {
|
|
16
26
|
for (const placeholder of [
|
|
17
27
|
'$CURRENT_USER',
|
|
@@ -25,3 +35,12 @@ export function extractRequiredDynamicVariableContext(permissions) {
|
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
37
|
}
|
|
38
|
+
function mergeContexts(context1, context2) {
|
|
39
|
+
const permissionContext = {
|
|
40
|
+
$CURRENT_USER: new Set([...context1.$CURRENT_USER, ...context2.$CURRENT_USER]),
|
|
41
|
+
$CURRENT_ROLE: new Set([...context1.$CURRENT_ROLE, ...context2.$CURRENT_ROLE]),
|
|
42
|
+
$CURRENT_ROLES: new Set([...context1.$CURRENT_ROLES, ...context2.$CURRENT_ROLES]),
|
|
43
|
+
$CURRENT_POLICIES: new Set([...context1.$CURRENT_POLICIES, ...context2.$CURRENT_POLICIES]),
|
|
44
|
+
};
|
|
45
|
+
return permissionContext;
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Accountability } from '@directus/types';
|
|
2
|
+
import type { Context } from '../types.js';
|
|
3
|
+
import { type DynamicVariableContext } from './extract-required-dynamic-variable-context.js';
|
|
4
|
+
export interface FetchDynamicVariableContext {
|
|
5
|
+
accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
|
|
6
|
+
policies: string[];
|
|
7
|
+
dynamicVariableContext: DynamicVariableContext;
|
|
8
|
+
}
|
|
9
|
+
export declare function fetchDynamicVariableData(options: FetchDynamicVariableContext, context: Context): Promise<Record<string, any>>;
|
package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js}
RENAMED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { getSimpleHash } from '@directus/utils';
|
|
3
3
|
import { getCache, getCacheValue, setCacheValue } from '../../cache.js';
|
|
4
|
-
import {
|
|
5
|
-
export async function
|
|
4
|
+
import {} from './extract-required-dynamic-variable-context.js';
|
|
5
|
+
export async function fetchDynamicVariableData(options, context) {
|
|
6
6
|
const { UsersService } = await import('../../services/users.js');
|
|
7
7
|
const { RolesService } = await import('../../services/roles.js');
|
|
8
8
|
const { PoliciesService } = await import('../../services/policies.js');
|
|
9
9
|
const contextData = {};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', permissionContext, { user: options.accountability.user }, async (fields) => {
|
|
10
|
+
if (options.accountability.user && (options.dynamicVariableContext.$CURRENT_USER?.size ?? 0) > 0) {
|
|
11
|
+
contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', options.dynamicVariableContext, { user: options.accountability.user }, async (fields) => {
|
|
13
12
|
const usersService = new UsersService(context);
|
|
14
13
|
return await usersService.readOne(options.accountability.user, {
|
|
15
14
|
fields,
|
|
16
15
|
});
|
|
17
16
|
});
|
|
18
17
|
}
|
|
19
|
-
if (options.accountability.role && (
|
|
20
|
-
contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE',
|
|
18
|
+
if (options.accountability.role && (options.dynamicVariableContext.$CURRENT_ROLE?.size ?? 0) > 0) {
|
|
19
|
+
contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', options.dynamicVariableContext, { role: options.accountability.role }, async (fields) => {
|
|
21
20
|
const rolesService = new RolesService(context);
|
|
22
21
|
return await rolesService.readOne(options.accountability.role, {
|
|
23
22
|
fields,
|
|
24
23
|
});
|
|
25
24
|
});
|
|
26
25
|
}
|
|
27
|
-
if (options.accountability.roles.length > 0 && (
|
|
28
|
-
contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES',
|
|
26
|
+
if (options.accountability.roles.length > 0 && (options.dynamicVariableContext.$CURRENT_ROLES?.size ?? 0) > 0) {
|
|
27
|
+
contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES', options.dynamicVariableContext, { roles: options.accountability.roles }, async (fields) => {
|
|
29
28
|
const rolesService = new RolesService(context);
|
|
30
29
|
return await rolesService.readMany(options.accountability.roles, {
|
|
31
30
|
fields,
|
|
@@ -33,10 +32,10 @@ export async function fetchDynamicVariableContext(options, context) {
|
|
|
33
32
|
});
|
|
34
33
|
}
|
|
35
34
|
if (options.policies.length > 0) {
|
|
36
|
-
if ((
|
|
35
|
+
if ((options.dynamicVariableContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
|
|
37
36
|
// Always add the id field
|
|
38
|
-
|
|
39
|
-
contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES',
|
|
37
|
+
options.dynamicVariableContext.$CURRENT_POLICIES.add('id');
|
|
38
|
+
contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', options.dynamicVariableContext, { policies: options.policies }, async (fields) => {
|
|
40
39
|
const policiesService = new PoliciesService(context);
|
|
41
40
|
return await policiesService.readMany(options.policies, {
|
|
42
41
|
fields,
|
package/dist/rate-limiter.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides
|
|
|
16
16
|
}
|
|
17
17
|
export { RateLimiterRes };
|
|
18
18
|
function getConfig(store = 'memory', configPrefix = 'RATE_LIMITER', overrides) {
|
|
19
|
-
const config = getConfigFromEnv(`${configPrefix}_`, `${configPrefix}_${store}_`);
|
|
19
|
+
const config = getConfigFromEnv(`${configPrefix}_`, { omitPrefix: `${configPrefix}_${store}_` });
|
|
20
20
|
if (store === 'redis') {
|
|
21
21
|
const Redis = require('ioredis');
|
|
22
22
|
const env = useEnv();
|
package/dist/services/assets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { ForbiddenError, IllegalAssetTransformationError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
|
|
2
|
+
import { ForbiddenError, IllegalAssetTransformationError, InvalidQueryError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
|
|
3
3
|
import { clamp } from 'lodash-es';
|
|
4
4
|
import { contentType } from 'mime-types';
|
|
5
5
|
import hash from 'object-hash';
|
|
@@ -130,7 +130,17 @@ export class AssetsService {
|
|
|
130
130
|
});
|
|
131
131
|
if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
|
|
132
132
|
transformer.rotate();
|
|
133
|
-
|
|
133
|
+
try {
|
|
134
|
+
for (const [method, ...args] of transforms) {
|
|
135
|
+
transformer[method].apply(transformer, args);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof Error && error.message.startsWith('Expected')) {
|
|
140
|
+
throw new InvalidQueryError({ reason: error.message });
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
134
144
|
const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
135
145
|
readStream.on('error', (e) => {
|
|
136
146
|
logger.error(e, `Couldn't transform file ${file.id}`);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
2
|
-
import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
|
|
3
1
|
import { Action } from '@directus/constants';
|
|
4
2
|
import { useEnv } from '@directus/env';
|
|
5
3
|
import { InvalidCredentialsError, InvalidOtpError, ServiceUnavailableError, UserSuspendedError, } from '@directus/errors';
|
|
@@ -10,6 +8,8 @@ import { getAuthProvider } from '../auth.js';
|
|
|
10
8
|
import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
|
|
11
9
|
import getDatabase from '../database/index.js';
|
|
12
10
|
import emitter from '../emitter.js';
|
|
11
|
+
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
12
|
+
import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
|
|
13
13
|
import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
|
|
14
14
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
15
15
|
import { getSecret } from '../utils/get-secret.js';
|
|
@@ -15,6 +15,9 @@ import { getSchema } from '../utils/get-schema.js';
|
|
|
15
15
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
16
16
|
import { transaction } from '../utils/transaction.js';
|
|
17
17
|
import { FieldsService } from './fields.js';
|
|
18
|
+
import { buildCollectionAndFieldRelations } from './fields/build-collection-and-field-relations.js';
|
|
19
|
+
import { getCollectionMetaUpdates } from './fields/get-collection-meta-updates.js';
|
|
20
|
+
import { getCollectionRelationList } from './fields/get-collection-relation-list.js';
|
|
18
21
|
import { ItemsService } from './items.js';
|
|
19
22
|
export class CollectionsService {
|
|
20
23
|
knex;
|
|
@@ -41,8 +44,11 @@ export class CollectionsService {
|
|
|
41
44
|
if (this.accountability && this.accountability.admin !== true) {
|
|
42
45
|
throw new ForbiddenError();
|
|
43
46
|
}
|
|
44
|
-
if (!payload
|
|
47
|
+
if (!('collection' in payload))
|
|
45
48
|
throw new InvalidPayloadError({ reason: `"collection" is required` });
|
|
49
|
+
if (typeof payload.collection !== 'string' || payload.collection === '') {
|
|
50
|
+
throw new InvalidPayloadError({ reason: `"collection" must be a non-empty string` });
|
|
51
|
+
}
|
|
46
52
|
if (payload.collection.startsWith('directus_')) {
|
|
47
53
|
throw new InvalidPayloadError({ reason: `Collections can't start with "directus_"` });
|
|
48
54
|
}
|
|
@@ -61,10 +67,13 @@ export class CollectionsService {
|
|
|
61
67
|
// transactions.
|
|
62
68
|
await transaction(this.knex, async (trx) => {
|
|
63
69
|
if (payload.schema) {
|
|
70
|
+
if ('fields' in payload && !Array.isArray(payload.fields)) {
|
|
71
|
+
throw new InvalidPayloadError({ reason: `"fields" must be an array` });
|
|
72
|
+
}
|
|
64
73
|
// Directus heavily relies on the primary key of a collection, so we have to make sure that
|
|
65
74
|
// every collection that is created has a primary key. If no primary key field is created
|
|
66
75
|
// while making the collection, we default to an auto incremented id named `id`
|
|
67
|
-
if (!payload.fields) {
|
|
76
|
+
if (!payload.fields || payload.fields.length === 0) {
|
|
68
77
|
payload.fields = [
|
|
69
78
|
{
|
|
70
79
|
field: 'id',
|
|
@@ -478,7 +487,18 @@ export class CollectionsService {
|
|
|
478
487
|
accountability: this.accountability,
|
|
479
488
|
schema: this.schema,
|
|
480
489
|
});
|
|
481
|
-
|
|
490
|
+
const fieldItemsService = new ItemsService('directus_fields', {
|
|
491
|
+
knex: trx,
|
|
492
|
+
accountability: this.accountability,
|
|
493
|
+
schema: this.schema,
|
|
494
|
+
});
|
|
495
|
+
await fieldItemsService.deleteByQuery({
|
|
496
|
+
filter: {
|
|
497
|
+
collection: { _eq: collectionKey },
|
|
498
|
+
},
|
|
499
|
+
}, {
|
|
500
|
+
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
501
|
+
});
|
|
482
502
|
await trx('directus_presets').delete().where('collection', '=', collectionKey);
|
|
483
503
|
const revisionsToDelete = await trx
|
|
484
504
|
.select('id')
|
|
@@ -494,6 +514,22 @@ export class CollectionsService {
|
|
|
494
514
|
await trx('directus_activity').delete().where('collection', '=', collectionKey);
|
|
495
515
|
await trx('directus_permissions').delete().where('collection', '=', collectionKey);
|
|
496
516
|
await trx('directus_relations').delete().where({ many_collection: collectionKey });
|
|
517
|
+
const { collectionRelationTree, fieldToCollectionList } = await buildCollectionAndFieldRelations(this.schema.relations);
|
|
518
|
+
const collectionRelationList = getCollectionRelationList(collectionKey, collectionRelationTree);
|
|
519
|
+
// only process duplication fields if related collections have them
|
|
520
|
+
if (collectionRelationList.size !== 0) {
|
|
521
|
+
const collectionMetas = await trx
|
|
522
|
+
.select('collection', 'archive_field', 'sort_field', 'item_duplication_fields')
|
|
523
|
+
.from('directus_collections')
|
|
524
|
+
.whereIn('collection', Array.from(collectionRelationList))
|
|
525
|
+
.whereNotNull('item_duplication_fields');
|
|
526
|
+
await Promise.all(Object.keys(this.schema.collections[collectionKey]?.fields ?? {}).map(async (fieldKey) => {
|
|
527
|
+
const collectionMetaUpdates = getCollectionMetaUpdates(collectionKey, fieldKey, collectionMetas, this.schema.collections, fieldToCollectionList);
|
|
528
|
+
for (const meta of collectionMetaUpdates) {
|
|
529
|
+
await trx('directus_collections').update(meta.updates).where({ collection: meta.collection });
|
|
530
|
+
}
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
497
533
|
const relations = this.schema.relations.filter((relation) => {
|
|
498
534
|
return relation.collection === collectionKey || relation.related_collection === collectionKey;
|
|
499
535
|
});
|